走廊泼水节
测试点数 10
总时限 10 s
总内存 256 MiB
通过率 41/55
描述
【简化版题意】给定一棵 \(N\) 个节点的树,要求增加若干条边,把这棵树扩充为完全图,并满足图的唯一最小生成树仍然是这棵树。求增加的边的权值总和最小是多少。
我们一共有 \(N\) 个OIER打算参加这个泼水节,同时很凑巧的是正好有 \(N\) 个水龙头(至于为什么,我不解释)。 \(N\) 个水龙头之间正好有 \(N-1\) 条小道,并且每个水龙头都可以经过小道到达其他水龙头(这是一棵树,你应该懂的..)。但是OIER门为了迎接中中的挑战,决定修建一些个道路(至于怎么修,秘密~),使得每个水龙头到每个水龙头之间都有一条直接的道路连接(也就是构成一个完全图呗~)。但是OIER门很懒得,并且记性也不好,他们只会去走那 \(N-1\) 条小道,并且希望所有水龙头之间修建的道路,都要大于两个水龙头之前连接的所有小道(小道当然要是最短的了)。所以神COW们,帮那些OIER们计算一下吧,修建的那些道路总长度最短是多少,毕竟修建道路是要破费的~~
输入格式
本题为多组数据~
第一行 \(t\) ,表示有 \(t\) 组测试数据
对于每组数据
第一行 \(N\) ,表示水龙头的个数(当然也是OIER的个数);
\(2\) 到 \(N\) 行,每行三个整数 \(X,Y,Z\) ;表示水龙头 \(X\) 和水龙头 \(Y\) 有一条长度为 \(Z\) 的小道
输出格式
对于每组数据,输出一个整数,表示修建的所有道路总长度的最短值。
样例输入
2
3
1 2 2
1 3 3
4
1 2 3
2 3 4
3 4 5
样例输出
4
17
数据范围与约定
每个测试点最多10组测试数据
50% n<=1500;
100% n<=6000
100% z<=100
样例解释
第一组数据,在 \(2\) 和 \(3\) 之间修建一条长度为4的道路,是这棵树变成一个完全图,且原来的树依然是这个图的唯一最小生成树.
思路
\(N\) 个节点的树有 \(N-1\) 条边。把这 \(N-1\) 条边按照权值从大到小排序,依次扫描每条边,执行一个类似于 \(Kruskal\) 算法的过程。
设当前扫描到边 \((x,y,z)\),\(x\) 所在的并查集为 \(S_x\) ,\(y\) 所在的并查集为 \(S_y\) ,此时应该合并 \(S_x\) 和 \(S_y\) 。合并后的集合 \(S_x \cup S_y\) 构成一棵树的结构。
\(\forall u\in S_x,v\in S_y\) ,若 \((u,v)\neq(x,y)\) ,则在最终的完全图中, 我们肯定需要在 \((u,v)\) 之间增加一条边。于是无向边 \((u,v)\)、\(S_x\) 中从 \(u\) 到 \(x\) 的路径、无向边 \((x,y)\) 以及 \(S_y\) 中从 \(v\) 到 \(y\) 的路径共同构成一个环。
此时,为了保证 \((x,y)\) 一定在最小生成树中,就必须让 \((x,y)\) 是连接集合 \(S_x\) 和 \(S_y\) 的权值最小的边(否则就有“用 \((u,v)\) 替换 \((x,y)\) ”的方案)。因此,\((u,v)\) 的边权应该定为 \(z+1\)。
\(S_x\) 与 \(S_y\) 之间一共会增加 |\(S_x\)| \(*\) |\(S_y\)|\(-1\) 条边,所以我们把 \((z+1) * (\)|\(S_x\)| \(*\) |\(S_y\)|\(-1)\) 累加到答案中即可。算法时间复杂度为 \(O(N log N)\) 。
代码
#include<cstdio>
#include<cctype>
#include<iostream>
#include<cstring>
#include<algorithm>
#define rg register
#define int long long
using namespace std;
inline int read(){
rg int f = 0, x = 0;
rg char ch = getchar();
while(!isdigit(ch)) f |= (ch == '-'), ch = getchar();
while( isdigit(ch)) x = (x << 1) + (x << 3) + (ch ^ 48), ch = getchar();
return f? -x: x;
}
const int N = 6010;
int T, n, f[N], ans, s[N];
struct edge{
int u, v, w;
}e[N];
inline bool cmp(rg edge a, rg edge b){
return a.w < b.w;
}
inline void init(){
for(rg int i = 1; i <= n; ++i) f[i] = i, s[i] = 1; //s表示集合初始化要包含本身所以是1
ans = 0;
memset(e,0,sizeof(e));
}
inline int find(int v){
return f[v] == v? v: f[v] = find(f[v]);
}
inline void merge(rg int u, rg int v, rg int w){
int x1 = find(u);
int x2 = find(v);
if(x1 != x2){
ans += (w + 1) * (s[x1] * s[x2] - 1);
f[x2] = x1;
s[x1] += s[x2];
}
}
signed main(){
T = read();
for(rg int o = 1; o <= T; ++o){
n = read();
init();
for(rg int i = 1; i < n; ++i){
e[i].u = read();
e[i].v = read();
e[i].w = read();
}
sort(e + 1, e + n, cmp);
for(rg int i = 1; i < n; ++i) merge(e[i].u, e[i].v, e[i].w);
printf("%lld\n", ans);
}
return 0;
}