专题 | 并查集&扩展

目录

  • 并查集

  • 扩展域并查集

  • 可持久化并查集

并查集

输入由两部分组成。

第一部分以N,M开始。N为问题涉及的人的个数(1≤N≤20000)。这些人的编号为1,2,3,…, N。下面有M行(1≤M≤1000000),每行有两个数ai,bi,表示已知ai和bi是亲戚。

第二部分以Q开始。以下Q行有Q个询问(1≤ Q ≤1000000),每行为ci,di,表示询问ci和di是否为亲戚。

核心代码

路径压缩

int find(x){
	if(fa[x]==x)return x;
	return fa[x]=find(fa[x]);
}

合并

void merge(int a,int b){
    a=find(a),b=find(b);
    fa[a]=b;
}

按秩合并

void merge(int a,int b){
	a=find(a),b=find(b);
	if(x==y)return ;
	if(sz[a]>sz[b])swap(a,b);
	fa[a]=b;
	sz[b]+=sz[a];//sz[]初始化为1
}

并查集维护并查集信息

维护并查集大小

不要忘记怎么写!只需要merge时修改即可。

int find(int a){
    if(fa[a]==a)return a;
    return fa[a]=find(fa[a]);
}

void merge(int faa,int fbb){
    if(faa!=fbb)fa[faa]=fbb,sz[fbb]+=sz[faa];
}

//merge(find(e[cur].a),find(e[cur].b));

www.luogu.com.cn

维护并查集大小,并且以此更新该点到链头的距离

int find(int x){
    if(fa[x]==x)return x;
    int f=fa[x];
    fa[x]=find(fa[x]);
    dis[x]+=dis[f];
    sz[x]+=sz[fa[x]];
    return fa[x];
}

void merge(int x,int y){
    int fx=find(x),fy=find(y);
    fa[fx]=fy;
    dis[fx]+=sz[fy];
    sz[fx]+=sz[fy];//更新集合大小
    sz[fy]=sz[fx];//记录当前点实在集合的大小
    
}

并查集求最小环

https://www.luogu.com.cn/problem/P2661


/*
CB Ntsc
*/

#include <bits/stdc++.h>
using namespace std;
#define int long long

#define rd read()
inline int read() {
    int xx = 0, ff = 1;
    char ch = getchar();
    while (ch < '0' || ch > '9') {
        if (ch == '-')
            ff = -1;
        ch = getchar();
    }
    while (ch >= '0' && ch <= '9') xx = xx * 10 + (ch - '0'), ch = getchar();
    return xx * ff;
}
inline void write(int out) {
    if (out < 0)
        putchar('-'), out = -out;
    if (out > 9)
        write(out / 10);
    putchar(out % 10 + '0');
}

//

const int N = 1e6 + 5;
const int INF = 1e9 + 5;
const int MOD = 998244353;

bool f1;
int t[N], fa[N], d[N], ans, n;
bool f2;
int find(int x){
	if(fa[x]!=x){
		int l=fa[x];
		fa[x]=find(fa[x]);
		d[x]+=d[l];
	}return fa[x];
}

signed main() {
   	n=rd;
   	for(int i=1;i<=n;i++){
   		fa[i]=i;t[i]=rd;
	}
	ans=INF;
	for(int i=1;i<=n;i++){
		int a=find(i),b=find(t[i]);
		if(a!=b){
			fa[a]=b;d[i]=d[t[i]]+1;
		}else{
			ans=min(ans,d[i]+d[t[i]]+1);
		}
	}
	cout<<ans;
	return 0;
}

扩展域并查集

[NOI2001] 食物链

动物王国中有三类动物 A , B , C A,B,C A,B,C,这三类动物的食物链构成了有趣的环形。 A A A B B B B B B C C C C C C A A A

现有 N N N 个动物,以 1 ∼ N 1 \sim N 1N 编号。每个动物都是 A , B , C A,B,C A,B,C 中的一种,但是我们并不知道它到底是哪一种。

有人用两种说法对这 N N N 个动物所构成的食物链关系进行描述:

  • 第一种说法是 1 X Y,表示 X X X Y Y Y 是同类。

  • 第二种说法是2 X Y,表示 X X X Y Y Y

此人对 N N N 个动物,用上述两种说法,一句接一句地说出 K K K 句话,这 K K K 句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。

  • 当前的话与前面的某些真的话冲突,就是假话;

  • 当前的话中 X X X Y Y Y N N N 大,就是假话;

  • 当前的话表示 X X X X X X,就是假话。

你的任务是根据给定的 N N N K K K 句话,输出假话的总数。

