网络分析

网络分析

小明正在做一个网络实验。

他设置了 $n$ 台电脑,称为节点,用于收发和存储数据。

初始时,所有节点都是独立的,不存在任何连接。

小明可以通过网线将两个节点连接起来,连接后两个节点就可以互相通信了。

两个节点如果存在网线连接,称为相邻。

小明有时会测试当时的网络,他会在某个节点发送一条信息,信息会发送到每个相邻的节点,之后这些节点又会转发到自己相邻的节点,直到所有直接或间接相邻的节点都收到了信息。

所有发送和接收的节点都会将信息存储下来。

一条信息只存储一次。

给出小明连接和测试的过程,请计算出每个节点存储信息的大小。

输入格式

输入的第一行包含两个整数 $n,m$,分别表示节点数量和操作数量。

节点从 $1$ 至 $n$ 编号。

接下来 $m$ 行,每行三个整数,表示一个操作。

  1. 如果操作为 1 a b ,表示将节点 $a$ 和节点 $b$ 通过网线连接起来。当 $a = b$ 时,表示连接了一个自环,对网络没有实质影响。
  2. 如果操作为 2 p t ,表示在节点 $p$ 上发送一条大小为 $t$ 的信息。

输出格式

输出一行,包含 $n$ 个整数,相邻整数之间用一个空格分割,依次表示进行完上述操作后节点 $1$ 至节点 $n$ 上存储信息的大小。

数据范围

$1 \leq n \leq 10000$,
$1 \leq m \leq {10}^{5}$,
$1 \leq t \leq 100$

输入样例1:

4 8
1 1 2
2 1 10
2 3 5
1 4 1
2 2 2
1 1 2
1 2 4
2 2 1

输出样例1:

13 13 5 3

 

解题思路

  当时写这题的时候想到了怎么做,但做法有点暴力。

  首先很容易想到用并查集来维护点的连通性。对于第二个操作,如果每次都直接给该连通块内的每个点加上一个数,那么时间复杂度就会达到$O \left( {n \times m} \right)$。那么我们可以想想,能不能只给代表元素这一个点加呢?意味着每次要给某个连通块内的点都加上一个数时,都只加在该连通块的代表元素上,加上的这个数意味着整一个连通块中的所有点都会加上这个数。那么最后每个结点最终加的数就应该等于这个点到根节点的路径的权值和。比如下面这个例子:

  如果要合并两个集合,那么要把被合并的那个集合和的根节点的值减去合并后那个集合的根节点的值:

  在路径压缩的中维护每个结点权值的方式是,

  • 如果某个结点就是根结点(代表元素),那么不用更新权值,直接返回根节点。
  • 如果某个结点的父节点是根节点,那么也不用更新,直接返回根节点。
  • 否则,这个结点的父节点不是根节点,那么其父节点会经过若干个路径指向根节点。那么先递归处理父节点,让父节点指向根节点并更新父节点权值,最后再让该结点指向根节点时,更新该结点的权值。由于递归处理后父节点的权值就变为由父节点到根节点的路径的权值和,在对该结点进行路径压缩前(指向根节点),就是要把权值更新成该结点到根节点的路径的权值和,并且一开始该结点存的数是该结点到父节点的这一条路径的权值和,因此只需要把当前值再加上更新后的父节点的权值,就是该结点到根节点的路径的权值和。

  AC代码如下:

 1 #include <cstdio>
 2 #include <algorithm>
 3 using namespace std;
 4 
 5 const int N = 1e4 + 10;
 6 
 7 int fa[N], cnt[N];
 8 
 9 int find(int x) {
10     if (fa[x] == x || fa[fa[x]] == fa[x]) return fa[x];
11     
12     int p = find(fa[x]);
13     cnt[x] += cnt[fa[x]];
14     
15     return fa[x] = p;
16 }
17 
18 int main() {
19     int n, m;
20     scanf("%d %d", &n, &m);
21     for (int i = 0; i <= n; i++) {
22         fa[i] = i;
23     }
24     
25     while (m--) {
26         int op, a, b;
27         scanf("%d %d %d", &op, &a, &b);
28         if (op == 1) {
29             if (find(a) == find(b)) continue;   // 如果两个结点在同一个集合就不能再合并,否则会减去多余的值
30             cnt[find(a)] -= cnt[find(b)];
31             fa[find(a)] = find(b);
32         }
33         else {
34             cnt[find(a)] += b;
35         }
36     }
37     
38     for (int i = 1; i <= n; i++) {
39         if (find(i) == i) printf("%d ", cnt[i]);
40         else printf("%d ", cnt[i] + cnt[find(i)]);
41     }
42     
43     return 0;
44 }

  还有一种合并的方法是,直接把两个集合合并到一个新的结点上,这个新的结点的权值为$0$,这样就不需要减去某个根节点的值了。

  AC代码如下:

 1 #include <cstdio>
 2 #include <algorithm>
 3 using namespace std;
 4 
 5 const int N = 2e4 + 10;
 6 
 7 int fa[N], cnt[N];
 8 
 9 int find(int x) {
10     if (fa[x] == x || fa[fa[x]] == fa[x]) return fa[x];
11     
12     int p = find(fa[x]);
13     cnt[x] += cnt[fa[x]];
14     
15     return fa[x] = p;
16 }
17 
18 int main() {
19     int n, m;
20     scanf("%d %d", &n, &m);
21     for (int i = 0; i <= n << 1; i++) {
22         fa[i] = i;
23     }
24     
25     int k = n + 1;
26     while (m--) {
27         int op, a, b;
28         scanf("%d %d %d", &op, &a, &b);
29         if (op == 1) {
30             if (find(a) == find(b)) continue;
31             fa[find(a)] = fa[find(b)] = k++;    // 合并到一个新开的结点,新开的点的权值为0
32         }
33         else {
34             cnt[find(a)] += b;
35         }
36     }
37     
38     for (int i = 1; i <= n; i++) {
39         if (find(i) == i) printf("%d ", cnt[i]);
40         else printf("%d ", cnt[i] + cnt[find(i)]);
41     }
42     
43     return 0;
44 }

  最后贴出我一开始的做法,当时写的时候不会写维护权值的路径压缩函数,因此每次合并时,用一个额外的集合来维护根节点包含哪些子节点,保证树的深度不超过$1$,即除去根节点外,该集合中其余的点都是叶子结点,这样就不用考虑在路径压缩时维护每个结点的权值了。不过这种方法会被卡,就是每次都把数量多的集合合并到数量小的集合的时候,因此需要额外维护每个集合包含结点的数量,每次都把数量小的集合合并到数量多的集合。

  AC代码如下:

 1 #include <cstdio>
 2 #include <unordered_set>
 3 #include <algorithm>
 4 using namespace std;
 5 
 6 const int N = 1e4 + 10;
 7 
 8 int fa[N], cnt[N], num[N];
 9 unordered_set<int> st[N];
