第十一届蓝桥杯省赛CB第一场-10.网络分析【带权并查集】

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(NM)
代码如下:

#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] xp[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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值