最小生成树
1. 最小生成树原理
- 最小生成树算法是指:
Prim算法
和Krusskal算法
。下面给出两个算法的算法过程以及正确性证明(证明都是相同的)
最小生成树的理论基础
(1)任何一条最小生成树一定可以包含无向图中权值最小的边;
(2)给定一张无向图G=(V, E),n=|V|,m=|E|。从E中选出k(<n-1)条边构成G的一个生成森林,若再从剩余的m-k条边中选n-1-k条边添加到生成森林中,使其成为G的生成树,并且选出的边的权值之和最小,则该生成树一定可以包含m-k条边中连接生成森林的两个不连通节点的权值最小的边。
Prim原理
Krusskal算法
AcWing 858. Prim算法求最小生成树
代码模板
#include <cstring>
#include <iostream>
using namespace std;
const int N = 510, INF = 0x3f3f3f3f;
int n, m; // 点数,边数
int g[N][N]; // 邻接矩阵
int dist[N]; // dist[j] 表示从已经求得mst的集合中到达j点的最短的一条
bool st[N]; // 已在mst集合中的点
int prim() {
memset(dist, 0x3f, sizeof dist);
int res = 0; // 记录整个图的MST最小权值和
for (int i = 0; i < n; ++i) {
// 寻找当前与mst集合连接的最小的边对应的顶点t
int t = -1;
for (int j = 1; j <= n; j++)
if (!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
if (i && dist[t] == INF) return INF; // 如果图不连通的话,不存在MST
if (i) res += dist[t]; // 这句话要放在更新的前面,否则,存在负权自环会更新dist[t]
st[t] = true; // 点t放入mst中
for (int j = 1; j <= n; j++) dist[j] = min(dist[j], g[t][j]);
}
return res;
}
int main() {
scanf("%d%d", &n, &m);
memset(g, 0x3f, sizeof g);
while (m--) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
g[a][b] = g[b][a] = min(g[a][b], c);
}
int t = prim();
if (t == INF) puts("impossible");
else printf("%d\n", t);
return 0;
}
AcWing 859. Kruskal算法求最小生成树
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 200010;
int n, m;
int p[N];
struct Edge {
int a, b, w;
bool operator<(const Edge &W) const {
return w < W.w;
}
} edges[N];
int find(int x) {
if (x != p[x]) p[x] = find(p[x]);
return p[x];
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 0; i < m; i++) {
int a, b, w;
scanf("%d%d%d", &a, &b, &w);
edges[i] = {a, b, w};
}
for (int i = 0; i <= n; i++) p[i] = i; // 并查集
// Kruskal算法
sort(edges, edges + m);
int res = 0, cnt = 0; // res存储MST权值和,cnt记录加入的边数
for (int i = 0; i < m; ++i) {
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
a = find(a), b = find(b);
if (a != b) {
p[a] = b;
res += w;
cnt++;
}
}
if (cnt < n - 1) puts("impossible");
else printf("%d\n", res);
return 0;
}
2. 最小生成树
AcWing 1140. 最短网络
问题描述
-
问题链接:AcWing 1140. 最短网络
分析
- 这个题就是让求解最小生成树,这里使用
prim算法
。
代码
- C++
#include <iostream>
#include <cstring>
using namespace std;
const int N = 110;
int n;
int g[N][N];
int dist[N];
bool st[N];
int prim() {
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
int res = 0;
for (int i = 0; i < n; i++) {
int t = -1;
for (int j = 1; j <= n; j++)
if (!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
res += dist[t];
st[t] = true;
for (int j = 1; j <= n; j++) dist[j] = min(dist[j], g[t][j]);
}
return res;
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
cin >> g[i][j];
cout << prim() << endl;
return 0;
}
AcWing 1141. 局域网
问题描述
-
问题链接:AcWing 1141. 局域网
分析
- 整个局域网可以看成一个图,但是这个图不一定是连通图。可以看成
kruskal算法
只求前一部分数据。 - 本题相当于在这个图的每个连通块内,求一棵生成树。如果要使得拔出网线的和最大,就需要使得剩余的网线的权值和最小,因此相当于求解每个连通块内的最小生成树。相当于求原图的"生成森林"。
- 这一题使用
prim算法
不容易写,因为prim算法
是从一个点开始向外扩散,我们需要对每个连通块单独处理。因此本题采用kruskal算法
求解。 - 虽然这个图可能不是连通的,但是直接对该图使用
kruskal算法
求解也是正确的。
代码
- C++
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110, M = 210;
int n, m;
int p[N];
struct Edge {
int a, b, w;
bool operator< (const Edge &e) const {
return w < e.w;
}
} edges[M];
int find(int x) {
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main() {
cin >> n >> m;
for (int i = 0; i < m; i++) {
int a, b, c;
cin >> a >> b >> c;
edges[i] = {a, b, c};
}
for (int i = 1; i <= n; i++) p[i] = i;
sort(edges, edges + m);
int res = 0;
for (int i = 0; i < m; i++) {
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
a = find(a), b = find(b);
if (a != b) p[a] = b;
else res += w;
}
cout << res << endl;
return 0;
}
AcWing 1142. 繁忙的都市
问题描述
-
问题链接:AcWing 1142. 繁忙的都市
分析
- 相当于让我们求一棵生成树,使得这棵生成树中道路分值最大的边在所有的生成树中是最小的。
- 对比:普通的最小生成树:所有的边权之和最小;本题中的最小生成树:最大的边权最小。
- 这一题可以使用二分,二分边权最大的边的值,然后判断边长小于等于该值的图能否使图中所有点连通即可。这里不适用这种方法求解。
- 直接使用
kruskal算法
求解,在过程中记录边权的最大值即可。证明方式可以参考前面的最小生成树原理。
代码
- C++
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 310, M = 8010;
int n, m;
int p[N];
struct Edge {
int a, b, w;
bool operator< (const Edge &e) const {
return w < e.w;
}
} edges[M];
int find(int x) {
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main() {
cin >> n >> m;
for (int i = 0; i < m; i++) {
int a, b, w;
scanf("%d%d%d", &a, &b, &w);
edges[i] = {a, b, w};
}
sort(edges, edges + m);
for (int i = 1; i <= n; i++) p[i] = i;
int res = 0;
for (int i = 0; i < m; i++) {
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
a = find(a), b = find(b);
if (a != b) {
p[a] = b;
res = w;
}
}
cout << n - 1 << ' ' << res << endl;
return 0;
}
AcWing 1143. 联络员
问题描述
-
问题链接:AcWing 1143. 联络员
分析
- 对于必须连接起来的点直接连接起来,并合并到一个集合中即可。
- 对于所有选择性通信渠道,按照
kruskal算法
处理即可。 - 可以看成
kruskal算法
只求后一部分数据。
代码
- C++
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 2010, M = 10010;
int n, m;
int p[N];
struct Edge {
int a, b, w;
bool operator< (const Edge &e) const {
return w < e.w;
}
} edges[M];
int find(int x) {
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) p[i] = i;
int res = 0, k = 0; // k: 可选边的条数
for (int i = 0; i < m; i++) {
int t, a, b, w;
cin >> t >> a >> b >> w;
if (t == 1) {
res += w;
p[find(a)] = find(b);
} else edges[k++] = {a, b, w};
}
sort(edges, edges + k);
for (int i = 0; i < k; i++) {
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
a = find(a), b = find(b);
if (a != b) {
p[a] = b;
res += w;
}
}
cout << res << endl;
return 0;
}
AcWing 1144. 连接格点
问题描述
-
问题链接:AcWing 1144. 连接格点
分析
- 首先这个题目需要抽象一下,我们将点阵中所有的点看成图中的点,点与点之间如果能直接到达,则连接一条无向边,横边权重为2,纵边权重为1。
- 因为一共有n*m个点,我们最少需要n*m-1条边才能将所有点连通,因为边权不是1就是2,是大于0的,为了使得边权最小,我们只需要选择n*m-1条边即可。
- 这一题相当于问:一些点已经连接起来了,求在剩余的边中添加哪些边可以使得所有点连通且花费最小。和上一题做法一样,使用
kruskal算法
求解即可。 - 这一题最多有 1 0 6 10^6 106条边, 2 × 1 0 6 2\times10^6 2×106条边。因为边权只有1和2,可以在建图的时候先将纵向边加入,再把横向边加入,这样就不需要对边进行排序了。
- 另外需要注意下面的问题不是最小生成树问题:给定n个点,m条边,边权可正可负。求将所有点连通的最小边权和是多少?
代码
- C++
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1010, M = N * N, E = 2 * N * N;
int n, m, k; // n行m列,一共k个点
int ids[N][N]; // 左上角点为(1,1),将二维的点映射为一维
struct Edge {
int a, b, w;
} edges[E];
int p[M]; // 并查集
int find(int x) {
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
void get_edges() {
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1}, du[4] = {1, 2, 1, 2};
for (int u = 0; u < 2; u++)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
for (int d = 0; d < 4; d++)
if (d % 2 == u) { // 保证先加入竖边
int x = i + dx[d], y = j + dy[d], w = du[d];
if (x > 0 && x <= n && y > 0 && y <= m) {
int a = ids[i][j], b = ids[x][y];
if (a < b) edges[k++] = {a, b, w};
}
}
}
int main() {
cin >> n >> m;
// 将二维的点映射为一维
for (int i = 1, t = 1; i <= n; i++)
for (int j = 1; j <= m; j++, t++)
ids[i][j] = t;
for (int i = 1; i <= n * m; i++) p[i] = i;
int x1, y1, x2, y2;
while (cin >> x1 >> y1 >> x2 >> y2) {
int a = ids[x1][y1], b = ids[x2][y2];
p[find(a)] = find(b);
}
get_edges(); // 建立所有的边
int res = 0;
for (int i = 0; i < k; i++) {
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
a = find(a), b = find(b);
if (a != b) {
p[a] = b;
res += w;
}
}
cout << res << endl;
return 0;
}
3. 最小生成树的扩展应用
AcWing 1146. 新的开始
问题描述
-
问题链接:AcWing 1146. 新的开始
分析
- 分析此题可知,图中的每个点要么自己有发电站(即不需要与其他点相连通,但耗费为 v i v_i vi),要么和其他已经供电的点连通。
- 解决此题我们可以设置一个虚拟源点,不妨设为0号点(代码中也是这样实现的)从这个源点向其他所有的点连一条边,权值为 v i v_i vi,此时我们得到一张新图,我们在这张新图上求最小生成树就可以得到最小花费。如果MST中有从0号点到其他点的边,则说明在该点直接修电站比较划算。
代码
- C++
#include <iostream>
#include <cstring>
using namespace std;
const int N = 310;
int n;
int g[N][N];
int dist[N];
bool st[N]; // 是否在MST中
int prim() {
memset(dist, 0x3f, sizeof dist);
dist[0] = 0;
int res = 0;
for (int i = 0; i < n + 1; i++) {
int t = -1;
for (int j = 0; j <= n; j++)
if (!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
st[t] = true;
res += dist[t];
for (int j = 0; j <= n; j++)
dist[j] = min(dist[j], g[t][j]);
}
return res;
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d", &g[0][i]);
g[i][0] = g[0][i];
}
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
scanf("%d", &g[i][j]);
printf("%d\n", prim());
return 0;
}
AcWing 1145. 北极通讯网络
问题描述
-
问题链接:AcWing 1145. 北极通讯网络
分析
- 分析可知本问题可以转化成:找到一个最小的d值,使得将所有权值大于d的边删去之后,整个图形的连通块的个数不超过k。
- 我们可以使用二分解决这个问题:每次二分一个mid值,大于mid代表不连通,小于等于mid代表连通,然后通过BFS或者DFS或者并查集判断连通块的数量。直到二分到答案为止。这种方法本题就不写了,下面演示使用
kruskal算法
解决该问题。
代码
- C++
#include <iostream>
#include <algorithm>
#include <cmath>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 510, M = N * N / 2;
int n, m, k; // 点数、边数、卫星数
struct Edge {
int a, b;
double w;
bool operator< (const Edge &e) {
return w < e.w;
}
} edges[M];
PII q[N]; // 存储读入村庄的坐标
int p[N]; // 并查集
double get_dist(PII a, PII b) {
int dx = a.x - b.x, dy = a.y - b.y;
return sqrt(dx * dx + dy * dy);
}
int find(int x) {
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main() {
cin >> n >> k;
for (int i = 0; i < n; i++) cin >> q[i].x >> q[i].y;
for (int i = 0; i < n; i++)
for (int j = 0; j < i; j++)
edges[m++] = {i, j, get_dist(q[i], q[j])};
for (int i = 0; i < n; i++) p[i] = i;
sort(edges, edges + m);
double res = 0;
int cnt = n; // 连通块个数
for (int i = 0; i < m; i++) {
if (cnt <= k) break;
int a = edges[i].a, b = edges[i].b;
double w = edges[i].w;
a = find(a), b = find(b);
if (a != b) {
p[a] = b;
cnt--;
res = w;
}
}
printf("%.2lf\n", res);
return 0;
}
AcWing 346. 走廊泼水节
问题描述
-
问题链接:AcWing 346. 走廊泼水节
分析
- 本题的做法:从小到大依次枚举每条树边(a, b, w),让a所在的连通分量中的所有点和b所在的连通分量中的所有点连接边,边权为w+1,这样增加的边的权值总和最小。
代码
- C++
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 6010;
int n;
struct Edge {
int a, b, w;
bool operator< (const Edge &e) {
return w < e.w;
}
} e[N];
int p[N], sz[N]; // 并查集
int find(int x) {
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main() {
int T;
cin >> T;
while (T--) {
cin >> n;
for (int i = 0; i < n - 1; i++) {
int a, b, c;
cin >> a >> b >> c;
e[i] = {a, b, c};
}
sort(e, e + n - 1);
for (int i = 1; i <= n; i++) p[i] = i, sz[i] = 1;
int res = 0;
for (int i = 0; i < n - 1; i++) {
int a = find(e[i].a), b = find(e[i].b), w = e[i].w;
if (a != b) {
res += (sz[a] * sz[b] - 1) * (w + 1);
p[a] = b;
sz[b] += sz[a];
}
}
cout << res << endl;
}
return 0;
}
AcWing 1148. 秘密的牛奶运输
问题描述
-
问题链接:AcWing 1148. 秘密的牛奶运输
分析
代码
- C++
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 510, M = 10010;
int n, m;
struct Edge {
int a, b, w;
bool f; // 该边是否在MST中
bool operator< (const Edge &e) {
return w < e.w;
}
} edges[M];
int p[N];
int dist1[N][N]; // MST中任意两点所在路径中边权最大值
int dist2[N][N]; // MST中任意两点所在路径中边权严格次大值
int h[N], e[N * 2], w[N * 2], ne[N * 2], idx; // 存储MST
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
int find(int x) {
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
// u: 当前考察的点; fa: u的父节点
// maxd1: 当前路径上的边权最大值; d1: 需要被赋值的最大值数组
// maxd2: 当前路径上的边权严格次大值; d2: 需要被赋值的严格次大值数组
void dfs(int u, int fa, int maxd1, int maxd2, int d1[], int d2[]) {
d1[u] = maxd1, d2[u] = maxd2;
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (j != fa) {
int td1 = maxd1, td2 = maxd2;
if (w[i] > td1) td2 = td1, td1 = w[i];
else if (w[i] < td1 && w[i] > td2) td2 = w[i];
dfs(j, u, td1, td2, d1, d2);
}
}
}
int main() {
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
for (int i = 0; i < m; i++) {
int a, b, w;
scanf("%d%d%d", &a, &b, &w);
edges[i] = {a, b, w};
}
// 求解MST
sort(edges, edges + m);
for (int i = 1; i <= n; i++) p[i] = i;
LL sum = 0; // MST权值和
for (int i = 0; i < m; i++) {
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
int pa = find(a), pb = find(b);
if (pa != pb) {
p[pa] = pb;
sum += w;
edges[i].f = true;
add(a, b, w), add(b, a, w);
}
}
// 求解dist1、dist2数组
for (int i = 1; i <= n; i++) dfs(i, -1, 0, 0, dist1[i], dist2[i]);
// 求解次小生成树
LL res = 1e20;
for (int i = 0; i < m; i++)
if (!edges[i].f) { // 只有是非树边才执行
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
LL t;
if (w > dist1[a][b])
t = sum + w - dist1[a][b];
else if (w > dist2[a][b])
t = sum + w - dist2[a][b];
res = min(res, t);
}
printf("%lld\n", res);
return 0;
}