对于全部数据, 1 ≤ N ≤ 5 × 1 0 4 1\le N\le 5 \times 10^4 1N5×104 1 ≤ K ≤ 1 0 5 1\le K \le 10^5 1K105

知识

并查集,扩展域并查集

代码

#include<bits/stdc++.h>
using namespace std;
const int N=5e4+5;
int fa[N*3],n,k,ans;
int find(int x){
	return (fa[x]!=x)?fa[x]=find(fa[x]):x;
}
void uni(int a,int b){
	fa[find(a)]=find(b);
}
int main(){
	scanf("%d%d",&n,&k);
	for(int i=1;i<=n*3;i++)fa[i]=i;
	while(k--){
		int a,b,op;
		scanf("%d%d%d",&op,&a,&b);
		if(a>n||b>n){
			ans++;continue;
		}
		if(op==1){
			if(find(a+n)==find(b)||find(a)==find(n+b))ans++;
			else uni(a,b),uni(a+n,b+n),uni(a+n+n,b+n+n);
			
		}else {
			if(find(a)==find(b)||find(a)==find(b+n))ans++;
			else uni(a+n,b),uni(a+n+n,b+n),uni(a,b+n+n);
			
		}
	}
	printf("%d\n",ans);
}

//GOOD LUCK ,NTSC/

[CEOI1999] Parity Game

Alice 和 Bob 在玩一个游戏:他写一个由 0 0 0 1 1 1 组成的序列。Alice 选其中的一段(比如第 3 3 3 位到第 5 5 5 位),问他这段里面有奇数个 1 1 1 还是偶数个 1 1 1。Bob 回答你的问题,然后 Alice 继续问。Alice 要检查 Bob 的答案,指出在 Bob 的第几个回答一定有问题。有问题的意思就是存在一个 01 01 01 序列满足这个回答前的所有回答,而且不存在序列满足这个回答前的所有回答及这个回答。

1 1 1 行一个整数 n n n,是这个 01 01 01 序列的长度。

2 2 2 行一个整数 m m m,是问题和答案的个数。

3 3 3 行开始是问题和答案,每行先有两个整数,表示你询问的段的开始位置和结束位置。然后是 Bob 的回答。odd表示有奇数个 1 1 1even 表示有偶数个 1 1 1

输出一行,一个数 x x x,表示存在一个 01 01 01 序列满足第 1 1 1 到第 x x x 个回答,但是不存在序列满足第 1 1 1 到第 x + 1 x+1 x+1 个回答。如果所有回答都没问题,你就输出所有回答的个数。

对于 100 % 100\% 100% 的数据, 1 ≤ n ≤ 1 0 9 1 \le n \leq 10^9 1n109 m ≤ 5 × 1 0 3 m \leq 5 \times 10^3 m5×103


这里我们考虑前缀和性质。要明确控制欲并查集可以维护什么,才能想到做法。

转化为并查集,即若[l,r]和为奇数,那么q_r和q_{l-1}(q_i表示[1,i]前缀和)的奇偶性相同,反之不相同。于是我们维护前缀和的奇偶性关系即可。

开两倍空间为[1,n][n+1,2n],合并a,b表示q_a,q_{b-1}奇偶性相同,合并a,b+n表示不同。



#include <bits/stdc++.h>
#include <queue>
#include <iostream>
#define rep(l, r, i) for (int i = l, END##i = r; i <= END##i; ++i)
#define per(r, l, i) for (int i = r, END##i = l; i >= END##i; --i)
using namespace std;
// #define pb push_back
#define mp make_pair
#define int long long
#define pii pair<int, int>
#define ps second
#define pf first

#define X(j) i[j]
#define Y(j) (dp[j] + (i[j] + L) * (i[j] + L))

#define rd read()
int read() {
  int xx = 0, ff = 1;
  char ch = getchar();
  while (ch < '0' || ch > '9') {
    if (ch == '-')
      ff = -1;
    ch = getchar();
  }
  while (ch >= '0' && ch <= '9')
    xx = xx * 10 + (ch - '0'), ch = getchar();
  return xx * ff;
}
void write(int out) {
  if (out < 0)
    putchar('-'), out = -out;
  if (out > 9)
    write(out / 10);
  putchar(out % 10 + '0');
}

const int N =2e5 + 5;
const int INF = 1e18;
const int MOD = 998244353;

int m,n,a[N],b[N],v[N];
int fa[N<<1];

int find(int x){
  if(x==fa[x])return x;
  return fa[x]=find(fa[x]);
}

