[HAOI2006] 聪明的猴子

GitHub跳转原题关系图返回列表

把两棵树之间能否跳过去看成边,所有树都能互达所需的最小跳距,等于一棵最小生成树中的最大边长;用 Prim 求出这个临界值后统计能达到的猴子数量。

OJ: luogu

题目 ID: P2504

难度:普及+/提高

标签:图论最小生成树贪心

日期: 2026-06-20 00:48

题意

给出若干只猴子的最大跳跃距离,以及 N 棵树的坐标。

如果一只猴子能在所有露出水面的树冠之间来回穿梭,也就是从任意一棵树出发,都能通过若干次跳跃到达其它所有树,那么这只猴子就算“可以在所有树冠上觅食”。

问一共有多少只猴子满足条件。

思路

先看一个直接按定义判断的小数据暴力:

cpp
// brute.cpp:枚举所有可能的跳跃阈值,直接检查图是否连通。
#include <bits/stdc++.h>
using namespace std;

const int MAXM = 505;
const int MAXN = 1005;

int monkey_cnt, tree_cnt;
long long jump_len[MAXM];
long long x[MAXN], y[MAXN];
bool vis[MAXN];

long long dis2(int i, int j) {
    long long dx = x[i] - x[j];
    long long dy = y[i] - y[j];
    return dx * dx + dy * dy;
}

bool connected(long long limit2) {
    queue<int> q;
    for (int i = 1; i <= tree_cnt; i++) {
        vis[i] = false;
    }

    q.push(1);
    vis[1] = true;
    int cnt = 1;

    while (!q.empty()) {
        int u = q.front();
        q.pop();

        for (int v = 1; v <= tree_cnt; v++) {
            if (vis[v]) {
                continue;
            }
            if (dis2(u, v) > limit2) {
                continue;
            }
            vis[v] = true;
            q.push(v);
            cnt++;
        }
    }

    return cnt == tree_cnt;
}

long long brute_need() {
    vector<long long> cand;
    cand.push_back(0);

    for (int i = 1; i <= tree_cnt; i++) {
        for (int j = i + 1; j <= tree_cnt; j++) {
            cand.push_back(dis2(i, j));
        }
    }

    sort(cand.begin(), cand.end());
    cand.erase(unique(cand.begin(), cand.end()), cand.end());

    for (int i = 0; i < (int)cand.size(); i++) {
        if (connected(cand[i])) {
            return cand[i];
        }
    }

    return 0;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> monkey_cnt;
    for (int i = 1; i <= monkey_cnt; i++) {
        cin >> jump_len[i];
    }

    cin >> tree_cnt;
    for (int i = 1; i <= tree_cnt; i++) {
        cin >> x[i] >> y[i];
    }

    long long need = brute_need();

    int answer = 0;
    for (int i = 1; i <= monkey_cnt; i++) {
        if (jump_len[i] * jump_len[i] >= need) {
            answer++;
        }
    }

    cout << answer << '\n';

    return 0;
}

暴力做法是:

  1. 枚举一个跳跃上限 D
  2. 把距离不超过 D 的两棵树连边
  3. 检查整张图是否连通
  4. 找到让图第一次连通的最小 D

这个做法很好理解,但如果直接枚举所有可能距离并反复建图检查,效率一般。

关键观察是:题目真正要问的,不是哪只猴子能跳哪条边,而是:

想让所有树互相可达,单次跳跃距离至少要多大?

把每棵树看成点,两棵树之间的欧几里得距离看成边权。

如果某个跳跃上限 D 足够,那么只保留边权不超过 D 的边,图就应该连通。

而这个“最小可行的 D”,正好等于一棵最小生成树里的最大边长

原因是:

  • 最小生成树把所有点连起来
  • 在所有生成树里,它会尽量让大边也压低
  • 所以它的最大边,正是连通全图所需的最小临界值

这题点数只有 1000,直接用 Prim O(n^2) 很合适:

  1. 逐步把点加入生成树
  2. 每次记录新加入那条边的长度
  3. 这些边里最大的那一个,就是所需最小跳距

最后再统计有多少只猴子的最大跳跃距离不小于这个值即可。

实现时为了避免开根号,代码里全程比较距离平方:

  • 边长用平方保存
  • 猴子的跳跃能力也改成平方比较

代码

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXM = 505;
const int MAXN = 1005;
const long long INF = (1LL << 62);

int monkey_cnt, tree_cnt;
long long jump_len[MAXM];
long long x[MAXN], y[MAXN];
long long dist_to_tree[MAXN];
bool vis[MAXN];

long long dis2(int i, int j) {
    long long dx = x[i] - x[j];
    long long dy = y[i] - y[j];
    return dx * dx + dy * dy;
}

long long prim_need() {
    for (int i = 1; i <= tree_cnt; i++) {
        dist_to_tree[i] = INF;
        vis[i] = false;
    }

    dist_to_tree[1] = 0;
    long long need = 0;

    for (int i = 1; i <= tree_cnt; i++) {
        int u = 0;
        for (int j = 1; j <= tree_cnt; j++) {
            if (vis[j]) {
                continue;
            }
            if (u == 0 || dist_to_tree[j] < dist_to_tree[u]) {
                u = j;
            }
        }

        vis[u] = true;
        need = max(need, dist_to_tree[u]);

        for (int v = 1; v <= tree_cnt; v++) {
            if (vis[v]) {
                continue;
            }
            long long w = dis2(u, v);
            if (w < dist_to_tree[v]) {
                dist_to_tree[v] = w;
            }
        }
    }

    return need;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> monkey_cnt;
    for (int i = 1; i <= monkey_cnt; i++) {
        cin >> jump_len[i];
    }

    cin >> tree_cnt;
    for (int i = 1; i <= tree_cnt; i++) {
        cin >> x[i] >> y[i];
    }

    long long need = prim_need();

    int answer = 0;
    for (int i = 1; i <= monkey_cnt; i++) {
        if (jump_len[i] * jump_len[i] >= need) {
            answer++;
        }
    }

    cout << answer << '\n';

    return 0;
}

复杂度

设树的数量为 N

Prim 使用邻接矩阵式的在线更新,不显式存所有边:

  • 时间复杂度 O(N2)O(N^2)
  • 空间复杂度 O(N)O(N)

总结

这题表面是在统计猴子,其实先要抽出一个图论核心问题:全图连通需要的最小单跳距离是多少。一旦把它识别成“最小生成树里的最大边”,后面就是一题很标准的 MST 变形。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析