【并查集】

并查集

解决连通性问题

模版代码

启发式合并

struct DSU {
    vector<int> fa, p, e, f;

    DSU(int n) {
        fa.resize(n + 1);
        iota(fa.begin(), fa.end(), 0);
        p.resize(n + 1, 1);
        e.resize(n + 1);
        f.resize(n + 1);
    }
    int get(int x) {
        while (x != fa[x]) {
            x = fa[x] = fa[fa[x]];
        }
        return x;
    }
    bool merge(int x, int y) { // 设x是y的祖先
        if (x == y) f[get(x)] = 1;
        x = get(x), y = get(y);
        e[x]++;
        if (x == y) return false;
        if (x < y) swap(x, y); // 将编号小的合并到大的上
        fa[y] = x;
        f[x] |= f[y], p[x] += p[y], e[x] += e[y];
        return true;
    }
    bool same(int x, int y) {
        return get(x) == get(y);
    }
    bool F(int x) { // 判断连通块内是否存在自环
        return f[get(x)];
    }
    int size(int x) { // 输出连通块中点的数量
        return p[get(x)];
    }
    int E(int x) { // 输出连通块中边的数量
        return e[get(x)];
    }
};

1.将两个集合合并
2.询问两个元素是否在同一集合中

基本原理

每个集合用一棵树来表示
树根的编号为整个集合的编号
每个节点存储它的父节点->p[N]

问题1:如何判断树根 if(p[x]==x)

问题2:如何求x的集合编号 while(p[x]!=x) x=p[x]

问题3:如何合并两个集合

px是x的集合编号 py是y的集合编号
p[px]=py

核心代码:递归查找

image

int p[N];//记录父亲数组 

int find(int x){//返回x所在集合的编号【祖宗节点】 + 路径压缩 
    if(p[x]!=x) p[x]=find(p[x]);//如果它不是根节点 -> 父节点=父节点的祖宗节点 
    return p[x];
}

【题目积累】

【并查集基本操作】Graph Composition

https://codeforces.com/contest/2060/problem/E
注意存边
注意操作顺序(先删边后连边)

思路

image

代码

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
typedef long long ll;
typedef pair<int,int> PII; 
struct DSU{
    vector<int> p,sz;
	//建立并查集
    DSU(int n):p(n + 1),sz(n + 1,1){for(int i = 1;i <= n;i++)p[i] = i;}
	//找父亲+合并
    int find(int x){return (p[x] == x) ? x : p[x] = find(p[x]);}
	//查找是否相同
    bool same(int x,int y){return find(x) == find(y);}
	//合并两集合
    bool merge(int x,int y){
        x = find(x),y = find(y);
        if(x == y)return false;
        sz[x] += sz[y];
        p[y] = x;
        return true;
    }
	//查询集合内数量
    int size(int x){return sz[find(x)];}
};
int t;
int n,m1,m2;
/*【思路】
删边:遍历F的每一条边 如果在G不连通就删掉 否则在F内连通
加边:遍历G的每一条边 如果在F不连通就加上 
*/ 
void solve(){
	cin>>n>>m1>>m2;
	vector<PII> gg,ff;//存边 
	DSU f(n),g(n);//建立并查集
	//先把所有边存下来 
	for(int i=1;i<=m1;i++){
		int u,v;
		cin>>u>>v;
		if(u>v) swap(u,v);
		ff.push_back({u,v});
	}
	for(int i=1;i<=m2;i++){
		int u,v;
		cin>>u>>v;
		if(u>v) swap(u,v);
		gg.push_back({u,v});
		//建立连通性
		g.merge(u,v);
	}
	ll ans=0;
	for(int i=0;i<m1;i++){
		int u=ff[i].first,v=ff[i].second;
		if(g.same(u,v)) f.merge(u,v);
		else ans++;
	}
	for(int i=0;i<m2;i++){
		int u=gg[i].first,v=gg[i].second;
		if(!f.same(u,v)){
			ans++;
			f.merge(u,v);
		}
	}
	cout<<ans<<endl;
}
signed main(){
      ios::sync_with_stdio(0);
      cin.tie(0);
      cout.tie(0);
      cin>>t;
      while(t--){
      	solve();
	}
      return 0;
}

【位运算优化n^2】隐匿社交网络

https://ac.nowcoder.com/acm/contest/100253/D

思路

看w数据范围 -> 每个数在二进制下最多64位(遍历64*n次即可)
->只要有一位一样就可以合并->当位有1就可以合并

代码

