[USACO14MAR] Watering the Fields S

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

这是带门槛的最小生成树:只有距离平方不小于 c 的边允许使用,直接在完全图上做 Prim,若中途出现不可达点则答案不存在。

OJ: luogu

题目 ID: P2212

难度:普及+/提高

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

日期: 2026-06-20 01:03

题意

给出 n 个点的坐标。

两点之间连边的代价定义为距离平方:

(x_i-x_j)^2 + (y_i-y_j)^2

但题目规定:如果这条边的代价小于 c,那么这条边根本不能用。

问在只允许使用代价不小于 c 的边时,能否把所有点连通;如果能,最小总代价是多少;如果不能,输出 -1

思路

先看一个只适合小数据的暴力:

cpp
// brute.cpp:小图枚举所有允许边的生成树。
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 10;
const int MAXM = 50;
const int INF = 1e9;

struct Edge {
    int u, v, w;
} edges[MAXM];

int n, limit_c;
int x[MAXN], y[MAXN];
int edge_cnt;
int picked[MAXM];
int fa[MAXN];
int best_answer = INF;

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

void init_dsu() {
    for (int i = 1; i <= n; i++) {
        fa[i] = i;
    }
}

int find_root(int x) {
    if (fa[x] == x) {
        return x;
    }
    fa[x] = find_root(fa[x]);
    return fa[x];
}

bool unite(int x, int y) {
    x = find_root(x);
    y = find_root(y);
    if (x == y) {
        return false;
    }
    fa[x] = y;
    return true;
}

void check_tree(int picked_cnt) {
    if (picked_cnt != n - 1) {
        return;
    }

    init_dsu();
    int sum = 0;

    for (int i = 1; i <= picked_cnt; i++) {
        Edge &e = edges[picked[i]];
        if (!unite(e.u, e.v)) {
            return;
        }
        sum += e.w;
    }

    int root = find_root(1);
    for (int i = 2; i <= n; i++) {
        if (find_root(i) != root) {
            return;
        }
    }

    best_answer = min(best_answer, sum);
}

void dfs(int pos, int picked_cnt) {
    if (picked_cnt > n - 1) {
        return;
    }
    if (pos > edge_cnt) {
        check_tree(picked_cnt);
        return;
    }

    picked[picked_cnt + 1] = pos;
    dfs(pos + 1, picked_cnt + 1);
    dfs(pos + 1, picked_cnt);
}

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

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

    edge_cnt = 0;
    for (int i = 1; i <= n; i++) {
        for (int j = i + 1; j <= n; j++) {
            int w = dis2(i, j);
            if (w < limit_c) {
                continue;
            }
            edges[++edge_cnt] = {i, j, w};
        }
    }

    dfs(1, 0);

    if (best_answer == INF) {
        cout << -1 << '\n';
    } else {
        cout << best_answer << '\n';
    }

    return 0;
}

暴力直接把所有“允许使用”的边列出来,然后枚举哪些边能组成生成树,取总代价最小的那个。

这个思路按定义完全正确,但边数一大就不能枚举了。

这题本质上仍然是最小生成树,只不过有一条额外限制:

  • 边权 < c 的边不能选

也就是说,我们是在一张“删掉非法边后的图”上求 MST。

由于原图本来是完全图,而 n <= 2000,不显式存下全部边会更省事,所以直接用 Prim O(n^2)

  1. 初始任选一个点加入生成树
  2. dist_to_tree[i] 维护点 i 到当前生成树的最小合法边权
  3. 每次选一个 dist_to_tree 最小的未访问点加入答案
  4. 再用这个点去更新其它点的最优接入代价

如果某一轮最小的 dist_to_tree 仍然是无穷大,说明剩下点都无法通过合法边接到当前生成树上,答案就是 -1

代码

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

const int MAXN = 2005;
const int INF = 1e9;

int n, limit_c;
int x[MAXN], y[MAXN];
int dist_to_tree[MAXN];
bool vis[MAXN];

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

int prim() {
    for (int i = 1; i <= n; i++) {
        dist_to_tree[i] = INF;
        vis[i] = false;
    }

    dist_to_tree[1] = 0;
    int answer = 0;

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

        if (u == 0 || dist_to_tree[u] == INF) {
            return -1;
        }

        vis[u] = true;
        answer += dist_to_tree[u];

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

    return answer;
}

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

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

    cout << prim() << '\n';

    return 0;
}

复杂度

设点数为 n

Prim 每轮:

  • 找一个最便宜接入的新点
  • 再扫一遍所有点更新距离

所以:

  • 时间复杂度 O(n2)O(n^2)
  • 空间复杂度 O(n)O(n)

总结

这题难点不在“距离平方”这个细节,而在先看出它仍然是 MST:只不过把边权小于 c 的边全部禁用了。识别成“带门槛的最小生成树”后,Prim 就能直接处理。

一图流解析

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

一图流解析