并查集

本文深入探讨并查集算法的应用,包括边带权、扩展域等高级技巧,解析银河英雄传说、奇偶游戏等经典题目,涵盖食物链、罪犯分配及超市问题的解决方案,详解CF1290C Prefix Enlightenment的解题思路。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

练习题

238. 银河英雄传说 (边带权)

链接:https://www.acwing.com/problem/content/240/

题意:完成两个指令:M i j 表示将第 i 个人所在的队列合并到第 j 个人所在队列的末尾
C i j 表示查询第 i 个人和第 j 个人是否在同一个队列中,并询问他们之间差了多少人

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=3e4+10;

int n=30000,m;
int p[maxn],sz[maxn],d[maxn];
char op[10];

int find(int x)
{
	if(p[x]==x) return x;
	int rt=find(p[x]);
	d[x]+=d[p[x]];
	return p[x]=rt;
}

int main()
{
	for(int i=1;i<=n;++i) p[i]=i,sz[i]=1;
	scanf("%d",&m);
	while(m--)
	{
		int x,y;
		scanf("%s%d%d",&op,&x,&y);
		if(op[0]=='M')
		{
			x=find(x),y=find(y);
			d[x]=sz[y];
			sz[y]+=sz[x];
			p[x]=y;
		}
		else
		{
			int fx=find(x),fy=find(y);
			if(fx!=fy) puts("-1");
			else printf("%d\n",max(0,abs(d[x]-d[y])-1));
		}
	}
	return 0;
}

239. 奇偶游戏 (边带权)

链接:https://www.acwing.com/problem/content/description/241/

思路

  • 奇偶性的传递关系:设 0 表示奇偶性相同, 1 表示奇偶性不同。假设,a 和 b 的奇偶性为 f 1 f_1 f1,b 和 c 的奇偶性为 f 2 f_2 f2。那么 a 和 c 的奇偶性为 f 3 = f 1 f_3=f_1 f3=f1^ f 2 f_2 f2
  • 每次询问 [ l , r ] [l,r] [l,r] 时,如果 l − 1 l-1 l1 r r r 在同一个队中,那么就判断这两个的奇偶性是不是相同。d[x]^d[y]表示通过 x、y 和队长的关系得到, x 和 y 的奇偶性的关系,此时判断 d[x]^d[y]是否等于 f f f 即可
  • 如果 l − 1 l-1 l1 r r r 不在同一个队中,那么就把 它们归在同一队列中。

在这里插入图片描述
我们需要处理的是 d[fx]的值,假设已知了:d[x]、d[y]、d[fx]。那么d[x]^d[fx]表示的是x和fy的奇偶性,d[x] ^ d[fx] ^ d[y]表示的是 x 和 y 的奇偶性。且我们一直它们的奇偶性为 f 。
因此 d[x] ^ d[fx] ^ d[y] = f 。即 d[fx] =f ^ d[x] ^ d[y]

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=2e4+10;

int n,m,p[maxn],d[maxn]; 

struct Node
{
	int l,r,f;
}opt[maxn];
vector<int> allx;

int find(int x)
{
	if(p[x]==x) return x;
	int root=find(p[x]);
	d[x]^=d[p[x]];
	return p[x]=root;
}

int main()
{
	scanf("%d",&n);
	scanf("%d",&m);
	for(int i=1;i<=m;++i)
	{
		int l,r,f;
		char type[10];
		scanf("%d%d%s",&l,&r,&type);	
		if(type[0]=='e') f=0;
		else f=1;
		l--;
		allx.push_back(l);
		allx.push_back(r);
		opt[i]={l,r,f}; 
	}	
	sort(allx.begin(),allx.end());
	allx.resize(unique(allx.begin(),allx.end())-allx.begin());
	n=allx.size();
	for(int i=1;i<=n;++i) p[i]=i;
	for(int i=1;i<=m;++i)
	{
		int l=lower_bound(allx.begin(),allx.end(),opt[i].l)-allx.begin()+1;
		int r=lower_bound(allx.begin(),allx.end(),opt[i].r)-allx.begin()+1;
		int fx=find(l),fy=find(r);
		if(fx==fy)
		{
			if(d[l]^d[r]!=opt[i].f)
			{
				printf("%d\n",i-1);
				return 0;
			}
		}
		else
		{
			d[fx]=d[l]^d[r]^opt[i].f;
			p[fx]=fy;
		}
	}
	printf("%d\n",m);
	return 0;
}

