并查集进阶 P1(带权并查集,并查集分类)

简介

在并查集的边上定义某种权值和这种权值在路径压缩时产生的运算,从而解决更多的问题。

为了维护并查集中的边权,需要将边权下放到子节点中存储。因此,每个节点存储的都是它到它的父节点之间的边权。只有当一个节点的父节点发生变化时,才需要相应地调整边权。一般情形中,这可能发生在路径压缩和合并两个节点时。例如,如果边权是当前节点与父节点之间的距离,那么,在路径压缩时,每次将当前节点的父节点替换为根节点,都需要将父节点到根节点的距离加到当前节点存储的边权上;类似地,在合并两个节点所在集合时,需要计算两个根节点之间新连接的边的权值。

贴几道题做练习。

1.模板带权并查集入门 蓝桥杯 2022 省 A 推导部分和

P8779 蓝桥杯 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 狡猾的商人

P2294 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 有多少答案是错的

问题 - 3038

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 银河英雄传说

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.除法求值

LCR 111. 除法求值 - 力扣(LeetCode)

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 食物链

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 提高组 关押罪犯

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,也算便宜了(除去高峰期)

相关推荐
牧羊人.3331 小时前
动手学深度学习 02 | 手写数字识别
图像处理·人工智能·深度学习·算法
武帝为此1 小时前
【InnoDB存储引擎介绍】
数据库·算法
小小龙学IT2 小时前
ONNX Runtime 开源 AI 推理引擎深度解析:从模型部署到边缘 AI 加速的全栈实战
c++·人工智能·开源
shehuiyuelaiyuehao2 小时前
算法32,连续数组,前缀和+哈希表
算法·leetcode·职场和发展
五_谷_丰_登2 小时前
平衡二叉树(AVL)讲解二
数据结构·c++
程序员贺加贝2 小时前
报表大 IN 优化:一条 product_profile 超大 IN SQL 背后的报表任务治理
算法·性能优化
知无不研2 小时前
std::function在使用时遇到的问题
c++·算法·st·function
薛定e的猫咪3 小时前
(IEEE Transactions 2025)自适应元强化学习动态柔性作业车间调度框架
网络·人工智能·算法
码场老菜鸟3 小时前
C++ 指针的深度理解与应用
java·c++·算法