【暑期训练】并查集

并查集基础知识:

功能:实现集合的合并与查找

实现原理:

  • 用树表示集合,若根节点相同则属于同一个集合,否则不属于同一个集合
  • 合并:将一个集合的根节点连接在另一个根节点的下面

按秩合并:

秩:树的高度
即将矮树的连接在高树的下面,若高度相同,则任意连接,并将树高加一。

int union_root(int x,int y,int r[])
{
    int x_root=find_root(x);
    int y_root=find_root(y);
    if(x_root==y_root)//如果在同一集合,即根节点相同
    {
        return 0;
    }
    else//如果不在同一集合,即根节点不同,则将矮树的连接在高树的下面
    {
        if(r[x_root]>r[y_root])
        {
            fa[y_root]=x_root;
        }
        else if(r[x_root]<r[y_root])
        {
            fa[x_root]=y_root;
        }
        else//若高度相同,则任意连接,并将树高加一
        {
            fa[x_root]=y_root;
            r[y_root]++;
        }
        return 1;
    }
}

路径压缩:

在搜索树时,若节点的父节点不是根结点,则将树的父节点直接改为根结点,减少下次搜索时间

int find_root(int x)
{
    if(fa[x]==x)return x;//若父节点是根结点
    fa[x]=find_root(fa[x]);//若节点的父节点不是根结点,则将树的父节点直接改为根结点
    return fa[x];
}

简写版:

int find_root(int x)
{
    return (fa[x]==x)?x:fa[x]=find_root(fa[x]);
}

一般来说并查集用路径压缩比较多,因为不用计算树高,写起来更简单。只用按秩合并或路径压缩的时间复杂度都是O(logn),按秩合并+路径压缩的并查集单次操作可以看作O(n)

相关算法学习视频:

【算法】并查集(Disjoint Set)[共3讲]

练习:

不带权的并查集:

洛谷P3367 【模板】并查集

#include <bits/stdc++.h>
using namespace std;
int n,m;
int fa[10005];
int find_root(int x)//路径压缩
{
    return (fa[x]==x)?x:fa[x]=find_root(fa[x]);
}

int judge_root(int x,int y,int z)
{
    int x_root=find_root(x);
    int y_root=find_root(y);
    if(z==1)
    {
        fa[x_root]=y_root;
    }
    if(z==2)
    {
    if(x_root==y_root)
    {
        printf("Y\n");
    }
    else
    {
       printf("N\n");
    }
    }
    return 0;
}

void init()
{
    for(int i=1;i<=n;i++)
    {
        fa[i]=i;
    }
}

int main()
{
    int z,x,y;
   while(cin>>n>>m)
   {
     init();
     while(cin>>z>>x>>y)
     {
      judge_root(x,y,z);
     }
   }
   return 0;
}

畅通工程并查集版

#include <bits/stdc++.h>
using namespace std;
int n,m,ans;
int fa[10005];
int find_root(int x)//路径压缩
{
    return (fa[x]==x)?x:fa[x]=find_root(fa[x]);
}

int union_root(int x,int y)//合并
{
    int x_root=find_root(x);
    int y_root=find_root(y);
    if(x_root!=y_root)
    {
        fa[x_root]=y_root;
        ans--;
    }
    return 0;
}
void init()//初始化
{
    for(int i=1;i<=n;i++)
    {
        fa[i]=i;
    }
}

int main()
{
    int x,y;
   while(cin>>n>>m&&n!=0)
   {
     ans=n-1;
     init();
     while(m--)
     {
      scanf("%d%d",&x,&y);
      union_root(x,y);
     }
     printf("%d\n",ans--);
   }
   return 0;
}

小希的迷宫

这个题要注意如果最后结果是多个联通分支是不满足题目条件“任意两个房间有且仅有一条路径可以相通”的,需要特判一下。

#include <bits/stdc++.h>
using namespace std;
int n,m;
int fa[100005],vis[100005];
struct sa
{
    int x,y;
}a[100005];
int find_root(int x)
{
    return (fa[x]==x)?x:fa[x]=find_root(fa[x]);
}