void merge(int a,int b){
  int faa=find(a),fbb=find(b);
  if(faa!=fbb) fa[faa]=fbb;
}




int e[N];
int cnt;
signed main(){
  n=rd,m=rd;
  for(int i=1;i<=m;i++){
    a[i]=rd-1,b[i]=rd;
    // cerr<<a[i]<<' '<<b[i]<<endl;
    e[++cnt]=a[i];
    e[++cnt]=b[i];
    string s;
    cin>>s;
    if(s=="odd")v[i]=1;
    // cerr<<v[i]<<endl;

  }
  sort(e+1,e+cnt+1);
  int l=unique(e+1,e+cnt+1)-e-1;
  // for(int i=1;i<=l*2;i++)cerr<<e[i]<<' ';
  // cerr<<endl;
  for(int i=1;i<=l*2;i++)fa[i]=i;//两倍初始化!
  for(int i=1;i<=m;i++){
    // cerr<<i<<' '<<a[i]<<' '<<b[i]<<' '<<endl;
    a[i]=lower_bound(e+1,e+l+1,a[i])-e;
    b[i]=lower_bound(e+1,e+l+1,b[i])-e;
    // cerr<<i<<' '<<a[i]<<' '<<b[i]<<' '<<endl;
    if(v[i]){
      if(find(a[i])==find(b[i])){
        cout<<i-1<<endl;
        return 0;
      }else{
        merge(a[i],b[i]+l);
        merge(a[i]+l,b[i]);
      }
    }else{
        if(find(a[i])==find(b[i]+l)){
          cout<<i-1<<endl;
          return 0;
        }else{
          merge(a[i],b[i]);
          merge(a[i]+l,b[i]+l);
      }
    }
  }


  
  cout<<m<<endl;

}

扩展域并查集判定二分图

并查集判断二分图(tourist做法) - yHan234 - 博客园

注意这里的二分图没有规定那些点必须在哪边,因此当且仅当图中出现奇环时二分图不成立,或者说是不能构成二分图。

用扩展域可以判定二分图。我们开两倍空间的并查集,左边为L(1\sim n),右边为R(n+1\sim 2n)。我们把每个点分裂为两个点为u_1,u_2,编号为u,n+u。在我们连接u,v时,就连接u_1-v_2和u_2-v_1。最后如果某一时刻存在p,p_n在同一个集合内,二分图就不成立了。

这里的意思就是u_1默认u在左部,u_2默认u在右部。连接u_1,-v_2就表示假设u在左部且v在右部,另一种情况亦然。如果存在某个p满足p_1和p_2间接或直接相连,那么就说明无论假设p在左部还是右部,为了尽可能让与p相连的点满足二分图性质,则会最终得到一个点x相连的p应该在假设的p的另外一部。即p无论放在哪一部都会得到相反的结论。二分图无法成立。

[NOIP2010 提高组] 关押罪犯

S 城现有两座监狱,一共关押着 N N N 名罪犯,编号分别为 1 ∼ N 1\sim N 1N。他们之间的关系自然也极不和谐。很多罪犯之间甚至积怨已久,如果客观条件具备则随时可能爆发冲突。我们用“怨气值”(一个正整数值)来表示某两名罪犯之间的仇恨程度,怨气值越大,则这两名罪犯之间的积怨越多。如果两名怨气值为 c c c 的罪犯被关押在同一监狱,他们俩之间会发生摩擦,并造成影响力为 c c c 的冲突事件。

每年年末,警察局会将本年内监狱中的所有冲突事件按影响力从大到小排成一个列表,然后上报到 S 城 Z 市长那里。公务繁忙的 Z 市长只会去看列表中的第一个事件的影响力,如果影响很坏,他就会考虑撤换警察局长。

在详细考察了 N N N 名罪犯间的矛盾关系后,警察局长觉得压力巨大。他准备将罪犯们在两座监狱内重新分配,以求产生的冲突事件影响力都较小,从而保住自己的乌纱帽。假设只要处于同一监狱内的某两个罪犯间有仇恨,那么他们一定会在每年的某个时候发生摩擦。

那么,应如何分配罪犯,才能使 Z 市长看到的那个冲突事件的影响力最小?这个最小值是多少?

输入格式

每行中两个数之间用一个空格隔开。第一行为两个正整数 N , M N,M N,M,分别表示罪犯的数目以及存在仇恨的罪犯对数。接下来的 M M M 行每行为三个正整数 a j , b j , c j a_j,b_j,c_j aj,bj,cj,表示 a j a_j aj 号和 b j b_j bj 号罪犯之间存在仇恨,其怨气值为 c j c_j cj。数据保证 1 < a j ≤ b j ≤ N , 0 < c j ≤ 1 0 9 1<a_j\leq b_j\leq N, 0 < c_j\leq 10^9 1<ajbjN,0<cj109,且每对罪犯组合只出现一次。