方法二:使用扩展域

  • 两个命题:前缀和奇数或者偶数。
  • 需要注意 点 是否被重复使用
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=4e4+10;

int n,m,p[maxn]; 

struct Node
{
	int l,r,f;
}opt[maxn];
vector<int> allx;

int find(int x)
{
	return p[x]==x?x:p[x]=find(p[x]);
}

bool check(int x,int y)
{
	return find(x)==find(y);
}

void merge(int x,int y)
{
	int fx=find(x),fy=find(y);
	p[fx]=fy;
}

int main()
{
	scanf("%d",&n);
	scanf("%d",&m);
	for(int i=1;i<=m;++i)
	{
		int l,r,f;
		char type[10];
		scanf("%d%d%s",&l,&r,&type);	
		if(type[0]=='e') f=0;
		else f=1;
		l--;
		allx.push_back(l);
		allx.push_back(r);
		opt[i]={l,r,f}; 
	}	
	sort(allx.begin(),allx.end());
	allx.resize(unique(allx.begin(),allx.end())-allx.begin());
	n=allx.size();
	for(int i=1;i<=2*n;++i) p[i]=i;
	for(int i=1;i<=m;++i)
	{
		int u=lower_bound(allx.begin(),allx.end(),opt[i].l)-allx.begin()+1;
		int v=lower_bound(allx.begin(),allx.end(),opt[i].r)-allx.begin()+1;
		if(opt[i].f==1)
		{
			if(check(u,v)||check(u+n,v+n))
			{
				printf("%d\n",i-1);
				return 0;
			}
			merge(u,v+n);
			merge(v,u+n);
		}
		else
		{
			if(check(u,v+n)||check(v,u+n))
			{
				printf("%d\n",i-1);
				return 0;				
			}
			merge(u,v);
			merge(u+n,v+n);
		}
	}
	printf("%d\n",m);
	return 0;
}

240. 食物链

链接:https://www.acwing.com/problem/content/242/

题意:给出 m 句话,判断与前面的矛盾的话的个数。
思路:使用扩展域

  • 如果 i 和 j 同类,那么就将 ( i 1 , j 1 ) ( i 2 , j 2 ) ( i 3 , j 3 ) (i_1,j_1)(i_2,j_2)(i_3,j_3) (i1,j1)(i2,j2)(i3,j3)设在同一个队列中,表示其中任意一个命题成立,其他命题也同时成立。检查类型 1 是否为真时,需要要判断 i 1 i_1 i1 j 2 , j 3 j_2,j_3 j2,j3是否在同一个队列中即可。
  • 如果 i 和 j 不同类,那么就将 ( i 1 , j 2 ) ( i 2 , j 3 ) ( i 3 , j 1 ) (i_1,j_2)(i_2,j_3)(i_3,j_1) (i1,j2)(i2,j3)(i3,j1)设在同一个队列中。检查类型 2 是否为真时,需要要判断 i 1 i_1 i1 j 1 , j 3 j_1,j_3 j1,j3是否在同一个队列中即可。
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=15e4+10;

int n,m;

int p[maxn];

int find(int x)
{
	return x==p[x]?x:p[x]=find(p[x]);
}

bool check(int x,int y)
{
	int fx=find(x),fy=find(y);
	return fx==fy;
}

void merge(int x,int y)
{
	int fx=find(x),fy=find(y);
	p[fx]=fy;
}

