把居住点建成完全图,按距离做 Kruskal 聚类,剩 k 个集合时的下一条跨集合边就是答案。
OJ: luogu
题目 ID: P4047
难度:普及+/提高
标签:最小生成树Kruskal并查集几何贪心
日期: 2026-01-03 10:27
题意
给定平面上的 n 个居住点,要把它们划分成 k 个部落。
两个部落之间的距离,定义为两个部落中最近一对点的距离。要求最大化“最近的两个部落之间的距离”,输出这个最大值。
思路
先看一个可以直接验证想法的朴素解:
cpp
#include <bits/stdc++.h>
using namespace std;
// brute.cpp:枚举小数据的 k 个部落划分,用来辅助对拍。
const int MAXN = 12;
int n, k;
int x_pos[MAXN], y_pos[MAXN];
int group_id[MAXN];
double answer;
double dist_point(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 >> n >> k;
for (int i = 1; i <= n; i++) {
cin >> x_pos[i] >> y_pos[i];
}
}
void evaluate() {
double nearest = 1e100;
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++) {
if (group_id[i] != group_id[j]) {
nearest = min(nearest, dist_point(i, j));
}
}
}
answer = max(answer, nearest);
}
void dfs(int pos, int used_groups) {
if (pos == n + 1) {
if (used_groups == k) {
evaluate();
}
return;
}
for (int g = 1; g <= used_groups; g++) {
group_id[pos] = g;
dfs(pos + 1, used_groups);
}
if (used_groups < k) {
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 = 0.0;
group_id[1] = 1;
dfs(2, 1);
cout << fixed << setprecision(2) << answer << '\n';
return 0;
}暴力枚举所有划分方法,可以直接检查定义,但划分数量太多,只能用于小数据对拍。
把每个居住点看作点,任意两个点之间连边,边权为欧氏距离。题目就变成:把完全图的点分成 k 个簇,使不同簇之间的最小边尽量大。
直觉上,距离越近的点越应该放在同一个部落里,否则答案会被这条短距离限制住。于是按距离从小到大做 Kruskal:
- 一开始每个点单独一个集合;
- 按距离从小到大枚举点对;
- 如果两点不在同一集合,并且当前集合数还大于
k,就合并; - 当集合数已经等于
k时,遇到的第一条跨集合边,就是最近的两个部落距离。
代码里用平方距离排序,避免比较浮点数。最终输出答案时再开平方并保留两位小数。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
const int MAXE = 1000005;
struct Edge {
int u, v;
long long dist2;
};
int n, k;
int x_pos[MAXN], y_pos[MAXN];
Edge edge[MAXE];
int edge_count;
int father[MAXN];
bool cmp_edge(const Edge &a, const Edge &b) {
return a.dist2 < b.dist2;
}
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;
}
long long square_dist(int i, int j) {
long long dx = x_pos[i] - x_pos[j];
long long dy = y_pos[i] - y_pos[j];
return dx * dx + dy * dy;
}
void read_input() {
cin >> n >> k;
for (int i = 1; i <= n; i++) {
cin >> x_pos[i] >> y_pos[i];
}
}
void build_edges() {
edge_count = 0;
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++) {
edge_count++;
edge[edge_count].u = i;
edge[edge_count].v = j;
edge[edge_count].dist2 = square_dist(i, j);
}
}
}
void solve() {
build_edges();
sort(edge + 1, edge + edge_count + 1, cmp_edge);
for (int i = 1; i <= n; i++) {
father[i] = i;
}
int components = n;
for (int i = 1; i <= edge_count; i++) {
int u = edge[i].u;
int v = edge[i].v;
if (find_root(u) == find_root(v)) {
continue;
}
if (components == k) {
double answer = sqrt((double)edge[i].dist2);
cout << fixed << setprecision(2) << answer << '\n';
return;
}
merge_set(u, v);
components--;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
read_input();
solve();
return 0;
}复杂度
完全图有
空间复杂度为
总结
这道题本质是 Kruskal 聚类。
最小生成树不一定要完整建出来:当连通块数剩下 k 个时停止合并,下一条连接不同集合的边就是最优部落间距。
