并查集
练习题
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 l−1 和 r r r 在同一个队中,那么就判断这两个的奇偶性是不是相同。d[x]^d[y]表示通过 x、y 和队长的关系得到, x 和 y 的奇偶性的关系,此时判断 d[x]^d[y]是否等于 f f f 即可
- 如果 l − 1 l-1 l−1 和 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;
}