int main()
{
	int ans=0;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=3*n;++i) p[i]=i;
	for(int i=1;i<=m;++i)
	{
		int op,u,v;
		scanf("%d%d%d",&op,&u,&v);
		if(u>n||v>n)
		{
			ans++;
			continue;
		}
		
		if(op==1)
		{
			if(check(u,v+n)||check(u,v+2*n))
			{
				ans++;
				continue;
			}
			merge(u,v);
			merge(u+n,v+n);
			merge(u+2*n,v+2*n);
		}
		else if(op==2)
		{
			if(u==v||check(u,v)||check(u,v+2*n))
			{
				ans++;
				continue;
			}
			merge(u,v+n);
			merge(u+n,v+2*n);
			merge(u+2*n,v); 
		}
	}
	printf("%d\n",ans);
	return 0;
}

257. 关押罪犯 (二分 || 扩展域)

题意:将 n 个罪犯关押在两座监狱中,某两个人关在一起会产生一些仇恨值,问怎样分配才能使得最大的仇恨值最小。求这个最大的仇恨值

思路:

  • 使用扩展域:两个命题,i 关在 a 监狱,或者 i 关在 b 监狱成立。从最大仇恨值开始贪心,将 i 和 j 分开。先判断 i 和 j 是否有冲突,即 i 和 j 或者 i + n 和 j + n 是否已经在一队中。如果已经在一堆中,那么发生冲突,此时的 c 就是答案。
  • 使用二分:二分答案,此时检查大于 ans 的关系中能不能分在两个监狱,也就是能不能变成一个二分图。
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=2e4+10,maxm=1e5+10;

int n,m;
int p[maxn<<1];

struct Node
{
	int a,b,c;
	bool operator<(const Node & B) const
	{
		return c>B.c;
	}
}pp[maxm];

int find(int x)
{
	return p[x]==x?x:p[x]=find(p[x]);
}

bool check(int x,int y)
{
	return find(x)==find(y);
}

void merge(int x,int y)
{
	int fx=find(x),fy=find(y);
	p[fx]=fy;
}

int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=2*n;++i) p[i]=i;
	for(int i=1;i<=m;++i)
	{
		int a,b,c;
		scanf("%d%d%d",&a,&b,&c);
		pp[i]={a,b,c};
	}
	sort(pp+1,pp+1+m);
	for(int i=1;i<=m;++i)
	{
		int a=pp[i].a,b=pp[i].b,c=pp[i].c;
		if(check(a,b)||check(a+n,b+n))
		{
			printf("%d\n",c);
			return 0;	
		}	
		merge(a,b+n);
		merge(b,a+n);
	}
	printf("0\n");
	return 0;
}

Supermarket POJ - 1456 (并查集维护最后一个 0 的位置)

链接:http://poj.org/problem?id=1456

题意:给定 n 个物品,每个物品有价格 v 和截止日期 d 。每天只能买一样物品,问最多能卖多少钱?

思路:贪心来做,将物品从大到小排序,然后每个物品从截止日期开始往前找第一个没卖过物品的空位。

  • 贪心的正确性:首先当前的物品的价格是最优的,如果有空位的是一定要取的。那么肯定是位置越靠近截止日期越好,因为可能会有价格小且截止日期靠前的物品,这样这些物品也有机会选到
  • 这里使用并查集维护 d q前面的第一个空位,每次占用一个位置时,更新父节点:pa[fx]=fx-1
#include <cstdio>
#include <algorithm>
#define ll long long
using namespace std;
const int maxn=1e4+10;

struct Node
{
    int v,d;
    bool operator<(const Node & b) const
    {
        return v>b.v;
    }
} p[maxn];
int pa[maxn],n;
int find(int x)
{
    return pa[x]==x?x:pa[x]=find(pa[x]);
}
int main()
{
    while(~scanf("%d",&n))
    {
        int m=0;
        for(int i=1; i<=n; ++i)
            scanf("%d%d",&p[i].v,&p[i].d),m=max(m,p[i].d);
        sort(p+1,p+1+n);
        for(int i=0; i<=m; ++i) pa[i]=i;
        int ans=0;
        for(int i=1; i<=n; ++i)
        {
            int fx=find(p[i].d);
            if(fx>=1)
            {
                ans+=p[i].v;
                pa[fx]=fx-1;
            }
        }
        printf("%d\n",ans);
    }
    return 0;
}