#include<bits/stdc++.h>
using namespace std;
#define endl '\n' 
typedef long long ll;
const int N=100010; 
int t,n;
ll w[N];
struct DSU{
    vector<int> p,sz;
	//建立并查集
    DSU(int n):p(n + 1),sz(n + 1,1){for(int i = 1;i <= n;i++)p[i] = i;}
	//找父亲+合并
    int find(int x){return (p[x] == x) ? x : p[x] = find(p[x]);}
	//查找是否相同
    bool same(int x,int y){return find(x) == find(y);}
	//合并两集合
    bool merge(int x,int y){
        x = find(x),y = find(y);
        if(x == y)return false;
        sz[x] += sz[y];
        p[y] = x;
        return true;
    }
	//查询集合内数量
    int size(int x){return sz[find(x)];}
};
void solve(){
	cin>>n;
	for(int i=1;i<=n;i++) cin>>w[i];
	DSU q(n);
	for(int i=63;i>=0;i--){//减少循环次数 
		int p=-1;//记录当前要合并进的集合
		for(int j=1;j<=n;j++){
			if(w[j]>>i&1){//取每一位
				if(p!=-1) q.merge(p,j); 
				p=j;
			}
		}
	}
	int ans=-1;
	for(int i=1;i<=n;i++) ans=max(ans,q.size(i));
	cout<<ans<<endl;
}
signed main(){
      ios::sync_with_stdio(0);
      cin.tie(0);
      cout.tie(0);
      cin>>t;
      while(t--){
      	solve();
	}
      return 0;
}

【图上的运用】在树上游玩

https://ac.nowcoder.com/acm/contest/100671/E
只涉及到边的题目:直接存边即可 不需要邻接表

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
typedef pair<int,int> PII;
typedef long long ll;
ll abss(ll a){return a>0?a:-a;}
ll max_(ll a,ll b){return a>b?a:b;}
ll min_(ll a,ll b){return a<b?a:b;}
bool cmpll(ll a,ll b){return a>b;}
const int N=100010;
const int mod=1e9+7;
int n,k;
int a[N];
int cnt[N];
struct DSU{
    vector<int> p,sz;
	//建立并查集
    DSU(int n):p(n + 1),sz(n + 1,1){for(int i = 1;i <= n;i++)p[i] = i;}
	//找父亲+合并
    int find(int x){return (p[x] == x) ? x : p[x] = find(p[x]);}
	//查找是否相同
    bool same(int x,int y){return find(x) == find(y);}
	//合并两集合
    bool merge(int x,int y){
        x = find(x),y = find(y);
        if(x == y)return false;
        sz[x] += sz[y];
        p[y] = x;
        return true;
    }
	//查询集合内数量
    int size(int x){return sz[find(x)];}
};
vector<PII> deg;
signed main(){
      ios::sync_with_stdio(0);
      cin.tie(0);
      cout.tie(0);
      cin>>n>>k;
      for(int i=1;i<=k;i++){
            int cc;
            cin>>cc;
            a[cc]=1;
      }
      /*涉及到对边操作的都直接存边!!!*/
      for(int i=1;i<=n-1;i++){
            int u,v;
            cin>>u>>v;
            deg.push_back({u,v});
      }
      DSU dsu(n);
      //枚举每条边:n-1条边所以可以枚举边
      for(auto [u,v]:deg){
            if(a[u]&&a[v]) dsu.merge(u,v);
      }
      //统计出边
      for(auto [u,v]:deg){
            if(!a[u] && a[v]) cnt[dsu.find(v)]++;//找到父亲再加:为统计连通块数量方便
            if(!a[v] && a[u]) cnt[dsu.find(u)]++;
      }
      int res=0;
      ll ans=1;
      for(int i=1;i<=n;i++){
            if(cnt[i]==0) continue;
            res++;//注意这里查询连通块数目的方式!非常简洁
            ans=ans*cnt[i]%mod;
      }
      cout<<res<<" "<<ans;
      return 0;
}

【带权并查集】食物链

题目来源:https://www.acwing.com/problem/content/242/
image
image

//【带权并查集】 
/*【判断谁吃谁:与根节点距离】差值%3 
 余1 :可以吃根节点 
 余2 :可以被根节点吃
 余0 : 与根节点同类 
*/ 
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int n,k;
int p[N],d[N];//d存储该节点到根节点的距离->有规律 
int ans=0;
int find(int x){
    if(p[x]!=x){
        int t=find(p[x]);//p[x]不能先更新! 
	    d[x]+=d[p[x]];//当前点到父节点的距离+父节点到根节点的距离=当前点到根节点的距离
	    p[x]=t;
    }
	return p[x];
}
int main(){
	scanf("%d%d",&n,&k);
	//预处理p数组!!!初始自己的祖宗节点都是自己!!!
	for(int i=1;i<=n;i++) p[i]=i;
	while(k--){
		int a,x,y;
		scanf("%d%d%d",&a,&x,&y);
		if(x>n || y>n) ans++;
		else{
			int px=find(x),py=find(y);//先找到各自的节点 
			if(a==1){//判断同类  
				if(px==py && (d[x]-d[y])%3) ans++;//在同一个树里->两者差与3的模不为0->不成立为假话 
				else if(px!=py){//不在同一个树里:真话->变为同一个树 
					p[px]=py;
					d[px]=d[y]-d[x];//与根节点距离相同:同类 
				} 
			}
			else{
				if(px==py && (d[x]-d[y]-1)%3) ans++;//x吃y->x=y+1为规律推出 
				else if(px!=py){
					p[px]=py;
					d[px]=d[y]+1-d[x];
				}
			}
		}
	}
	printf("%d\n",ans);
	return 0;
}

