把卫星电话理解为允许保留 S 个无线连通块,在完全图上 Kruskal 到剩 S 个集合。
OJ: luogu
题目 ID: P1991
难度:普及/提高-
标签:最小生成树Kruskal并查集几何聚类
日期: 2026-06-22 21:54
题意
有 P 个哨所,其中可以给 S 个哨所配卫星电话。任意两个有卫星电话的哨所可以无视距离通信。
普通无线电的通信距离统一为 D。要求选择尽量小的 D,使所有哨所最终都能直接或间接连通。
思路
先看一个可以直接验证想法的朴素解:
cpp
#include <bits/stdc++.h>
using namespace std;
// brute.cpp:枚举小数据的卫星分块划分,验证最小 D。
const int MAXP = 12;
int s, p;
int x_pos[MAXP], y_pos[MAXP];
int group_id[MAXP];
double answer;
double distance_between(int i, int j) {
double dx = x_pos[i] - x_pos[j];
double dy = y_pos[i] - y_pos[j];
return sqrt(dx * dx + dy * dy);
}
void read_input() {
cin >> s >> p;
for (int i = 1; i <= p; i++) {
cin >> x_pos[i] >> y_pos[i];
}
}
void evaluate() {
double need = 0.0;
for (int g = 1; g <= s; g++) {
vector<int> nodes;
for (int i = 1; i <= p; i++) {
if (group_id[i] == g) {
nodes.push_back(i);
}
}
if ((int)nodes.size() <= 1) {
continue;
}
bool used[MAXP];
double dist[MAXP];
for (int i = 0; i < (int)nodes.size(); i++) {
used[i] = false;
dist[i] = 1e100;
}
dist[0] = 0.0;
for (int step = 0; step < (int)nodes.size(); step++) {
int u = -1;
for (int i = 0; i < (int)nodes.size(); i++) {
if (!used[i] && (u == -1 || dist[i] < dist[u])) {
u = i;
}
}
used[u] = true;
need = max(need, dist[u]);
for (int v = 0; v < (int)nodes.size(); v++) {
if (!used[v]) {
double w = distance_between(nodes[u], nodes[v]);
if (w < dist[v]) {
dist[v] = w;
}
}
}
}
}
answer = min(answer, need);
}
void dfs(int pos, int used_groups) {
if (pos == p + 1) {
if (used_groups == s) {
evaluate();
}
return;
}
for (int g = 1; g <= used_groups; g++) {
group_id[pos] = g;
dfs(pos + 1, used_groups);
}
if (used_groups < s) {
group_id[pos] = used_groups + 1;
dfs(pos + 1, used_groups + 1);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
read_input();
answer = 1e100;
group_id[1] = 1;
dfs(2, 1);
cout << fixed << setprecision(2) << answer << '\n';
return 0;
}如果没有卫星电话,所有哨所必须靠无线电连成一张图,答案就是最小生成树中的最大边。
现在有 S 个卫星电话,可以把最多 S 个无线连通块用卫星连接起来。所以问题变成:用无线电把所有点连成 S 个连通块,并让块内需要的最大边尽量小。
这就是 Kruskal 聚类:
- 枚举所有哨所点对,边权为欧氏距离;
- 按距离从小到大排序;
- 用并查集合并不同连通块;
- 当连通块数量降到
S时停止; - 最后一次加入的边长,就是最小通信距离
D。
也可以理解为:先建完整 MST,再删掉最大的 S-1 条边。剩下每个连通块内部靠无线电,块与块之间靠卫星电话。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXP = 505;
const int MAXE = 130000;
struct Edge {
int u, v;
double w;
};
int s, p;
int x_pos[MAXP], y_pos[MAXP];
Edge edge[MAXE];
int edge_count;
int father[MAXP];
bool cmp_edge(const Edge &a, const Edge &b) {
return a.w < b.w;
}
int find_root(int x) {
if (father[x] == x) {
return x;
}
father[x] = find_root(father[x]);
return father[x];
}
bool merge_set(int x, int y) {
int rx = find_root(x);
int ry = find_root(y);
if (rx == ry) {
return false;
}
father[rx] = ry;
return true;
}
double distance_between(int i, int j) {
double dx = x_pos[i] - x_pos[j];
double dy = y_pos[i] - y_pos[j];
return sqrt(dx * dx + dy * dy);
}
void read_input() {
cin >> s >> p;
for (int i = 1; i <= p; i++) {
cin >> x_pos[i] >> y_pos[i];
}
}
void build_edges() {
edge_count = 0;
for (int i = 1; i <= p; i++) {
for (int j = i + 1; j <= p; j++) {
edge_count++;
edge[edge_count].u = i;
edge[edge_count].v = j;
edge[edge_count].w = distance_between(i, j);
}
}
}
void solve() {
build_edges();
sort(edge + 1, edge + edge_count + 1, cmp_edge);
for (int i = 1; i <= p; i++) {
father[i] = i;
}
int components = p;
double answer = 0.0;
for (int i = 1; i <= edge_count && components > s; i++) {
if (merge_set(edge[i].u, edge[i].v)) {
answer = edge[i].w;
components--;
}
}
cout << fixed << setprecision(2) << answer << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
read_input();
solve();
return 0;
}复杂度
完全图有
空间复杂度为
总结
卫星电话的作用不是改变边权,而是允许最终保留多个无线连通块。
因此 Kruskal 不必做到全图连通,只要合并到剩下 S 个连通块即可。