258. 石头剪子布 (枚举 + 并查集)

链接:https://www.acwing.com/problem/content/description/260/

题意:有 n 个人玩石头剪刀布,只有裁判可以任意出,其他人每次只能出固定的一个,告诉你 m 组游戏的输赢结果,让你判断,如果只有一个人是裁判,则输出裁判编号和确定轮数。如果必须有多个人是裁判则输出“Impossible”,如果裁判的人选多于 1 人,则输出“Can not determine”。

思路:根据数据范围可以枚举每个人为裁判,如果除裁判以外的场次都合法,则当前枚举的人可能是裁判

  • 如果合法次数为 0 ,说明只 ban 掉一个人的场次依然发生冲突,说明必须存在多个裁判,输出“Impossible”
  • 如果合法次数大于 1 ,说明有多个人可能是裁判。输出“Can not determine”
  • 如果合法次数为 1 ,说明裁判唯一。那么确定几轮可以这样想:一共枚举了 n 次,有 n - 1 次是不合法的。把这 n - 1 一个人都排除掉的最大次数,就可以确定最后一个人是裁判

代码:先检查并查集再加入

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=2000+10;

int n,m,last;
int q[maxn][3],p[maxn];
int find(int x)
{
	return p[x]==x?x:p[x]=find(p[x]);
}
bool check(int u,int v)
{
	return find(u)==find(v);
}
void merge(int u,int v)
{
	int fx=find(u),fy=find(v);
	p[fx]=fy;
}
bool solve(int x)
{
	for(int i=1;i<=3*n;++i) p[i]=i;
	for(int i=1;i<=m;++i)
	{
		int u=q[i][0],v=q[i][2],op=q[i][1];
		u++,v++;
		if(u==x||v==x) continue;
		if(op=='=')
		{
			if(check(u,v+n)||check(u,v+2*n))
			{
				last=max(last,i);
				return 0;
			}
			merge(u,v);
			merge(u+n,v+n);
			merge(u+2*n,v+2*n);
		}
		else if(op=='>')
		{
			if(check(u,v)||check(u,v+2*n))
			{
				last=max(last,i);
				return 0;
			}
			merge(u,v+n);
			merge(u+n,v+2*n);
			merge(u+2*n,v);			
		}
		else if(op=='<')
		{
			if(check(v,u)||check(v,u+2*n))
			{
				last=max(last,i);
				return 0;
			}
			merge(v,u+n);
			merge(v+n,u+2*n);
			merge(v+2*n,u);			
		}
	}
	return 1;
}
int main()
{
	while(~scanf("%d%d",&n,&m))
	{
		for(int i=1;i<=m;++i)
			scanf("%d%c%d",&q[i][0],&q[i][1],&q[i][2]);
		int cnt=0,ans;
		last=0;
		for(int i=1;i<=n;++i)
		{
			if(solve(i))
			{
				cnt++;
				ans=i-1;
			}
		}
		if(cnt==0) puts("Impossible");
		else if(cnt>=2) puts("Can not determine");
		else if(cnt==1) printf("Player %d can be determined to be the judge after %d lines\n",ans,last);	
	}
	return 0;
}

代码:先加入并查集在检查

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=2000+10;

int n,m,last;
int q[maxn][3],p[maxn];

int find(int x)
{
	return p[x]==x?x:p[x]=find(p[x]);
}
bool check(int u)
{
	int f1=find(u),f2=find(u+n),f3=find(u+2*n);
	if(f1==f2||f1==f3||f2==f3) return 1;
	return 0;
}
void merge(int u,int v)
{
	int fx=find(u),fy=find(v);
	p[fx]=fy;
}