【维护点和边】Make it Forest

https://atcoder.jp/contests/abc399/tasks/abc399_c
注意merge内加边和维护边的操作

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
typedef pair<int,int> PII;
typedef long long ll;
ll abss(ll a){return a>0?a:-a;}
ll max_(ll a,ll b){return a>b?a:b;}
ll min_(ll a,ll b){return a<b?a:b;}
bool cmpll(ll a,ll b){return a>b;}
const int N=2e5+10;
int n,m;
bool st[N];
/*
并查集 该连通块
若边数>=点数 ans+=边数-点数+1
*/
struct DSU{
      vector<int> p,sz1,sz2;
        //建立并查集
      DSU(int n):p(n + 1),sz1(n + 1,1),sz2(n+1,0){for(int i = 1;i <= n;i++)p[i] = i;}
        //找父亲+合并
      int find(int x){return (p[x] == x) ? x : p[x] = find(p[x]);}
        //查找是否相同
      bool same(int x,int y){return find(x) == find(y);}
        //合并两集合
      bool merge(int x,int y){
          x = find(x),y = find(y);
          if(x == y){
            sz2[x]+=1;
            return false;
          }
          sz2[x]+=sz2[y]+1;
          sz1[x] += sz1[y];
          p[y] = x;
          return true;
      }
        //查询集合内数量
      int size(int x){return sz1[find(x)];}
      int size2(int x){
            return sz2[find(x)];
      }
  };
signed main(){
      ios::sync_with_stdio(0);
      cin.tie(0);
      cout.tie(0);
      cin>>n>>m;
      DSU dsu(n+1);
      for(int i=1;i<=m;i++){
            int u,v;
            cin>>u>>v;
            dsu.merge(u,v);
      }
      /*
      for(int i=1;i<=n;i++){
            cout<<dsu.size(i)<<" ";
      }
      cout<<endl;
      for(int i=1;i<=n;i++){
            cout<<dsu.size2(i)<<" ";
      }
      */
      int ans=0;
      for(int i=1;i<=n;i++){
        int p=dsu.find(i);
        if(!st[p]){
          ans+=dsu.sz2[p]-dsu.sz1[p]+1;
        }
        st[p]=1;
        st[i]=1;
      }
      cout<<ans;
      return 0;
}

Disappearing Permutation

【只要涉及到多点关联关系的,都可以尝试并查集/连通块的思想解决问题】
https://codeforces.com/contest/2086/problem/C

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
typedef pair<int,int> PII;
typedef long long ll;
ll abss(ll a){return a>0?a:-a;}
ll max_(ll a,ll b){return a>b?a:b;}
ll min_(ll a,ll b){return a<b?a:b;}
bool cmpll(ll a,ll b){return a>b;}
int t;
int n;
struct DSU{
      vector<int> p,sz;
        //建立并查集
      DSU(int n):p(n + 1),sz(n + 1,1){for(int i = 1;i <= n;i++)p[i] = i;}
        //找父亲+合并
      int find(int x){return (p[x] == x) ? x : p[x] = find(p[x]);}
        //查找是否相同
      bool same(int x,int y){return find(x) == find(y);}
        //合并两集合
      bool merge(int x,int y){
          x = find(x),y = find(y);
          if(x == y)return false;
          sz[x] += sz[y];
          p[y] = x;
          return true;
      }
        //查询集合内数量
      int size(int x){return sz[find(x)];}
  };
void solve(){
      cin>>n;
      vector<int> p(n+1),d(n+1);
      for(int i=1;i<=n;i++) cin>>p[i];
      for(int i=1;i<=n;i++) cin>>d[i];
      int ans=0;
      DSU dsu(n);
      for(int i=1;i<=n;i++){
            dsu.merge(i,p[i]);
      }
      vector<bool> st(n+1,0);
      for(int i=1;i<=n;i++){
            int b=dsu.find(p[d[i]]);
            if(!st[b]){
                  ans+=dsu.size(b);
                  cout<<ans<<" ";
                  st[b]=1;
            }
            else{
                  cout<<ans<<" ";
            }
      }
      cout<<endl;
}
signed main(){
      ios::sync_with_stdio(0);
      cin.tie(0);
      cout.tie(0);
      cin>>t;
      while(t--) solve();
      return 0;
}
posted @ 2024-10-28 00:10  White_ink  阅读(32)  评论(0)    收藏  举报