输出格式

共一行,为 Z 市长看到的那个冲突事件的影响力。如果本年内监狱中未发生任何冲突事件,请输出 0

对于 30 % 30\% 30% 的数据有 N ≤ 15 N\leq 15 N15

对于 70 % 70\% 70% 的数据有 N ≤ 2000 , M ≤ 50000 N\leq 2000,M\leq 50000 N2000,M50000

对于 100 % 100\% 100% 的数据有 N ≤ 20000 , M ≤ 100000 N\leq 20000,M\leq 100000 N20000,M100000

可持久化并查集

给定 n n n 个集合,第 i i i 个集合内初始状态下只有一个数,为 i i i

m m m 次操作。操作分为 3 3 3 种:

  • 1 a b 合并 a , b a,b a,b 所在集合;

  • 2 k 回到第 k k k 次操作(执行三种操作中的任意一种都记为一次操作)之后的状态;

  • 3 a b 询问 a , b a,b a,b 是否属于同一集合,如果是则输出 1 1 1,否则输出 0 0 0

对于 100 % 100\% 100% 的数据, 1 ≤ n ≤ 1 0 5 1\le n\le 10^5 1n105 1 ≤ m ≤ 2 × 1 0 5 1\le m\le 2\times 10^5 1m2×105 1 ≤ a , b ≤ n 1 \le a, b \le n 1a,bn

代码分析

迟迟u和

struct node{
	int lc,rc,v,rnk;
}tr[M];

int getrnk(int x,int l,int r,int p){//单点查询秩
    if(l==r)return tr[x].rnk;
    int mid=l+r>>1;
    if(p<=mid)return getrnk(tr[x].lc,l,mid,p);
    return getrnk(tr[x].rc,mid+1,r,p);
}

void changernk(int pre,int &now,int l,int r,int p,int v){//修改秩(树的高度)
    now=++idx;
    tr[now]=tr[pre];
    if(l==r){
        tr[now].rnk=max(tr[now].rnk,v);//要比对取最大值
        return;
    }
    int mid=l+r>>1;
    if(p<=mid)changernk(tr[pre].lc,tr[now].lc,l,mid,p,v);
    else changernk(tr[pre].rc,tr[now].rc,mid+1,r,p,v);
    return ;
}

查询


int query(int x,int l,int r,int p){//查询父节点
    if(l==r){
        return tr[x].v;
    }
    int mid=l+r>>1;
    if(p<=mid)return query(tr[x].lc,l,mid,p);
    return query(tr[x].rc,mid+1,r,p);
}

int find(int x,int p){
    int fa=query(x,1,n,p);
    if(p==fa)return fa;
    return find(x,fa);//x是版本
}

修改&合并

void change(int pre,int now,int l,int r,int p,int fa){//修改父节点 
    now=++idx;
    tr[now]=tr[pre];
    if(l==r){
        tr[now].v=fa;
        return;
    }
    int mid=l+r>>1;
    if(p<=mid)change(tr[pre].lc,tr[now].lc,l,mid,p,fa);
    else change(tr[pre].rc,tr[now].rc,mid+1,r,p,fa);
}

void merge(int a,int b,int i){
	a=find(rt[i-1],a),b=find(rt[i-1],b);
    // cerr<<"OK\n";
	if(a==b){rt[i]=rt[i-1];return ;}
	if(getrnk(rt[i-1],1,n,a)>getrnk(rt[i-1],1,n,b))swap(a,b);
	// int t;
    change(rt[i-1],rt[i],1,n,a,b);
    int t=rt[i];
    changernk(t,rt[i],1,n,b,getrnk(rt[i-1],1,n,a)+1);//sz[b]+=sz[a];
	
}

COde

WA64pts

/*
CB Ntsc
*/

#include <bits/stdc++.h>
using namespace std;
//#define int long long
#define mp make_pair

const int N = 2e5 + 5;
const int M = 1e7 + 5;
const int INF = 1e9 + 5;
const int MOD = 1e9 + 7;
bool f1;
int rt[N];
int q, n, m, ans, idx, T;
vector<int> b;
bool f2;


