简介
在并查集的边上定义某种权值和这种权值在路径压缩时产生的运算,从而解决更多的问题。
为了维护并查集中的边权,需要将边权下放到子节点中存储。因此,每个节点存储的都是它到它的父节点之间的边权。只有当一个节点的父节点发生变化时,才需要相应地调整边权。一般情形中,这可能发生在路径压缩和合并两个节点时。例如,如果边权是当前节点与父节点之间的距离,那么,在路径压缩时,每次将当前节点的父节点替换为根节点,都需要将父节点到根节点的距离加到当前节点存储的边权上;类似地,在合并两个节点所在集合时,需要计算两个根节点之间新连接的边的权值。
贴几道题做练习。
1.模板带权并查集入门 蓝桥杯 2022 省 A 推导部分和
cpp
#include<bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
bool tin =0;
const int N=100005;
int fa[N],dist[N];
int find(int x)
{
if(x!=fa[x])
{
int t=fa[x];
fa[x]=find(fa[x]);
dist[x]+=dist[t];
//区别于普通的并查集,我们当前点距离父亲的父亲的距离,应该加上父亲与父亲的父亲的距离
//本身和普通并查集一样,不过多了加上父距离这一操作
}
return fa[x];
}
void uni(int a,int b,int v)
{
int faa=find(a),fbb=find(b);
if(faa==fbb)
return ;
dist[faa]=v+dist[b]-dist[a];
//合并时,只是规范父亲的距离,v+dist[b]即是a到fbb的距离,所以要减去dist[a]
//就变成了faa到fbb的了,对于a,b节点,在find中会再进行规整
fa[faa]=fbb;
}
void solve()
{
int n,m,q,a,b,v;
cin>>n>>m>>q;
n++;
for(int i=1;i<=n;i++)
fa[i]=i,dist[i]=0;
for(int i=0;i<m;i++)
{
cin>>a>>b>>v;
b++;
//我们需要通过类似前缀和的方式来求区间和,所以我们把右边界扩大1来实现[l,r),这样在求区间[l,r]时,直接使用a,b前缀和操作即可
uni(a,b,v);
}
for(int i=0;i<q;i++)
{
cin>>a>>b;
b++;
int ta=find(a),tb=find(b);
if(ta!=tb)
{
cout<<"UNKNOWN"<<endl;
}
else
{
cout<<dist[a]-dist[b]<<endl;
}
}
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);
int T=1;
if(tin)
cin>>T;
while(T--)
{
solve();
}
return 0;
}
2.模板 HNOI2005 狡猾的商人
cpp
#include<bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
bool tin =1;
const int N=105;
int fa[N],dist[N];
int find(int x)
{
if(x!=fa[x])
{
int t=fa[x];
fa[x]=find(fa[x]);
dist[x]+=dist[t];
//区别于普通的并查集,我们当前点距离父亲的父亲的距离,应该加上父亲与父亲的父亲的距离
//本身和普通并查集一样,不过多了加上父距离这一操作
}
return fa[x];
}
void uni(int a,int b,int v)
{
int faa=find(a),fbb=find(b);
if(faa==fbb)
return ;
dist[faa]=v+dist[b]-dist[a];
//合并时,只是规范父亲的距离,v+dist[b]即是a到fbb的距离,所以要减去dist[a]
//就变成了faa到fbb的了,对于a,b节点,在find中会再进行规整
fa[faa]=fbb;
}
void solve()
{
int n,m,a,b,v;
cin>>n>>m;
n++;
for(int i=1;i<=n;i++)
fa[i]=i,dist[i]=0;
int ok=1;
for(int i=0;i<m;i++)
{
cin>>a>>b>>v;
b++;
if(find(a)==find(b))//是相同集合就判断
{
if(dist[a]-dist[b]!=v)
ok=0;
}
else//非相同集合就合并
uni(a,b,v);
}
cout<<(ok?"true":"false")<<endl;//使用一些三目运算符
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);
int T=1;
if(tin)
cin>>T;
while(T--)
{
solve();
}
return 0;
}
3.模板航电oj 有多少答案是错的
cpp
#include<iostream>
#define int long long
#define endl "\n"
using namespace std;
bool tin =0;
const int N=200005;
int fa[N],dist[N];
int find(int x)
{
if(fa[x]!=x)
{
int t=fa[x];
fa[x]=find(fa[x]);
dist[x]+=dist[t];
}
return fa[x];
}
void uni(int a,int b,int c)
{
int faa=find(a),fbb=find(b);
dist[faa]=c+dist[b]-dist[a];
fa[faa]=fbb;
}
void solve()
{
int n,m;
while((cin>>n>>m))//多样例
{
int a,b,c;
n++;
for(int i=0;i<=n;i++)//重置
{
fa[i]=i;
dist[i]=0;
}
int ans=0;
for(int i=0;i<m;i++)
{
cin>>a>>b>>c;
b++;
int fa=find(a),fb=find(b);
if(fa!=fb)
uni(a,b,c);
else
{
if(dist[a]-dist[b]!=c)
ans++;
}
}
cout<<ans<<endl;
}
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);
int T=1;
if(tin)
cin>>T;
while(T--)
{
solve();
}
return 0;
}
需要注意的是,这道题是多实例,但是题目并没有写(卡了我一个多小时找没有的错误,我甚至都在怀疑其实这个板子是错误板子了......)
4.设置距离洛谷P1196 NOI2002 银河英雄传说
cpp
#include "bits/stdc++.h"
#define int long long
#define endl "\n"
using namespace std;
bool tin = 0;
const int N=300005;
int dis[N],fa[N],siz[N];//分别为 与父节点的距离 父节点 当前列大小
int find(int x)
{
if(fa[x]!=x)
{
int t=fa[x];//暂时保留当前父亲
fa[x]=find(fa[x]);//向上先更新父亲的最大祖先
dis[x]+=dis[t];//当前距离就是距离父亲的加上父亲距离父亲的
}
return fa[x];
}
void uni(int x,int y)
{
int tx=find(x),ty=find(y);
if(tx!=ty)//父亲一样就不动了
{
dis[ty]=siz[tx];//父亲距离就是父亲的长度
siz[tx]+=siz[ty];//父亲接收了当前队列,长度增加接受长度
fa[ty]=tx;//更新父亲
}
}
int dt(int x,int y)
{
int tx=find(x),ty=find(y);
if(tx!=ty)//不在一个队列直接-1
{
return -1;
}
else
{
return abs(dis[x]-dis[y])-1;//在直接abs求大小就是
}
}
void solve()
{
int t;
cin>>t;
for(int i=1;i<=300000;i++)
{
dis[i]=0,siz[i]=1,fa[i]=i;//初始化
}
char op;
int x,y;
while(t--)
{
cin>>op>>x>>y;
if(op=='M')
{
uni(x,y);//合并
}
else
{
cout<<dt(x,y)<<endl;//求值
}
}
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);
int T=1;
if(tin)
cin>>T;
while(T--)
{
solve();
}
return 0;
}
NOI 最简单的一题,你来你也行。
5.维护倍数LCR 111.除法求值
cpp
class Solution {
public:
int fa[60];//父节点
double mul[60];//当前点与根节点倍数
map<string,int> has;//hash数字代替字符串
int find(int x)
{
cout<<x<<" ";
if(fa[x]!=x)
{
int t=fa[x];
fa[x]=find(fa[x]);
mul[x]=1.0*mul[t]*mul[x];//倍数相乘
}
return fa[x];
}
void uni(int x,int y,double m)
{
int tx=find(x),ty=find(y);
if(tx!=ty)
{
mul[tx]=mul[y]*m/mul[x];//可以自己推导一下,合并父节点,那就是父节点之间的倍数
fa[tx]=ty;
}
}
double getmu(int x,int y)
{
int tx=find(x),ty=find(y);
if(tx!=ty)
{
return -1;
}
return mul[x]*1.0/mul[y];//都是父节点倍数,直接除
}
vector<double> calcEquation(vector<vector<string>>& equations, vector<double>& values, vector<vector<string>>& queries) {
vector<double> ans;
int n=values.size();
string s1,s2;
int cnt=0;
for(int i=0;i<n;i++)
{
s1=equations[i][0];//hash处理
if(!has.count(s1))
{
has[s1]=cnt;
fa[cnt]=cnt;
mul[cnt]=1;
cnt++;
}
s1=equations[i][1];
if(!has.count(s1))
{
has[s1]=cnt;
fa[cnt]=cnt;
mul[cnt]=1;
cnt++;
}
}
for(int i=0;i<n;i++)
{
uni(has[equations[i][0]],has[equations[i][1]],values[i]);
}
int t1,t2;
n=queries.size();
for(int i=0;i<n;i++)
{
if(has.count(queries[i][0])&&has.count(queries[i][1]))
{
ans.push_back(getmu(has[queries[i][0]],has[queries[i][1]]));
}
else
{
ans.push_back(-1);
}
}
return ans;
}
};
到这里,这些基础的带权并查集基本都能写了,很简单啊。
6.维护种类P2024 NOI2001 食物链
cpp
#include <bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
bool tin = 0;
const int N=50005;
int dis[N],fa[N];//父节点距离,父亲数组
//我们可以根据与根节点的距离判断出关系,我们设,x与y在同一食物链且dis[y]+1==dis[x],那么x就吃y
//这样我们推导 x吃y y吃z 假设dis[z]=0,y吃z dis[y]=dis[z]+1=1,x吃y,dis[x]=dis[y]+1=2,x到z距离就是2,此时说明x被z吃
//根据食物链 我们确实可以推导出z吃x,此时三种状态同类,吃,被吃我们都分别可以用0,1,2表示出来了
int ans=0;//初始化答案
int find(int x)
{
if(fa[x]!=x)
{
int t=fa[x];
fa[x]=find(fa[x]);
dis[x]=(dis[t]+dis[x])%3;//本质是距离维护,但是根据%3可以维持食物链的物种循环
}
return fa[x];
}
void uni(int x,int y)
{
int tx=find(x),ty=find(y);
if(tx!=ty)
{
fa[tx]=ty;
dis[tx]=(dis[ty]+dis[y]-dis[x]+3)%3;//由于x,y是同一种,所以dis[x]==dis[y],根据这个推导出父亲之间的关系
}
else
{
if(dis[x]!=(dis[y]))
ans++;
}
}
void eat(int x,int y)
{
int tx=find(x),ty=find(y);
if(tx!=ty)
{
fa[tx]=ty;
dis[tx]=(dis[ty]+dis[y]-dis[x]+3+1)%3;////由于x吃y,所以dis[x]==(dis[y]+1)%3,根据这个推导出父亲之间的关系
}
else
{
if(dis[x]!=(dis[y]+1)%3)
ans++;
}
}
void solve()
{
int n,t;
cin>>n>>t;
for(int i=1;i<=n;i++)
{
dis[i]=0,fa[i]=i;
}
char op;
int x,y;
while(t--)
{
cin>>op>>x>>y;
if(x<1||x>n||y<1||y>n)
ans++;
else
{
if(op=='1')
{
uni(x,y);
}
else
{
eat(x,y);
}
}
}
cout<<ans;
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);
int T=1;
if(tin)
cin>>T;
while(T--)
{
solve();
}
return 0;
}
本身其实和距离维护没什么区别,只不过对距离对 3 取模,维护了三个物种。
7.并查集分类P1525 NOIP 2010 提高组 关押罪犯
这题有多种解法,我先贴一个并查集常规分类做法。
cpp
#include <bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
bool tin = 0;
const int N=20005;
int enemy[N],fa[N];
array <int,3> arr[N*10];
int find(int x)//常规路径压缩优化
{
if(fa[x]!=x)
{
fa[x]=find(fa[x]);
}
return fa[x];
}
void uni(int x,int y)//常规合并
{
int tx=find(x),ty=find(y);
fa[tx]=ty;
}
void solve()
{
int n,t,a,b,c,x,y;
cin>>n>>t;
for(int i=1;i<=n;i++)
{
enemy[i]=0,fa[i]=i;
}
for(int i=0;i<t;i++)
{
cin>>arr[i][1]>>arr[i][2]>>arr[i][0];
}
sort(arr,arr+t,greater<array<int,3> >() ) ;//贪心,优先分开仇恨大的
int ans=0;
for(int i=0;i<t;i++)
{
x=arr[i][1],y=arr[i][2];
int tx=find(x),ty=find(y);
if(tx==ty)//无法分开,合并
{
ans=arr[i][0];
break;
}
if(!enemy[tx])//没有敌人,设置敌人
enemy[tx]=ty;
else
{
uni(y,enemy[tx]);//有敌人,把两个敌人合并
}
if(!enemy[ty])//一样操作
enemy[ty]=tx;
else
{
uni(x,enemy[ty]);
}
}
cout<<ans;
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);
int T=1;
if(tin)
cin>>T;
while(T--)
{
solve();
}
return 0;
}
其实这里的 enemy 数组有点难想,不过想明白整体思路还是很简单的。
然后再贴一个二分图解法
cpp
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define endl "\n"
int tin = 0;
vector <array<int,2> > h[20005];
int color[20005];//颜色数组,采用dfs染色判断是否冲突
int n,m;
int dfs(int u,int c,int limit)
{
color[u]=c;
for(auto x:h[u])
{
if(x[1]>limit)//说明当前这俩不能在一起
{
if(color[x[0]])
{
if(color[x[0]]==c)//颜色一致,冲突
return 0;
}
else if(dfs(x[0],3-c,limit)==0) return 0;
}
}
return 1;//最后没有颜色冲突,合规
}
int check(int limit)
{
for(int i=0;i<=n;i++)
{
color[i]=0;
}
for(int i=1;i<=n;i++)
{
if(!color[i])//没有颜色就染
{
if(!dfs(i,1,limit))
{
return 0;
}
}
}
return 1;
}
void solve()
{
cin>>n>>m;
int a,b,c;
for(int i=0;i<m;i++)
{
cin>>a>>b>>c;
h[a].push_back({b,c}),h[b].push_back({a,c});//存图
}
int l=0,r=1000000000;
while(l<r)//二分模板
{
int mid=l+r>>1;
if(check(mid))
r=mid;
else
l=mid+1;
}
cout<<l;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T=1;
if(tin)
cin >> T;
while(T--){
solve();
}
return 0;
}
相比之下我觉得二分图好理解,所以专门贴上
8.并查集分类P1892 BalticOI 2003 团伙 (Day 2)
P1892 BalticOI 2003 团伙 (Day 2) - 洛谷
和上一题基本一样,没什么好说的贴个代码过了
cpp
#include <bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
bool tin = 0;
const int N=5005;
int enemy[N],fa[N];
int find(int x)
{
if(fa[x]!=x)
{
fa[x]=find(fa[x]);
}
return fa[x];
}
void uni(int x,int y)
{
int tx=find(x),ty=find(y);
fa[tx]=ty;
}
void solve()
{
int n,t,a,b,c,x,y;
cin>>n>>t;
for(int i=1;i<=n;i++)
{
enemy[i]=0,fa[i]=i;
}
char op;
for(int i=0;i<t;i++)
{
cin>>op>>x>>y;
if(op=='F')
{
uni(x,y);
}
else
{
if(!enemy[x])
{
enemy[x]=y;
}
else
{
uni(enemy[x],y);
}
if(!enemy[y])
{
enemy[y]=x;
}
else
{
uni(enemy[y],x);
}
}
}
int ans=0;
for(int i=1;i<=n;i++)
{
if(i==find(i))
ans++;
}
cout<<ans;
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);
int T=1;
if(tin)
cin>>T;
while(T--)
{
solve();
}
return 0;
}
9.异或并查集Exclusive-OR UVA-12232
Exclusive-OR - UVA 12232 - Virtual Judge
对于存在真实值和相对值的情况,我们可以引入虚点(原来不存在的点),让真实值挂在虚点下去表示,别的基本和之前一样
cpp
#include <bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
bool tin = 0;
const int N=300005;
int dis[N],fa[N],num[N];
char tos[N];
int find(int x)
{
if(fa[x]!=x)
{
int t=fa[x];
fa[x]=find(fa[x]);
dis[x]^=dis[t];//参考距离,一样的道理
}
return fa[x];
}
void solve()
{
int n,m,x,y,v,tim=0,ok,cnt,k,tx,ty,ans,jud;
char op;
string s;
while(cin>>n>>m&&n&&m)
{
for(int i=0;i<=n;i++)
{
dis[i]=0,fa[i]=i;
}
cout<<"Case "<<++tim<<":"<<endl;
ok=0,cnt=0;
while(m--)
{
cin>>op;
if(op=='I')
{
cnt++;
getline(cin,s);
k=s.size();
for(int i=0;i<k;i++)
tos[i]=s[i];
tos[k]='\0';//小巧思,由于读入两个值和三个值是一样op,所以我们需要sscanf根据返回值确定操作
if(sscanf(tos, "%lld%lld%lld", &x,&y,&v) == 2)
{
v=y,y=n;//若为两个,我们添加虚点n,让n为真实值头部
}
if(ok)
continue;
tx=find(x),ty=find(y);
if(tx==n)
swap(tx,ty);//n需要永远为头,因为别的都是不确定的,和n这个真实值存在差异
if(tx==ty)
{
if((dis[x]^dis[y])!=v)//前后矛盾
{
cout<<"The first "<<cnt<<" facts are conflicting."<<endl;
ok=1;
}
}
else
{
fa[tx]=ty;
dis[tx]=(dis[x]^dis[y]^v);//添加,确定父节点距离
}
}
else
{
cin>>k;
ans=0;
ty=find(n);
for(int i=1;i<=k;i++)
{
cin>>x;
tx=find(x);
if (tx!=n)num[tx]++;//是不存在的值那么需要判断出现的奇偶,因为不确定的值都是由父节点异或出,
// 只有偶数次才能刚好消掉不确定的父节点,奇数次会存在不确定父节点,导致答案错误
ans^=dis[x];
}
if(ok)
continue;
jud=0;
for(int i=0;i<=n;i++)
{
if(num[i]%2==1)
jud=1;
num[i]=0;
}
if(jud)
cout<<"I don't know.";//奇数次不确定,I don't know.
else
cout<<ans;
cout<<endl;
}
}
cout<<endl;
}
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);
int T=1;
if(tin)
cin>>T;
while(T--)
{
solve();
}
return 0;
}
ok,以上就是带权并查集的联系,师承左程云,以上题库是在B站up左程云视频跟练
修正代码部分采用deep seek v4pro
第六题代码我是在高峰期让我纠错,用了大概0.7元,相比之前确实贵了好多,但是效果确实可以,而且相比其他ai,也算便宜了(除去高峰期)