原题链接:E. Building Forest
题目大意:
给定 n n n 次操作,每次操作要操作 k k k 次。
对于第 i i i 次操作而言:
- 先给出一个数字 k k k ,之后跟着 2 k 2k 2k 个数字: v 1 , w 1 , v 2 , w 2 . . . v k , w k v_1,w_1,v_2,w_2...v_k,w_k v1,w1,v2,w2...vk,wk。
- 对于每一组 v j , k j ( 1 ≤ j ≤ k ) v_j,k_j(1 \leq j \leq k) vj,kj(1≤j≤k) 而言,代表着从 v j v_j vj 出发,到达 v j v_j vj 所在连通块的根节点(设为 r o o t root root ),并且让该根节点对点 i i i 连上一条权值为 d i s t ( v j , r o o t ) + w j dist(v_j,root)+w_j dist(vj,root)+wj 的单向边,其中 d i s t ( u , v ) dist(u,v) dist(u,v) 代表着 u → v u \rightarrow v u→v 的最短路长度。
- 保证所加的边不会有自环和重边,且不会形成环(即一定会是一张 D A G DAG DAG 图)。
在你执行完所有操作后,问你这一张 D A G DAG DAG 图上的所有边权的权值之和是多少,输出这个权值和对 1 0 9 + 7 10^{9}+7 109+7 取模的值。
解题思路:
看到一眼就想到了带权并查集。
我们引入一个数据结构。
带权并查集
这个并查集维护的是点的连通性,以及集合中任意两点之间的距离(注意,这个"距离"并不一定代表着原图两点的距离,根据合并情况的不同,代表的信息会是不一样的),并查集能做到的带权并查集也能做到,带权并查集事实上就是并查集的另一扩展。
带权并查集事实上是将一些要维护的信息作为"边权",这些信息构成的内容变成"距离",然后经过合并集合后我们就可以高效的查询集合内的信息了。
下面讲解一下带权并查集维护信息的方式。
路径压缩
我们考虑一下在并查集路径压缩时,如果带上边权,我们的权值应该怎么样来进行维护。
假设我们的边权为 w w w ,距离数组 d i s u dis_u disu 代表点 u u u 到并查集的根节点的距离,我们来考虑下图的信息合并。
假设我们最初时有两个分别以 1 , 2 1,2 1,2 为根的集合,我们将其合并了,但我们现在想把节点 3 3 3 的路径压缩到根节点 1 1 1 上,考虑如何进行合并。
我们将每个边带上边权,我们最初时的状态是这样的:
此时我们要把集合
2
2
2 合并到集合
1
1
1 上,并且这一条边的边权权值为
3
3
3 。
我们的 d i s u dis_u disu 代表的是点 u u u 到达该点的根节点 r o o t root root 的距离,那么显然,我们的 d i s 2 = 0 → d i s 2 = 3 dis_2 = 0 \rightarrow dis_2 = 3 dis2=0→dis2=3 ,这样我们的 d i s 2 dis_2 dis2 就维护好了,此时我们两个集合的根节点就变成了 1 1 1 。
此时我们就只有
d
i
s
3
dis_3
dis3 没有向上压缩路径了,考虑如何将其合并到节点
1
1
1 上 。
注意到一个问题,我们在合并集合时,由于我们使用的是递归式合并,我们执行合并时一定都是两个集合进行合并。并且其内部的所有点都会合并到其对应的根节点上,不会出现类似链状的情况。
那么我们递归向上进行合并时,我们的父节点一定会与我们所在集合其对应的根节点
1
1
1 相连,那么我们一定会转化到下面这种情况:
那么此时我们的
3
3
3 想要与
1
1
1 直接相连,那么我们的边权就显而易见了:
d i s 3 = d i s 2 + d i s 3 \begin{array}{c} dis_3=dis_2+dis_3 \end{array} dis3=dis2+dis3
这样子,我们的路径压缩就成功了。
function<int(int)> find = [&](int x) {
if (x == fa[x]) return x;
find(fa[x]); dis[x] += dis[fa[x]];//权值加上父亲的距离
return fa[x] = fa[fa[x]];
};
集合合并
假设我们最初时有两个分别以
1
,
2
1,2
1,2 为根的集合,现在我想让两个集合相互连通,同时也要知道联通后这两个集合中任意两点的距离,考虑怎么维护这个信息。
(首先强调,这里的边并不一定代表着单向边,笔者为了方便标记,图中的这些单向边的意义是指出表示一个点的根节点,而在带权并查集中这些单向边应该是双向边)
假设我们让点 3 3 3 和点 4 4 4 连上一条边,权值为 4 4 4 ,考虑要如何把两个集合合并。首先我们肯定要将边 ( 2 , 3 ) (2,3) (2,3) 其转化到 ( 1 , 2 ) (1,2) (1,2) 上来进行合并,才能保证合并不会破坏并查集的结构。
那么这一条边权会是多少呢?
首先先说明一个定义,在所有路径都已经被压缩的情况下,我们的
d
i
s
u
dis_u
disu 数组代表的是:我们从点
u
u
u 出发到达点
r
o
o
t
root
root ,经过的所有"边权"的权值之和为
d
i
s
u
dis_u
disu 。
那么对于上图来说,我们要连上 2 → 1 2 \rightarrow 1 2→1 这条边,其权值便代表的是新的 d i s 2 dis_2 dis2 的值,即 d i s t ( 2 , 1 ) dist(2,1) dist(2,1) 的值。
又因为我们是通过 ( 3 , 4 ) (3,4) (3,4) 这一条边将两个集合相连的,所以要我们会从点 2 2 2 出发,经过这一条边再到达点 1 1 1 ,那么这一条边的权值就是。
d i s 2 = d i s 4 + w − d i s 3 \begin{array}{c} dis_2=dis_4+w-dis_3 \end{array} dis2=dis4+w−dis3
还有要注意的一点就是,根据连边的情况不同,合并后所代表的信息同时也会不同,因此要注意合并的顺序。
bool merge(int x, int y, i64 w) {
int xf = find(x), yf = find(y);
if (xf == yf) return false;//如果已经在一个集合内 则不合并
siz[yf] += siz[xf]; fa[xf] = yf;//否则改变根节点的父节点
dis[xf] = -dis[x] + w + dis[y];//改变根节点的边权
return true;
}
查询距离
既然我们以及将集合合并成为了另一个集合:
从下图:
变为:
现在我们要询问两个点之间的"距离"?
- 我们要查询 d i s t ( 1 , 2 ) dist(1,2) dist(1,2) 的值,那么答案即为 d i s 1 − d i s 4 dis_1 - dis_4 dis1−dis4 。
- 我们要查询 d i s t ( 3 , 2 ) dist(3,2) dist(3,2) 的值,那么答案即为 d i s 3 − d i s 4 dis_3 - dis_4 dis3−dis4 。
- 我们要查询 d i s t ( 2 , 3 ) dist(2,3) dist(2,3) 的值,那么答案即为 d i s 2 − d i s 3 dis_2 - dis_3 dis2−dis3 。
同理,询问根据查询的不同也会有所不同。
i64 dist(int x, int y) {
int xf = find(x), yf = find(y);
if (xf != yf) return -1;
return abs(dis[x] + -dis[y]);
}
回到本题:
题目要求的是点 v j v_j vj 到达根节点 r o o t root root 的距离 d i s t ( v 1 , r o o t ) + w j dist(v1,root)+w_j dist(v1,root)+wj,考虑如何用带权并查集维护这个问题。
注意到题目的特殊性质:
让该根节点对点 i i i 连上一条权值为 d i s t ( v j , r o o t ) + w j dist(v_j,root)+w_j dist(vj,root)+wj 的单向边 。
如果我们让边权的信息为原图的边权,距离的信息为原图中 v v v 到达 r o o t root root 的距离,且按照题中的连边方式的话,我们查询的信息 d i s t ( v , r o o t ) dist(v,root) dist(v,root) 是能正确被维护出来的。
但是任取两点 d i s t ( u , v ) dist(u,v) dist(u,v) 得到的距离是不对的,这个可以自己思考一下。
那么我们直接按照带权并查集的过程模拟操作即可。
时间复杂度: O ( n log n ) O(n \log n) O(nlogn)(事实上不是 log n \log n logn 的复杂度)
AC代码
#include <bits/stdc++.h>
#define YES return void(cout << "Yes\n")
#define NO return void(cout << "No\n")
using namespace std;
using ui64 = unsigned long long;
using PII = pair<int, int>;
using i64 = long long;
const int mod = 1e9 + 7;
struct DSU2 {
vector<int> fa, siz;
vector<i64> dis;
DSU2(int n) : fa(n + 1), siz(n + 1, 1), dis(n + 1) { iota(fa.begin(), fa.end(), 0); };
function<int(int)> find = [&](int x) {
if (x == fa[x]) return x; find(fa[x]);
(dis[x] += dis[fa[x]]) %= mod;
return fa[x] = fa[fa[x]];
};
int size(int x) { return siz[find(x)]; }
i64 dist(int x, int y) {
int xf = find(x), yf = find(y);
if (xf != yf) return -1;
return dis[x] + -dis[y];
}
bool same(int x, int y) { return find(x) == find(y); }
bool merge(int x, int y, i64 w) {
int xf = find(x), yf = find(y);
if (xf == yf) return false;
siz[yf] += siz[xf]; fa[xf] = yf;
(dis[xf] = -dis[x] + w + dis[y]) %= mod;
return true;
}
};
void solve() {
int n;
cin >> n;
DSU2 dsu(n);
i64 ans = 0;
for (int i = 1; i <= n; ++i) {
int x; cin >> x;
for (int j = 1; j <= x; ++j) {
i64 v, w;
cin >> v >> w;
int root = dsu.find(v);
//注意查询的顺序 v -> root 的距离
i64 W = (dsu.dist(v, root) + w) % mod;
//注意连边的顺序 root -> i 权值为 w
dsu.merge(root, i, W);
(ans += W) %= mod;
}
}
cout << (ans + mod) % mod << '\n';
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int t = 1; //cin >> t;
while (t--) solve();
return 0;
}