#define rd read()
inline int read() {
    int xx = 0, ff = 1;
    char ch = getchar();
    while (ch < '0' || ch > '9') {
        if (ch == '-')
            ff = -1;
        ch = getchar();
    }
    while (ch >= '0' && ch <= '9') xx = xx * 10 + (ch - '0'), ch = getchar();
    return xx * ff;
}
inline void write(int out) {
    if (out < 0)
        putchar('-'), out = -out;
    if (out > 9)
        write(out / 10);
    putchar(out % 10 + '0');
}

struct node{
	int lc,rc,v,rnk;
}tr[N*50];//50倍空间 ,32不够 



void build(int &x,int l,int r){
	x=++idx;
	if(l==r){
        tr[x].v=l;
        tr[x].rnk=1;
        return;
    }
	build(tr[x].lc,l,l+r>>1);
	build(tr[x].rc,(l+r>>1)+1,r);
}

int getrnk(int x,int l,int r,int p){//单点查询秩
    if(l==r)return tr[x].rnk;
    int mid=l+r>>1;
    if(p<=mid)return getrnk(tr[x].lc,l,mid,p);
    return getrnk(tr[x].rc,mid+1,r,p);
}

void changernk(int pre,int &now,int l,int r,int p,int v){//修改秩(树的高度)
    now=++idx;
    tr[now]=tr[pre];
    if(l==r){
        tr[now].rnk=max(tr[now].rnk,v);//要比对取最大值
        return;
    }
    int mid=l+r>>1;
    if(p<=mid)changernk(tr[pre].lc,tr[now].lc,l,mid,p,v);
    else changernk(tr[pre].rc,tr[now].rc,mid+1,r,p,v);
    return ;
}
// void insert(int pre,int &now,int l,int r,int v){
// 	now=++idx;//动态开点.新插入一个点 为了可以方便的用这个now更新上一个函数空间的tr[now].lc(或rc),我们就引用一下
// 	tr[now]=tr[pre];//复制旧点的信息
// 	tr[now].v++;//点权+1,因为插入的树在now的区间内
// 	if(l==r)return;
// 	int mid=l+r>>1;
// 	if(v<=mid)insert(tr[pre].lc,tr[now].lc,l,mid,v);
// 	else insert(tr[pre].rc,tr[now].rc,mid+1,r,v);
// }

int query(int x,int l,int r,int p){//查询父节点
    if(l==r){
        return tr[x].v;
    }
    int mid=l+r>>1;
    if(p<=mid)return query(tr[x].lc,l,mid,p);
    return query(tr[x].rc,mid+1,r,p);
}

int find(int x,int p){
    int fa=query(x,1,n,p);
    if(p==fa)return fa;
    return find(x,fa);//x是版本
}

int change(int pre,int l,int r,int p,int fa){//修改父节点 
  //这里不能引用&now直接修改,在merge中会出错->note
    int now=++idx;
    tr[now]=tr[pre];
    if(l==r){
        tr[now].v=fa;
        return now;
    }
    int mid=l+r>>1;
    if(p<=mid)tr[now].lc=change(tr[pre].lc,l,mid,p,fa);
    else tr[now].rc=change(tr[pre].rc,mid+1,r,p,fa);
    return now;
}

void merge(int a,int b,int i){
	a=find(rt[i-1],a),b=find(rt[i-1],b);
    // cerr<<"OK\n";
	if(a==b){rt[i]=rt[i-1];return ;}
	if(getrnk(rt[i-1],1,n,a)>getrnk(rt[i-1],1,n,b))swap(a,b);
	// int t;
//    change(rt[i-1],rt[i],1,n,a,b);
	int t=change(rt[i-1],1,n,a,b);
//    int t=rt[i];
    changernk(t,rt[i],1,n,b,getrnk(rt[i-1],1,n,a)+1);//sz[b]+=sz[a];
	
}


signed main() {
    // freopen("chfran.in", "r", stdin);
    // freopen("chfran.out", "w", stdout);
    n=rd;m=rd;
    
    build(rt[0],1,n);
    
    for(int i=1;i<=m;i++) {
        int op=rd;
        if(op==1){
            int a=rd,b=rd;
            merge(a,b,i);
        }if(op==2){
            int k=rd;
            rt[i]=rt[k];
        }if(op==3){
            int a=rd,b=rd;
            rt[i]=rt[i-1];
            if(find(rt[i],a)==find(rt[i],b))cout<<1<<endl;
            else cout<<0<<endl;
        }
    }
    
    
}

/*
1
2 5 1 
0 0 1 
0 0 4 

*/

Hint

int change(int pre,int l,int r,int p,int fa){//修改父节点 
  //这里不能引用&now直接修改,在merge中会出错->note
    ...
}

原因:

  • 13
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值