最小生成树(复习总结)
最小生成树的基本应用
对于稠密图
对于稠密图可以采用\(Prime\)算法,其思想就是从原点开始出发,在已确定的点连接未确定的点的边中,选取长度最小的一条,假如到最小生成树中
Code
int g[N][N], d[N], n;
bool st[N];
int prime(int start) {
memset(d, 0x3f, sizeof d);
d[start] = 0;
for (int i = 1; i < n; i++) {
int x = 0;
for (int j = 1; j <= n; j++) if (!st[j] && (!x || d[j] < d[x]))
x = j;
st[x] = 1;
for (int y = 1; y <= n; y++) if (!st[y])
d[y] = min(d[y], g[x][y]);
}
int res = 0;
for (int i = 1; i <= n; i++) res += d[i];
return res;
}
最短网络
Prime的板子题,什么好说的
稀疏图
对于稀疏图用\(Kruskal\)算法,其原理是从贪心的角度出发,按照边权从小到大选取边且每个点最多只能连接两个边。中间用并查集进行优化,具体过程如下:
- 将所有的边从小到大排序,遍历每个边及其顶点
- 如果两个点不在同一联通块中,那么将两个点合并到同一联通块中,边权加入最小生成树的权值中
struct Edge {
int a, b, w;
bool operator < (const Edge &W) const {
return w < W.w;
}
} edge[N];
int n, p[N], k;
int kruskal() {
int res = 0;
// 并查集初始化
for (int i = 1; i <= n; i++) p[i] = i;
sort(edge, edge + k);
for (int i = 0; i < k; i++) {
auto [a, b, c] = edge[i];
int fa = find(a), fb = find(b);
if (fa == fb) res += w;
else p[fa] = fb;
}
return res;
}
局域网
板子题,不想多说
繁忙的都市
板子题,不过求的是最大值
把p==1
的边直接加入生成树后,再存下p==2
的边,对剩下的边求最小生成树
#include <iostream>
#include <algorithm>
using namespace std;
constexpr int N = 10010;
struct Edge {
int a, b, w;
bool operator < (const Edge &W) const {
return w < W.w;
}
} edge[N];
int n, m, k, p[N];
int find(int x) {
if (p[x] != x) p[x] = find(p[x]); return p[x];
}
int main() {
int res = 0;
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) p[i] = i;
while (m --) {
static int c, a, b, w;
scanf("%d%d%d%d", &c, &a, &b, &w);
if (c == 1) res += w, p[find(a)] = find(b);
else edge[k++] = {a, b, w};
}
sort(edge, edge + k);
for (int i = 0; i < k; i++) {
auto [a, b, w] = edge[i];
int fa = find(a), fb = find(b);
if (fa != fb) {
res += w;
p[fa] = fb;
}
}
printf("%d\n", res);
return 0;
}
将整个矩阵展开至一维,则有\((i,j)=i*m+j\)
这样就可以用Kruskal来解决这道题,由于纵向的权值小于横向的,所以处理完所有存在的边后,先对所有纵向的边枚举建边,再枚举横向即可。
#include <iostream>
#include <algorithm>
using namespace std;
constexpr int N = 1110, M = N * N;
int n, m, p[M];
struct Edge {
int a, b, w;
bool operator < (const Edge &W) const {
return w < W.w;
}
} edge[M];
int find(int x) {
if (p[x] != x) p[x] = find(p[x]); return p[x];
}
int main() {
int x1, y1, x2, y2, res = 0;
scanf("%d%d", &n, &m);
for (int i = 0; i < m * n; i++) p[i] = i;
while (~scanf("%d%d%d%d", &x1, &y1, &x2, &y2)) {
int a = (x1 - 1) * m + y1 - 1, b = (x2 - 1) * m + y2 - 1;
p[find(a)] = find(b);
}
for (int i = 0; i < (n - 1) * m; i++) {
int a = find(i), b = find(i + m);
if (p[a] != p[b]) {
p[a] = b;
res++;
}
}
for (int i = 0; i < n * m; i++) {
if (i % m < m - 1) {
int a = find(i), b = find(i + 1);
if (a != b) {
p[a] = b;
res += 2;
}
}
}
printf("%d\n", res);
return 0;
}
最小生成树的拓展应用
新的开始 - 虚拟原点+最小生成树
本题有两种操作,一个是直接在图上建立发电站,另一种是从发电站向其他点建立电网。
由于每个点都可以建立电站,那么我们可以引入虚拟原点0,这样每个点单独建立电站就转化成了从虚拟原点0向每个点建立一条边,这样问题就转换成了求最小生成树。
Code
#include <iostream>
#include <algorithm>
using namespace std;
constexpr int N = 1e5 + 100, M = 330;
struct Edge {
int a, b, w;
bool operator < (const Edge &W) const {
return w < W.w;
}
} edge[N];
int n, k, g[M][M], p[M], res;
int find(int x) {
if (p[x] != x) p[x] = find(p[x]); return p[x];
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
int x; scanf("%d", &x);
edge[k++] = {0, i, x};
p[i] = i;
}
for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) {
scanf("%d", &g[i][j]);
if (j > i) edge[k++] = {i, j, g[i][j]};
}
sort(edge, edge + k);
for (int i = 0; i < k; i++) {
auto [a, b, w] = edge[i];
int fa = find(a), fb = find(b);
if (fa != fb) {
p[fa] = fb;
res += w;
}
}
printf("%d\n", res);
return 0;
}
北极通讯网络 - 贪心
由题意可知,若干个点构成一棵生成树,满足的\(d\)值一定是其中最大的边权。
但是如果有\(k\)条边的限制,\(k\)越多,\(d\)也就越小。若想要让\(d\)最小,那么一定要在一棵最小生成树里寻找边的值;要想让所有点都能连接,那么就应该先满足边权大的边。
所以本题的答案就是求图中的所有点的构成的最小生成树中,第\(k\)大的边。
注意:
- 0台设备时等同于1台设备
- 当要求的卫星台数大于地点数,\(d=0\)
Code
#include <iostream>
#include <cmath>
#include <algorithm>
#include <vector>
using namespace std;
typedef pair<int, int> PII;
constexpr int N = 1e6 + 100, M = 550;
PII node[M];
struct Edge {
int a, b;
double w;
bool operator < (const Edge &W) const {
return w < W.w;
}
} edge[N];
int n, k, p[N], cnt, m;
int find(int x) {
if (p[x] != x) p[x] = find(p[x]); return p[x];
}
inline double cal(PII A, PII B) {
auto [a, b] = A;
auto [l, r] = B;
return sqrt(1.0 * (a - l) * (a - l) + (b - r) * (b - r));
}
inline double kruskal() {
if (n <= k) return 0;
double s[M];
int cnt = 0;
for (int i = 0; i < m; i++) {
auto [a, b, w] = edge[i];
int fa = find(a), fb = find(b);
if (fa != fb) {
p[fa] = fb;
s[cnt++] = w;
}
}
return s[cnt - k];
}
int main() {
scanf("%d %d", &n, &k);
for (int i = 1; i <= n * n; i++) p[i] = i;
k = max(1, k);
for (int i = 1; i <= n; i++) {
int a, b; scanf("%d%d", &a, &b);
node[i] = {a, b};
}
for (int i = 1; i < n; i++) for (int j = i + 1; j <= n; j++)
edge[m++] = {i, j, cal(node[i], node[j])};
sort(edge, edge + m);
printf("%.2lf\n", kruskal());
return 0;
}
走廊泼水节
题目描述的很明确,难点在于如何去构造。
考虑Kruskal算法的过程:
- 将所有的边按照从小到大的顺序排列
- 如果对于一条边\(u\to v\),假如他们不在同一个联通块中,那么他们之间会选择最短的一条边连接
在本题中,除了要连接最短的边\(w\),还要让其他点都相互连接起来,为了保证只有这一个最小生成树,那么其他边的边权应该为\(w+1\)。两个联通块中的所有点相互连一条边,那么会有\(cnt_a\times cnt_b\)条边,排除掉最小的边,新图要添加的边权为\((w+1)\times (cnt_a\times cnt_b-1)\)条边。
Code
#include <iostream>
#include <algorithm>
using namespace std;
constexpr int N = 6600;
int n, p[N], cnt[N];
struct Edge {
int a, b, w;
bool operator < (const Edge &W) const {
return w < W.w;
}
} edge[N];
int find(int x) {
if (p[x] != x) p[x] = find(p[x]); return p[x];
}
int main() {
int _T;
scanf("%d", &_T);
while (_T --) {
scanf("%d", &n);
for (int i = 1; i <= n; i++) p[i] = i, cnt[i] = 1;
for (int i = 1; i < n; i++) {
int a, b, w; scanf("%d%d%d", &a, &b, &w);
edge[i] = {a, b, w};
}
sort(edge + 1, edge + n);
int res = 0;
for (int i = 1; i < n; i++) {
auto [a, b, w] = edge[i];
a = find(a), b = find(b);
if (a != b) {
p[a] = b;
res += (w + 1) * (cnt[a] * cnt[b] - 1);
cnt[b] += cnt[a];
}
}
printf("%d\n", res);
}
return 0;
}
秘密的牛奶运输 - 次小生成树
本题就是让你求一个最小生成树
求出最小生成树后,对生成树的每一条边用非树边进行替换:
- 当非树边大于生成树的最大边时,非树边替换最大边,计算权值和,更新答案
- 当非树边小于生成树的最大边时,如果非树边大于第二大的边,那么替换后更新一下答案
由于本题的数据量较小,所以可以直接通过遍历图的形式求解,求出任意两点间的最小边和次小边。最终答案为\(\max(Sum+w-d)\)
Code
#include <iostream>
#include <algorithm>
using namespace std;
constexpr int N = 550, M = 20010;
typedef long long LL;
struct Edge {
int a, b, w;
bool f;
bool operator < (const Edge &W) const {
return w < W.w;
}
} edge[M / 2];
int n, m, p[N], d1[N][N], d2[N][N];
int h[N], e[M], ne[M], w[M], idx;
int find(int x) {
if (p[x] != x) p[x] = find(p[x]); return p[x];
}
inline void add(int a, int b, int c) {
e[++idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx;
}
void dfs(int u, int fa, int max1, int max2, int d1[], int d2[]) {
d1[u] = max1, d2[u] = max2;
for (int i = h[u]; i; i = ne[i]) {
int j = e[i];
if (j == fa) continue;
int x = max1, y = max2;
if (w[i] > x) {
y = x, x = w[i];
} else if (w[i] < x && w[i] > y) y = w[i];
dfs(j, u, x, y, d1, d2);
}
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) p[i] = i;
for (int i = 0; i < m; i++) {
int a, b, w; scanf("%d%d%d", &a, &b, &w);
edge[i] = {a, b, w};
}
sort(edge, edge + m);
LL sum = 0, res = 1e18;
for (int i = 0; i < m; i++) {
auto &[a, b, w, f] = edge[i];
int fa = find(a), fb = find(b);
if (fa != fb) {
add(a, b, w), add(b, a, w);
p[fa] = fb;
f = 1;
sum += w;
}
}
for (int i = 1; i <= n; i++) dfs(i, -1, -1e9, -1e9, d1[i], d2[i]);
for (int i = 0; i < m; i++) {
auto [a, b, w, f] = edge[i];
if (!f) {
if (w > d1[a][b]) res = min(res, sum + w - d1[a][b]);
else if (w > d2[a][b]) res = min(res, sum + w - d2[a][b]);
}
}
printf("%lld\n", res);
return 0;
}