题目给的是完整邻接矩阵,直接用 Prim 维护每个未选点到当前生成树的最小连边代价,逐个把点加入生成树即可。
OJ: luogu
题目 ID: P1546
难度:普及-
标签:图论最小生成树贪心
日期: 2026-06-20 00:36
题意
给出 n 个农场之间两两连线的代价矩阵,要求选出一种连线方案:
- 能让所有农场都连通
- 总花费最小
这就是一张无向带权图上的最小生成树问题。
思路
先看一个最直接的小数据暴力:
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;
int edge_cnt;
int picked[MAXM];
int best_answer = INF;
int fa[MAXN];
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];
}
void unite(int x, int y) {
x = find_root(x);
y = find_root(y);
if (x != y) {
fa[x] = y;
}
}
bool check_tree(int picked_cnt) {
if (picked_cnt != n - 1) {
return false;
}
init_dsu();
for (int i = 1; i <= picked_cnt; i++) {
int id = picked[i];
unite(edges[id].u, edges[id].v);
}
int root = find_root(1);
for (int i = 2; i <= n; i++) {
if (find_root(i) != root) {
return false;
}
}
return true;
}
void dfs(int pos, int picked_cnt, int sum) {
if (picked_cnt > n - 1) {
return;
}
if (sum >= best_answer) {
return;
}
if (pos > edge_cnt) {
if (check_tree(picked_cnt)) {
best_answer = min(best_answer, sum);
}
return;
}
picked[picked_cnt + 1] = pos;
dfs(pos + 1, picked_cnt + 1, sum + edges[pos].w);
dfs(pos + 1, picked_cnt, sum);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
edge_cnt = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
int w;
cin >> w;
if (j > i) {
edges[++edge_cnt] = {i, j, w};
}
}
}
dfs(1, 0, 0);
cout << best_answer << '\n';
return 0;
}暴力按最小生成树的定义做:
- 从所有边里选出
n-1条 - 判断这
n-1条边能不能把所有点连起来 - 在所有合法生成树里取边权和最小的那个
这个方法非常直观,但边一多就完全不可用。
题目输入给的是完整邻接矩阵,图非常稠密,直接用 Prim 会比 Kruskal 更顺手。
Prim 的想法是:
- 先任选一个点放进生成树
- 对每个还没加入生成树的点,维护它和当前生成树之间的最小连边代价
- 每次选其中代价最小的点加入生成树
- 用这个新点去更新其它点的最小连边代价
为什么这样是对的?
因为每一步都在“已选点集合”和“未选点集合”之间,选了一条最便宜的跨边。根据最小生成树的切分性质,这样的边一定可以放心加入答案。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 105;
const int INF = 1e9;
int n;
int cost[MAXN][MAXN];
int dist_to_tree[MAXN];
bool vis[MAXN];
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;
}
}
vis[u] = true;
answer += dist_to_tree[u];
for (int v = 1; v <= n; v++) {
if (vis[v]) {
continue;
}
if (cost[u][v] < dist_to_tree[v]) {
dist_to_tree[v] = cost[u][v];
}
}
}
return answer;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
cin >> cost[i][j];
}
}
cout << prim() << '\n';
return 0;
}复杂度
设点数为 n。
邻接矩阵版 Prim 每次找一个新点,并顺手更新一整行代价:
- 时间复杂度
- 空间复杂度
总结
这题的本质就是最小生成树。因为输入天然就是矩阵,直接写 Prim 最自然:维护“每个点离当前生成树最近的代价”,一轮一轮扩展即可。