bool solve(int x)
{
	for(int i=1;i<=3*n;++i) p[i]=i;
	for(int i=1;i<=m;++i)
	{
		int u=q[i][0],v=q[i][2],op=q[i][1];
		u++,v++;
		if(u==x||v==x) continue;
		if(op=='=')
		{
			merge(u,v);
			merge(u+n,v+n);
			merge(u+2*n,v+2*n);
		}
		else
		{
		    if(op=='<') swap(u,v);
			merge(u,v+n);
			merge(u+n,v+2*n);
			merge(u+2*n,v);			
		}
		if(check(u)||check(v))
		{
		    last=max(last,i);
		    return 0;
		}
	}
	return 1;
}
int main()
{
	while(~scanf("%d%d",&n,&m))
	{
		for(int i=1;i<=m;++i)
			scanf("%d%c%d",&q[i][0],&q[i][1],&q[i][2]);
		int cnt=0,ans;
		last=0;
		for(int i=1;i<=n;++i)
		{
			if(solve(i))
			{
				cnt++;
				ans=i-1;
			}
		}
		if(cnt==0) puts("Impossible");
		else if(cnt>=2) puts("Can not determine");
		else if(cnt==1) printf("Player %d can be determined to be the judge after %d lines\n",ans,last);	
	}
	return 0;
}

CF1290C Prefix Enlightenment

链接:https://www.luogu.com.cn/problem/CF1290C

题意:给定一个长度 n 的 01 串和 m 个操作,每个操作可以使得 01 串的某些位置翻转。对于每一个 i ,求使得 01 串 1 ~ i 每一个位置都变为 1 的最小操作次数。且任意三个操作的交集为空

思路:任意三个操作的交集为空,说明了 01串的同一个位置,最多被两个操作所控制,那么就可以用扩展域来写。

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=3e5+10,inf=1e6;

int n,m,c;
char s[maxn];
vector<int> vec[maxn];
int p[maxn<<1],visit[maxn];
ll sz[maxn<<1];

int find(int x)
{
    return p[x]==x?x:p[x]=find(p[x]);
}
bool check(int x,int y)
{
    return find(x)==find(y);
}
void merge(int x,int y)
{
    int fx=find(x),fy=find(y);
    if(fx!=fy)
    {
        p[fx]=fy;
        sz[fy]+=sz[fx];
    }
}

int main()
{
    scanf("%d%d",&n,&m);
    scanf("%s",s+1);
    for(int i=1; i<=m; ++i)
    {
        scanf("%d",&c);
        for(int j=1; j<=c; ++j)
        {
            int x;
            scanf("%d",&x);
            vec[x].push_back(i);
        }
    }
    for(int i=1; i<=2*m; ++i)
    {
        p[i]=i;
        if(i<=m) sz[i]=1;
    }

    int ans=0;
    for(int i=1; i<=n; ++i)
    {
        if(vec[i].size()==1)
        {
            int x=vec[i][0];
            if(visit[x]) ans-=min(sz[find(x)],sz[find(x+m)]);

            if(s[i]=='0') sz[find(x+m)]=inf;
            else sz[find(x)]=inf;
            ans+=min(sz[find(x)],sz[find(x+m)]);
            visit[x]=1;
        }
        else if(vec[i].size()==2)
        {
            int x=vec[i][0],y=vec[i][1];
            if(check(x,y)||check(x,y+m))
            {
                printf("%d\n",ans);
                continue;
            }

            if(visit[x]) ans-=min(sz[find(x)],sz[find(x+m)]);
            if(visit[y]) ans-=min(sz[find(y)],sz[find(y+m)]);

            if(s[i]=='0')
            {
                merge(x,y+m);
                merge(x+m,y);
            }
            else
            {
                merge(x,y);
                merge(x+m,y+m);
            }
            ans+=min(sz[find(x)],sz[find(x+m)]);
            visit[x]=visit[y]=1;
        }
        printf("%d\n",ans);
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值