int union_root(int x,int y)
{
    int x_root=find_root(x);
    int y_root=find_root(y);
    if(x_root!=y_root)
    {
        fa[x_root]=y_root;
        return 0;
    }
    else{return 1;}
}
int main()
{
    int x,y,f,cnt;
    while(1)
    {
    n=0;
    f=0;
    memset(fa,0,sizeof(fa));
    memset(vis,0,sizeof(vis));
    while(cin>>x>>y)
    {
      if(x==-1&&y==-1){break;}
      if(x==0&&y==0){break;}
      a[n].x=x;
      a[n].y=y;
      fa[a[n].x]=a[n].x;
      fa[a[n].y]=a[n].y;
      vis[a[n].x]=1;
      vis[a[n].y]=1;
      n++;
    }
    for(int i=0;i<n;i++)
    {
        if(union_root(a[i].x,a[i].y)==1){f=1;}
    }
    cnt=0;
    for(int i=0;i<100005;i++)//判断是否存在多个联通分支
    {
        if(vis[i]&&fa[i]==i)
        {
            cnt++;
        }
    }
    if(cnt>1){f=1;}
    if(!(x==-1&&y==-1))
    {
    if(f==1)
     {
         printf("No\n");
     }
     else
     {
         printf("Yes\n");
     }
    }
    if(x==-1&&y==-1){break;}
    }
    return 0;
}

带权的并查集:

带权的并查集与不带权的并查集相比多了一个比较的过程,实现原理是一样的。

P3366 【模板】最小生成树

最小生成树有两个算法:Prim和Kruskal,我写的都是Kruskal算法。
算法原理:最小生成树(Kruskal(克鲁斯卡尔)和Prim(普里姆))算法动画演示

#include <bits/stdc++.h>
using namespace std;
const int MAXN=200005;
int n,m;
int fa[MAXN],vis[MAXN];
struct sa
{
    int x,y,z;
}edge[MAXN];
int find_root(int x)
{
    return (fa[x]==x)?x:fa[x]=find_root(fa[x]);
}

bool cmp(struct sa x,struct sa y)
{
    return x.z<y.z;
}

int main()
{
    while(cin>>n>>m)
    {
        for(int i=1;i<=m;i++)
        {
            cin>>edge[i].x>>edge[i].y>>edge[i].z;
        }
        sort(edge+1,edge+1+m,cmp);
        for(int i=1;i<=n;i++)
        {
            fa[i]=i;
            vis[i]=1;
        }
        int sum=0,total=0;
        for(int i=1;i<=m;i++)
        {
            if(total==n-1){break;}
            int u=edge[i].x;
            int v=edge[i].y;
            int u_root=find_root(u);
            int v_root=find_root(v);
            if(u_root!=v_root)
            {
              fa[u_root]=v_root;
              sum+=edge[i].z;
              total++;
            }
        }
        int cnt=0;
        for(int i=1;i<=n;i++)
        {
            if(fa[i]==i)
                cnt++;
        }
        //printf("cnt=%d\n",cnt);
        if(cnt>1){printf("orz\n");}
        else{printf("%d\n",sum);}
    }
    return 0;
}

湖南修路

这题要考虑已经修过的路无论权值多少都不用再修的情况。

#include <bits/stdc++.h>
using namespace std;
const int MAXN=200005;
int n,m;
int fa[MAXN],vis[MAXN];
struct sa
{
    int x,y,z;
    int flag;
}edge[MAXN];
int find_root(int x)
{
    return (fa[x]==x)?x:fa[x]=find_root(fa[x]);
}

bool cmp(struct sa x,struct sa y)
{
    if(x.flag!=y.flag)//若路已经修过了,则不用考虑其权值都排在前面,若没修过,则按权值从小到大排
    {
        return x.flag>y.flag;
    }
    else
     return x.z<y.z;
}

int main()
{
    while(cin>>n)
    {
        if(n==0){break;}
        m=(n*(n-1))/2;
        for(int i=1;i<=m;i++)
        {
            cin>>edge[i].x>>edge[i].y>>edge[i].z>>edge[i].flag;
        }
        sort(edge+1,edge+1+m,cmp);
        for(int i=1;i<=n;i++)
        {
            fa[i]=i;
        }
        int total=0,sum=0;
        for(int i=1;i<=m;i++)
        {
            if(total==n-1){break;}
            int u=edge[i].x;
            int v=edge[i].y;
            int u_root=find_root(u);
            int v_root=find_root(v);
            if(u_root!=v_root)
            {
              if(!edge[i].flag)
              {
              fa[u_root]=v_root;
              sum+=edge[i].z;
              }
              total++;
            }
        }
        printf("%d\n",sum);
    }
    return 0;
}

