最小生成树
最小生成树
AcWing.346 走廊泼水节
简要题意
给定一个 N 个节点的树,要求增加若干条边,把这棵树扩充为完全图,并满足图的唯一最小生成树仍然是这棵树。求增加的边的权值总和最小是多少,保证边权位非负整数。
题目分析
考虑 kruskal 的过程,是把权值从小到大排序,依次扫描每一个边。那么我们想让这棵新的树的最小生成树仍然不变且是唯一的,那么我们的边权应该设为 \(z+1\) ,\(z\) 是当前扫描边的边长。那么两个点之间要比原图多多少条边呢?为 \(|S_x|*|S_y|-1\),\(S_x\) 表示 \(x\) 所在的并查集。所以只需要在原来跑 kruskal 的过程上多维护一个 \(S\) 就可以了。
struct rec
{
int x, y, z;
friend bool operator < (rec a, rec b)
{
return a.z < b.z;
}
} edge[M];
int find(int x)
{
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
int kruscal()
{
sort(edge + 1, edge + n);
int idx = 0; ans = 0;
for (rint i = 1; i <= n; i++) fa[i] = i, s[i] = 1;
for (rint i = 1; i < n; i++)
{
int x = find(edge[i].x);
int y = find(edge[i].y);
if (x == y) continue;
fa[x] = y;
idx++;
ans += (edge[i].z + 1) * (s[x] * s[y] - 1);
s[y] += s[x];
}
if (idx < n - 1) return inf;
return ans;
}
signed main()
{
int T;
cin >> T;
while (T--)
{
cin >> n;
for (rint i = 1; i < n; i++)
{
cin >> edge[i].x >> edge[i].y >> edge[i].z;
}
cout << kruscal() << endl;
}
return 0;
}
AcWing 347. 野餐规划
简要题意
给定一张 \(N\) 个点 \(M\) 条边的无向图,求出无向图的一棵最小生成树,满足 \(1\) 号节点的度数不超过给定的整数 \(S\)。\(N\) 不超过 \(30\).
题目分析
首先,去掉一号节点之后,无向图可能会分成若干个联通块。可以用深度优先遍历划分出图中的每个联通块。设联通块共有 \(T\) 个,若 \(T > S\),则本题无解。
对于每个联通块,在这个联通块内部求出它的最小生成树,然后从联通块中选出一个节点 \(p\) 与 \(1\) 号节点相连,其中无向边 \((1,p)\) 的权值尽量小。
此时,我们已经得到了原无向图的一棵生成树,\(1\) 号节点的度数为 \(T\)。我们还可以尝试改动 \(S-T\),让答案更优。
考虑无向图中从节点 \(1\) 出发的每条边 \((1,x)\) ,边权为 \(z\)。如果 \((1,x)\) 还不在当前的生成树中,那么继续找到当前生成树中从 \(x\) 到 \(1\) 的路径上权值最大的边 \((u,v)\),边权为 \(w\)。求出使得 \(w-z\) 最大的点 \(x_0\)。若 \(x_0\) 对应的 \(w_0-z_0 > 0\),则从树中删掉边 \((u_0,v_0)\),加入边 \((1,x_0)\),答案就会变小 \(w_0-z_0\)。
重复上一步 \(S - T\) 或者直到 \(w_0-z_0<=0\),就得到了题目所求的最小生成树。
int n, s;
int tot;
int g[N][N];
int fa[N];
int block[N], cntb;
map<pair<int, int>, bool> v;
map<string, int> mp;
struct rec
{
int x, y, z;
friend bool operator < (rec a, rec b)
{
return a.z < b.z;
}
} edge[N];
struct node
{
int a, b;
int dist;
} f[N];
int find(int x)
{
if (fa[x] != x) fa[x] = find(fa[x]);
return fa[x];
}
int kruskal()
{
sort(edge + 1, edge + 1 + n);
for (rint i = 1; i <= tot; i++) fa[i] = i;
int ans = 0;
for (rint i = 1; i <= n; i++)
{
int x = find(edge[i].x);
int y = find(edge[i].y);
if (x == 1 || y == 1 || x == y) continue;
fa[x] = y;
v[{edge[i].x, edge[i].y}] = 1;
v[{edge[i].y, edge[i].x}] = 1;
ans += edge[i].z;
}
return ans;
}
void dfs(int x)
{
for (rint y = 2; y <= tot; y++)
{
if (!g[x][y] || block[y]) continue;
block[y] = cntb;
dfs(y);
}
}
void dfs(int x, int father)
{
for (rint y = 2; y <= tot; y++)
{
if (y == father || !v[{x, y}]) continue;
if (~f[y].dist) continue;
if (f[x].dist > g[x][y]) f[y] = f[x];
else f[y] = {x, y, g[x][y]};
dfs(y, x);
}
}
signed main()
{
cin >> n;
mp["Park"] = tot = 1;
for (rint i = 1; i <= n; i++)
{
string a, b;
int c;
cin >> a >> b >> c;
if (!mp[a]) mp[a] = ++tot;
if (!mp[b]) mp[b] = ++tot;
g[mp[a]][mp[b]] = g[mp[b]][mp[a]] = c;
edge[i] = {mp[a], mp[b], c};
}
cin >> s;
for (rint i = 2; i <= tot; i++)
{
if (!block[i])
{
cntb++;
block[i] = cntb;
dfs(i);
}
}
int sum = kruskal();
for (rint i = 1; i <= cntb; i++)
{
int minn = inf, id = 0;
for (rint j = 2; j <= tot; j++)
{
if (block[j] == i)
{
if (g[1][j] && minn > g[1][j])
{
minn = g[1][j];
id = j;
}
}
}
sum += minn;
v[{1, id}] = 1;
v[{id, 1}] = 1;
}
int t = cntb;
while (t < s)
{
s--;
for (rint i = 0; i <= 25; i++) f[i] = {0, 0, -1};
dfs(1, 0);
int maxx = 0, id = 0;
for (rint j = 2; j <= tot; j++)
{
if (g[1][j] && maxx < f[j].dist - g[1][j])
{
maxx = f[j].dist - g[1][j];
id = j;
}
}
if (!maxx) break;
v[{f[id].a, f[id].b}] = v[{f[id].b, f[id].a}] = 0;
v[{1, id}] = v[{id, 1}] = 1;
sum -= maxx;
}
cout << "Total miles driven: " << sum << endl;
return 0;
}
AcWing348. 沙漠之王
简要题意
给这一张 \(N\) 个点 \(M\) 条边的无向图,图中每条边 \(e\) 都有一个收益 \(C_e\) 和一个成本 \(R_e\),求该图的一颗生成树 \(T\), 使树中各边的收益之和除以成本之和,即 \(∑_{e∈T}C_e/∑_{e∈T}R_e\) 最大。\((1<N,M<10000)\)
题目分析
\(x=w/l\) 即 \(w-l*x =0,f(x) = w-l*x\);将边权更改为 \(w-l*x\) 来求生成树
因为 \(f(x)\) 是个单调递减函数,随着 \(x\) 的增大而减少,对于任意一个生成树如果 \(f(x)>0\),则 \(l\) 需要增大 \(f(x)<0\) 否则 \(l\) 需要减小 若要满足 \(f(x)==0\) 恒成立
1.若要 \(x\) 取最大值,则不能存在任意一个生成树 \(f(x)>0\), 否则 \(x\) 还能继续增大,即任意生成树 \(f(x)<=0\) 若存在一个生成树 \(f(x)>0\),则那个生成树的比率一定大于当前 \(x\), \(w/l > x\) 即 \(w-l*x > 0\)
2.若要 \(x\) 取最小值,则不能存在任意一个生成树 \(f(x)<0\),否则 \(x\) 还能继续减小,即任意生成树 \(f(x)>=0\) 若存在一个生成树 \(f(x)<0\),则那个生成树的比率一定小于当前 \(x\), \(w/l < x\) 即 \(w-l*x < 0\)
若要满足 \(f(x)>0\) 恒成立,则最小生成树 \(>0\)
若要满足 \(f(x)<0\) 恒成立,则最大生成树 \(<0\)
此题目求解最小的 \(x\) 值,也就是检查是否所有的生成树 \(f(x)>=0\),即最小生成树 \(>=0\)
如果最小生成树大于 \(0\),所有的生成树都满足 \(f(x)>0\), 尝试增加 \(x\) 得到 \(f(x)=0\)
否则,有生成树不满足这个条件,那么 \(x\) 一定要减少来使所有 \(f(x)>=0\)
double calc(int a, int b)
{
return sqrt((x[a] - x[b]) * (x[a] - x[b]) + (y[a] - y[b]) * (y[a] - y[b]));
}
bool check(double mid)
{
fill(dist, dist + n + 1, dinf);
fill(v, v + n + 1, 0);
dist[1] = 0;
double ans = 0;
for (rint i = 1; i <= n; i++)
{
int x = 0;
for (rint j = 1; j <= n; j++)
{
if (!v[j] && (x == 0 || dist[j] < dist[x])) x = j;
}
v[x] = 1;
ans += dist[x];
for (rint y = 1; y <= n; y++)
{
if (!v[y]) dist[y] = min(dist[y], fabs(w[x] - w[y]) - mid * calc(x, y));
}
}
return ans >= 0;
}
signed main()
{
while (cin >> n && n)
{
for (rint i = 1; i <= n; i++)
{
cin >> x[i] >> y[i] >> w[i];
}
double l = 0, r = 10000000;
double ans;
while ((r - l) > eps)
{
double mid = (l + r) / 2;
if (check(mid)) ans = mid, l = mid;
else r = mid;
}
cout << fixed << setprecision(3) << ans << endl;
}
return 0;
}
AcWing.349. 黑暗城堡
题目大意
问你有多少棵最短路径树
题目分析
这里用的邻接矩阵 Dijkstra
对于已经是最短路的情况,对于任意两个点 \(x,y\) 有 \(dist[y] <= dist[x] + z\)
现在考虑 \(dist[y] <= dist[x] + z\),那么在最短路径生成树中一定不能有这一条边。如果有这一条边,那么 \(y\) 的路径就不是最小的。(因为是树,所以只能是这一个点来对 \(y\) 进行更新)
那么当 \(dist[y]==dist[x]+z\),最短路径生成树里面可以包含这一条边。
1.对于每一个点,都有到达 \(1\) 号点的距离。现在按照距离从小到大对点进行考虑。
2.对于考虑到的 \(i\) 个点,查找已经遍历过的集合,看有多少 \(x\) 满足 \(dist[i]==dist[x]+z\)。这是方案数。使用乘法原理。
int n, m;
int a[N][N];
int dist[N];
int ans = 1;
bool v[N];
pair<int, int> f[N];
signed main()
{
cin >> n >> m;
memset(a, 0x3f, sizeof a);
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for (rint i = 1; i <= n; i++) a[i][i] = 0;
for (rint i = 1; i <= m; i++)
{
int x, y, z;
cin >> x >> y >> z;
a[x][y] = a[y][x] = min(a[x][y], z);
}
for (rint i = 1; i <= n; i++)
{
int x = 0;
for (rint j = 1; j <= n; j++)
if (!v[j] && (x == 0 || dist[x] > dist[j])) x = j;
v[x] = 1;
for (rint y = 1; y <= n; y++)
{
if (!v[y]) dist[y] = min(dist[y], dist[x] + a[x][y]);
}
}
for (rint i = 1; i <= n; i++) f[i] = {dist[i], i};
sort(f + 1, f + n + 1);
memset(v, 0, sizeof v);
v[1] = 1;
for (rint i = 2; i <= n; i++)
{
int y = f[i].second;
int cnt = 0;
for (rint x = 1; x <= n; x++)
{
if (v[x] && dist[x] + a[x][y] == dist[y]) cnt++;
}
ans = ans * cnt % mod;
v[y] = 1;
}
cout << ans << endl;
return 0;
}
AcWing.388. 四叶草魔杖
题目大意
给定一张无向图,结点和边均有权值。所有结点权值之和为 \(0\),点权可以沿边传递,传递点权的代价为边的权值。求让所有结点权值化为 \(0\) 的最小代价。
题目分析
容易想到本题与最小生成树有关。一种不难想出的思路是求出原图的最小生成树,将最小生成树上所有边的权值之和作为答案。
但经过思考,可以发现这样得到的不一定是最优解。首先,原图可能并不联通;其次,可以将原图划分为若干个点权之和均为 \(0\) 的子图,在这些子图中分别转移点权,最后将答案合并。这样得到的方案或许会更优。
此时我们发现划分方案不止一种,如何确定最终的方案成了需要解决的最大问题。
注意到本题中 \(N\) 范围较小,允许我们把所有点权和为 \(0\) 的子图(以下简称“合法子图”)的最小生成树全部求出。因此可以先枚举原图点集的所有子集,对于每个点权和为 \(0\) 的点集,用这些点和连接它们的边构造一张合法子图。我们能够轻易求出这些合法子图的最小生成树。但有些合法子图或许并不联通,为避免对之后的求解造成影响,需要把这些子图的最小生成树边权和设为 \(\infty\)。
接下来需要把这些子图中的若干个合并起来,得到全局最优解。与划分的情形相同,合并这些子图的方案也有多种。可以使用 \(DP\) 得到最优解。
具体地,考虑进行类似背包的 \(DP\),将每个合法子图视作可以放入背包的一个物品。设 \(A\)、\(B\) 为两个不同合法子图的点集,合法子图的最小生成树边权和为 \(S\),可以写出如下状态转移方程:
$f_{A \cup B}=min {f_{A\cup B}, f_{A}+S_{B} },A\cap B=\oslash $
最终 \(f_{2^n-1}\) 即为所求的答案。
int n, m;
int a[N], fa[N];
int s[M], f[M], p[M];
struct rec
{
int x, y, z;
friend bool operator < (rec a, rec b)
{
return a.z < b.z;
}
} edge[M];
int find(int x)
{
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
int kruskal(int s)
{
int ans = 0;
for (rint i = 0; i < n; i++) if (s & (1 << i)) fa[i] = i;
for (rint i = 1; i <= m; i++)
{
if (!(s & (1 << (edge[i].x))) || !(s & (1 << (edge[i].y)))) continue;
int x = find(edge[i].x);
int y = find(edge[i].y);
if (x == y) continue;
fa[x] = y;
ans += edge[i].z;
}
int father = -1;
for (rint i = 0; i < n; i++)
{
if (s & (1 << i))
{
if (father == -1) father = find(i);
else if (find(i) != father) return inf;
}
}
return ans;
}
signed main()
{
cin >> n >> m;
for (rint i = 1; i <= n; i++) cin >> a[i];
for (rint i = 1; i <= m; i++) cin >> edge[i].x >> edge[i].y >> edge[i].z;
for (rint i = 1; i < (1 << n); i++)
for (rint j = 0; j < n; j++)
if (i & (1 << j))
s[i] += a[j + 1];
sort(edge + 1, edge + m + 1);
for (rint i = 1; i < (1 << n); i++)
{
if (!s[i]) p[i] = kruskal(i);
f[i] = inf;
}
f[0] = 0;
for (rint i = 1; i < (1 << n); i++)
{
if (s[i]) continue;
for (rint j = 0; j < (1 << n); j++)
{
if (!(i & j)) f[i | j] = min(f[i | j], f[j] + p[i]);
}
}
if (f[(1 << n) - 1] >= inf) puts("Impossible");
else cout << f[(1 << n) - 1] << endl;
return 0;
}
P3623 免费道路
题目大意
个图边权为 \(0\) 或 \(1\),求一个生成树使得边权和为 \(k\)
题目分析
题目中说要保留 \(k\) 条边,有一些 \(1\) 边是要必须保留的。这样才能保证图的连通性。
先以 \(0\) 边优先做一遍 kruskal,把必须要加入的 \(1\) 边个数算出。
再做一遍 kruskal,先把那些必须加的加入,然后再加 \(1\) 边使得达到 \(k\) 条,然后就一直加 \(0\) 边了。
在上述过程中,有两种无解的情况:
1.图不练通
2.必须要加的边超过 \(k\)
bool cmp1(node a, node b) { return a.z > b.z;}
bool cmp2(node a, node b) { return a.z < b.z;}
int find(int x)
{
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void check()
{
int l = find(1);
for (rint i = 2; i <= n; i++)
{
int r = find(i);
if (r != l) puts("no solution"), exit(0);
l = r;
}
}
signed main()
{
cin >> n >> m >> k;
for (rint i = 1; i <= m; i++)
{
cin >> edge[i].x >> edge[i].y >> edge[i].z;
}
//kruscal 1
cnt = idx = 0;
for (rint i = 1; i <= n; i++) fa[i] = i;
sort(edge + 1, edge + m + 1, cmp1);
for (rint i = 1; i <= m; i++)
{
int x = find(edge[i].x);
int y = find(edge[i].y);
if (x == y) continue;
fa[x] = y;
if (!edge[i].z)
{
idx++;
edge[i].z = -1;
}
}
if (idx > k)
{
puts("no solution");
return 0;
}
check();
//kruscal 2
cnt = idx = 0;
for (rint i = 1; i <= n; i++) fa[i] = i;
sort(edge + 1, edge + m + 1, cmp2);
for (rint i = 1; i <= m; i++)
{
int x = find(edge[i].x);
int y = find(edge[i].y);
if (x == y) continue;
if (edge[i].z == 1 || idx < k)
{
fa[x] = y;
ans[++cnt] = edge[i];
if (edge[i].z < 1) idx++, edge[i].z = 0;
}
}
if (idx < k)
{
puts("no solution");
return 0;
}
check();
for (rint i = 1; i <= cnt; i++)
{
if (ans[i].z == -1)
{
ans[i].z = 0;
}
cout << ans[i].x << " " << ans[i].y << " " << ans[i].z << endl;
}
return 0;
}
CF888G Xor-MST
题目大意
给定 \(n\) 个结点的无向完全图。每个点有一个点权为 \(a_i\)。连接 \(i\) 号结点和 \(j\) 号结点的边的边权为 \(a_i\oplus a_j\)。求这个图的 MST 的权值。
题目分析
每一轮维护一个 Trie 来存储所有 \(a_i\) ,对于每个联通块,先把联通块里的数在 Trie 删掉,然后在 Trie 里查询其他点和联通块里每个点的最小异或和。
int n, m;
int a[N], L[M], R[M];
int ch[2][M], rt, cnt;
void insert(int &k, int id, int dep)
{
if (!k) k = ++cnt;
if (!L[k]) L[k] = id;
R[k] = id;
if (dep == -1) return ;
insert(ch[(a[id] >> dep) & 1][k], id, dep - 1);
}
int query(int k, int x, int dep)
{
if (dep == -1) return 0;
int v = (x >> dep) & 1;
if (ch[v][k]) return query(ch[v][k], x, dep - 1);
return query(ch[v ^ 1][k], x, dep - 1) + (1 << dep);
}
int dfs(int k, int dep)
{
if (dep == -1) return 0;
if (ch[0][k] && ch[1][k])
{
int ans = 1e18;
for (rint i = L[ch[0][k]]; i <= R[ch[0][k]]; i++)
{
ans = min(ans, query(ch[1][k], a[i], dep - 1) + (1 << dep));
}
return dfs(ch[0][k], dep - 1) + dfs(ch[1][k], dep - 1) + ans;
}
else if (ch[0][k]) return dfs(ch[0][k], dep - 1);
else if (ch[1][k]) return dfs(ch[1][k], dep - 1);
return 0;
}
signed main()
{
cin >> n;
for (rint i = 1; i <= n; i++) cin >> a[i];
sort(a + 1, a + n + 1);
for (rint i = 1; i <= n; i++) insert(rt, i, 30);
cout << dfs(rt, 30) << endl;
return 0;
}
P4208 最小生成树计数
题目大意
一个简单无向加权图。求这个图中有多少个不同的最小生成树。如果两颗最小生成树中至少有一条边不同,则这两个最小生成树就是不同的。
题目分析
这个题做法很多,难的不会,就会一个简单的做法,复杂度是 \(O(2^{10}m)\) 的,足以通过此题。
显然的,每种权值的边的数量是固定的,那么我们先统计出每种权值需要多少条边,记为 \(c_i\)
发现具有相同权值的边的数量不超过 \(10\) 条,暴力枚举第 \(i\) 种权值的边选择哪 \(c_i\) 条,乘法原理统计答案。
然后我第一遍打完发现运行样例结果得出来 \(7\),因为要快速分开连通块,并查集中不能使用路径压缩。
int n, m, cnt, sum;
int l[M], r[M], c[M];
int fa[N];
struct rec
{
int x, y, z;
friend bool operator < (rec a, rec b)
{
return a.z < b.z;
}
} edge[M];
int find(int x)
{
return fa[x] == x ? x : find(fa[x]);
}
void dfs(int now, int x, int num)
{
if (now > r[x])
{
sum += (num == c[x]);
return ;
}
int X = find(edge[now].x);
int Y = find(edge[now].y);
if (X == Y)
{
dfs(now + 1, x, num);
return ;
}
fa[X] = Y;
dfs(now + 1, x, num + 1);
fa[X] = X;
fa[Y] = Y;
dfs(now + 1, x, num);
}
void kruscal()
{
sort(edge + 1, edge + m + 1);
for (rint i = 1; i <= n; i++) fa[i] = i;
int idx = 0;
for (rint i = 1; i <= m; i++)
{
if (edge[i].z != edge[i - 1].z)
{
r[cnt] = i - 1;
l[++cnt] = i;
}
int x = find(edge[i].x);
int y = find(edge[i].y);
if (x == y) continue;
idx++;
c[cnt]++;
fa[x] = y;
}
r[cnt] = m;
if (idx < n - 1)
{
puts("0");
exit(0);
}
}
signed main()
{
cin >> n >> m;
for (rint i = 1; i <= m; i++) cin >> edge[i].x >> edge[i].y >> edge[i].z;
kruscal();
for (rint i = 1; i <= n; i++) fa[i] = i;
int ans = 1;
for (rint i = 1; i <= cnt; i++)
{
sum = 0;
dfs(l[i], i, 0);
ans = ans * sum % mod;
for (rint j = l[i]; j <= r[i]; j++)
fa[find(edge[j].x)] = find(edge[j].y);
}
cout << ans << endl;
return 0;
}
UVA1395
题目大意
求所有生成树中最大边权与最小边权差最小的,输出它们的差值。
题目分析
想到暴力,求出每个生成树的边权差值,最后取个 min。
首先对边排个序,枚举 K 然后不断地记录答案就可以了。
int kruscal()
{
ans = inf;
memset(edge, 0, sizeof edge);
cin >> n >> m;
if (m == 0 && n == 0) return 0;
for (rint i = 1; i <= m; i++) cin >> edge[i].x >> edge[i].y >> edge[i].z;
sort(edge + 1, edge + m + 1);
for (rint i = 1; i <= m; i++)
{
int idx = 0, maxx = 0;
for (rint j = 1; j <= n; j++) fa[j] = j;
for (rint j = i; j <= m; j++)
{
int x = find(edge[j].x);
int y = find(edge[j].y);
if (x == y) continue;
fa[x] = y;
idx++;
maxx = max(maxx, edge[j].z - edge[i].z);
if (idx == n - 1)
{
break;
}
}
if (idx < n - 1) continue;;
ans = min(ans, maxx);
}
if (ans >= inf) cout << -1 << endl;
else cout << ans << endl;
return 1;
}
signed main()
{
while (kruscal());
return 0;
}
CF1242B
题目大意
求出一张 01 完全图的最小生成树的权值
题目分析
数据范围很大,直接 Kruscal 不了。考虑返璞归真使用 dfs
求出所有可以用 0 边连成的联通块,那么将这些联通快连起来,就可以构造出一颗最小生成树
int n, m;
int ans;
unordered_map<int, int> g[N];
set<int> s, s1;
void dfs(int x)
{
vector<int> v;
v.clear(), s1.clear();
for (auto i = s.begin(); i != s.end(); i++)
{
if (!g[x][*i]) v.push_back(*i);
else s1.insert(*i);
}
s = s1;
for (rint i : v) dfs(i);
}
signed main()
{
cin >> n >> m;
for (rint i = 1; i <= m; i++)
{
int a, b;
cin >> a >> b;
g[a][b] = g[b][a] = 1;
}
for (rint i = 1; i <= n; i++) s.insert((int)i);
while (!s.empty())
{
ans++;
int t = *s.begin();
s.erase(t);
dfs(t);
}
cout << ans - 1 << endl;
return 0;
}