10 
11 int find(int x) {
12     return fa[x] == x ? fa[x] : fa[x] = find(fa[x]);
13 }
14 
15 int main() {
16     int n, m;
17     scanf("%d %d", &n, &m);
18     for (int i = 0; i <= n; i++) {
19         fa[i] = i;
20         num[i] = 1;
21     }
22     
23     while (m--) {
24         int op, a, b;
25         scanf("%d %d %d", &op, &a, &b);
26         if (op == 1) {
27             if (find(a) == find(b)) continue;
28             
29             int pa = find(a), pb = find(b);
30             if (num[pa] > num[pb]) swap(pa, pb);    // 保证每次都用多的合并少的
31             num[pb] += num[pa];
32             
33             for (auto &it : st[pa]) {
34                 cnt[it] += cnt[pa] - cnt[pb];   // 加上根节点的值就是这个结点原本的权值,再减去新的根节点的权值
35                 fa[it] = pb;    // 一个集合的叶子节点变成另外一个集合的叶子节点
36                 st[pb].insert(it);  // 维护记录根节点包含的叶子结点
37             }
38             
39             st[pa].clear();
40             cnt[pa] -= cnt[pb]; // 最后处理被合并集合的根节点
41             fa[pa] = pb;
42             st[pb].insert(pa);
43         }
44         else {
45             cnt[find(a)] += b;
46         }
47     }
48     
49     for (int i = 1; i <= n; i++) {
50         if (find(i) == i) printf("%d ", cnt[i]);
51         else printf("%d ", cnt[i] + cnt[find(i)]);
52     }
53     
54     return 0;
55 }

 

参考资料

  AcWing 2069. 网络分析(蓝桥杯C++ AB组辅导课):https://www.acwing.com/video/2022/

posted @ 2022-03-23 15:04  onlyblues  阅读(168)  评论(0编辑  收藏  举报
Web Analytics