最小树1

#include <bits/stdc++.h>
using namespace std;
const int MAXN=10005;
int n,m;
int fa[MAXN],vis[MAXN];
struct sa
{
    int x,y,z;
}edge[MAXN];
int find_root(int x)
{
    return (fa[x]==x)?x:fa[x]=find_root(fa[x]);
}

bool cmp(struct sa x,struct sa y)
{
    return x.z<y.z;
}

int main()
{
    while(cin>>m>>n)
    {
        if(m==0){break;}
        for(int i=1;i<=m;i++)
        {
            cin>>edge[i].x>>edge[i].y>>edge[i].z;
        }
        sort(edge+1,edge+1+m,cmp);
        for(int i=1;i<=n;i++)
        {
            fa[i]=i;
            vis[i]=1;
        }
        int sum=0,total=0;
        for(int i=1;i<=m;i++)
        {
            if(total==n-1){break;}
            int u=edge[i].x;
            int v=edge[i].y;
            int u_root=find_root(u);
            int v_root=find_root(v);
            if(u_root!=v_root)
            {
              fa[u_root]=v_root;
              sum+=edge[i].z;
              total++;
            }
        }
        int cnt=0;
        for(int i=1;i<=n;i++)//检查联通分支数
        {
            if(fa[i]==i)
                cnt++;
        }
        if(cnt>1){printf("?\n");}//若联通分支数>1,则不是最小生成树
        else{printf("%d\n",sum);}
    }
    return 0;
}

剩下几个题太相似了,就不贴了。

最小瓶颈路

定义:
最小瓶颈路即为两点之间所有路径中权值最大的边最小的路径
案例分析:
案例所示图如下
在这里插入图片描述
以1 7为例:
在众多的路径当中,显然图示这条路径的边的最大权值最小,为30。
在这里插入图片描述
结合Kruskal算法我们可以这样思考:在生成最小生成树的同时,检查两个点是否在同一个连通分支里,当添加某一边使得恰好两个点处于同一连通分支中时,该边即为所求。
关于最小瓶颈路的讲解还可以参考:
最小瓶颈路含义
20190521测试总结

#include <bits/stdc++.h>
using namespace std;
const int MAXN=100005;
int n,m,k;
int fa[MAXN],ans[MAXN];//fa[MAXN]保存父节点,ans[MAXN]保存询问的答案
struct sa
{
    int x,y,z;
}edge[MAXN],vis[MAXN];
int find_root(int x)
{
    return (fa[x]==x)?x:fa[x]=find_root(fa[x]);
}

bool cmp(struct sa x,struct sa y)
{
     return x.z<y.z;
}

int main()
{
    while(~scanf("%d%d%d",&n,&m,&k))
    {
        memset(ans,-1,sizeof(ans));
        for(int i=1;i<=m;i++)
        {
            scanf("%d%d%d",&edge[i].x,&edge[i].y,&edge[i].z);
        }
        sort(edge+1,edge+1+m,cmp);
        for(int i=1;i<=n;i++)
        {
            fa[i]=i;
        }
        for(int i=1;i<=k;i++)
        {
            scanf("%d%d",&vis[i].x,&vis[i].y);
        }
        int total=0;
        for(int i=1;i<=m;i++)
        {
            if(total==n-1){break;}
            int u=edge[i].x;
            int v=edge[i].y;
            int u_root=find_root(u);
            int v_root=find_root(v);
            if(u_root!=v_root)
            {
              fa[u_root]=v_root;
              total++;
              for(int j=1;j<=k;j++)
              {
                  if(ans[j]==-1&&find_root(vis[j].x)==find_root(vis[j].y))//检查两个点是否在同一个连通分支里,当添加某一边使得恰好两个点处于同一连通分支中时,该边即为所求
                  {
                    ans[j]=edge[i].z;
                  }
              }
            }
        }
        for(int i=1;i<=k;i++)
        {
            printf("%d\n",ans[i]);
        }
    }
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值