Date:2022.04.06
题意描述:
小明正在做一个网络实验。
他设置了 n 台电脑,称为节点,用于收发和存储数据。
初始时,所有节点都是独立的,不存在任何连接。
小明可以通过网线将两个节点连接起来,连接后两个节点就可以互相通信了。
两个节点如果存在网线连接,称为相邻。
小明有时会测试当时的网络,他会在某个节点发送一条信息,信息会发送到每个相邻的节点,之后这些节点又会转发到自己相邻的节点,直到所有直接或间接相邻的节点都收到了信息。
所有发送和接收的节点都会将信息存储下来。
一条信息只存储一次。
给出小明连接和测试的过程,请计算出每个节点存储信息的大小。
输入格式
输入的第一行包含两个整数 n,m,分别表示节点数量和操作数量。
节点从 1 至 n 编号。
接下来 m 行,每行三个整数,表示一个操作。
如果操作为 1 a b,表示将节点 a 和节点 b 通过网线连接起来。当 a = b 时,表示连接了一个自环,对网络没有实质影响。
如果操作为 2 p t,表示在节点 p 上发送一条大小为 t 的信息。
输出格式
输出一行,包含 n 个整数,相邻整数之间用一个空格分割,依次表示进行完上述操作后节点 1 至节点 n 上存储信息的大小。
数据范围
1≤n≤10000,
1≤m≤10^5,
1≤t≤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
(
N
∗
M
)
O(N*M)
O(N∗M)。
代码如下:
#include <bits/stdc++.h>
using namespace std;
const int N = 10010;
typedef long long LL;
LL p[N],n,m,ans[N];
LL find(LL x)
{
if(p[x]!=x) return p[x]=find(p[x]);
return p[x];
}
int main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++) p[i]=i;
while(m--)
{
LL op,a,b;cin>>op>>a>>b;
if(op==1) p[find(a)]=find(b);
else
{
LL jia=find(a);
for(int i=1;i<=n;i++)
if(find(i)==jia) ans[i]+=b;
}
}
for(int i=1;i<=n;i++) cout<<ans[i]<<' ';
return 0;
}
7/10,t了三个点。
思路②:我们发现瓶颈在每次修改集合中所有点的权值,因此我们优化的核心思想是“给某个集合中所有结点加一个权值时,只加到某个代表结点上”。正常思维来想,我们一定先考虑加到根节点上,那是否可行?我们顺着推一下。
由上,我们约定:每个点的真实权值规定为“该点到根节点路径上所有权值之和”。
题意给了我们两个操作:
①
o
p
=
=
1
:
op==1:
op==1:给一个集合的所有结点加上一个权值。这个显然很好办,就等价于给这个集合的根节点加上一个权值。我们画个图来理解一下。
图中红色的表示每个点的真实权值。
②
o
p
=
=
2
:
op==2:
op==2:合并两个分别以
a
a
a和
b
b
b为根的集合。显然我们不能不管真实权值直接合并,例如我们要将以
a
a
a为根的集合合并到以
b
b
b为根的集合,且合并后“新根”为
b
b
b,那么如果直接合并相当于将之前以
a
a
a为根的所有结点的真实权值全加了
b
b
b,显然不合理。
我们有两种方法解决这个问题:
I
I
I.合并时,构建一个权值为
0
0
0的“虚拟新根”,如图:
I
I
.
II.
II.我们仍然让
b
b
b为合并后的“新根”,但对于合并过来的
a
a
a的权值
d
[
a
]
−
=
d
[
b
]
d[a]-=d[b]
d[a]−=d[b],这样就弥补了原以
a
a
a为根的集合中所有点上权值的差距。见下图:
问题还没有完全解决,我们并查集过程中为保证复杂度接近
O
(
1
)
O(1)
O(1)要“路径压缩”,但路径压缩时显然不能不更改每个点的真实权值,那我们怎么改变呢?
首先我们要明白路径压缩的原理,这里的并查集为保证时间复杂度采取了递归版,因此经过路径压缩最终能保证“所有节点都与集合的根节点直接相连”。
【这里建议先看一下这篇文章。】
我们注意到在路径压缩过程中,有的点经过路径压缩后和根节点相对位置不会改变,因此在路径压缩过程中真实权值不需要变动。讨论一下:
①若一个点
x
x
x有:
p
[
x
]
=
=
x
p[x]==x
p[x]==x ||
p
[
p
[
x
]
]
=
=
p
[
x
]
p[p[x]]==p[x]
p[p[x]]==p[x],也就是
x
x
x为根或
x
的
父
节
点
p
[
x
]
x的父节点p[x]
x的父节点p[x]为根时,不需要变动
x
x
x的真实权值。
②否则需要变动。如何变动?见下图。
观察到我们需要变动
d
[
x
]
d[x]
d[x],因为如果不变动经过路径压缩点
x
x
x的真实权值就是
d
[
x
]
+
d
[
u
]
d[x]+d[u]
d[x]+d[u],而真正的真实权值是
d
[
x
]
+
d
[
p
[
x
]
]
+
d
[
u
]
d[x]+d[p[x]]+d[u]
d[x]+d[p[x]]+d[u],因此
d
[
x
]
+
=
d
[
p
[
x
]
]
d[x]+=d[p[x]]
d[x]+=d[p[x]]即完成更新。
至此,完成题目。(别忘了并查集初始化,
p
[
i
]
=
i
;
p[i]=i;
p[i]=i;)
代码如下:
#include <bits/stdc++.h>
using namespace std;
const int N = 10010;
typedef long long LL;
LL p[N],n,m,ans[N],d[N];
LL find(LL x)
{
if(p[x]==x||p[p[x]]==p[x]) return p[x];//注意这里要返回p[x],也就是根。
//路径压缩前后点x与根节点(x本身或其父节点p[x])的相对位置未变,则不用改变每个点的真实值。
LL r=find(p[x]);
//先把其父节点路径压缩,并且真实权值转化好。
//递归的过程。
d[x]+=d[p[x]];//再将自己的真实权值转化。
p[x]=r;//路径压缩最后一步,将x与根节点r直接相连。
return r;
}
int main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++) p[i]=i;
while(m--)
{
LL op,a,b;cin>>op>>a>>b;
if(op==1)
{
a=find(a),b=find(b);//这里已经路径压缩了,值也都合法
if(a!=b)//表示操作1,合并两个集合。
{
d[a]-=d[b];//选用方法II,避免集合a中所有真实值错误。
p[a]=b;//将以a为根的集合合并到以b为根的集合中。
}
}
else
{
a=find(a);
d[a]+=b;//给根节点加上权值b,代表给集合里所有点加上权值b。
}
}
for(int i=1;i<=n;i++)
if(i==find(i)) cout<<d[i]<<' ';//自己就是根。
else cout<<d[i]+d[find(i)]<<' ';//压缩后每个点到根距离都是1(因为路径都压缩了),因此点的真实距离就是点与根的权值相加。
return 0;
}