并查集和最小生成树

一:并查集
1.并查集的概念:顾名思义,并查集就是合并集合或者在集合里查找元素的算法。

2.性质:并查集算法(union_find sets)不支持分割一个集合,可求连通子图、求最小生成树。
3.引例:杭电HDU1232畅通工程

某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府“畅通工程”的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问最少还需要建设多少条道路?

测试输入包含若干测试用例。每个测试用例的第1行给出两个正整数,分别是城镇数目N ( < 1000 )和道路数目M;随后的M行对应M条道路,每行给出一对正整数,分别是该条道路直接连通的两个城镇的编号。为简单起见,城镇从1到N编号。
注意:两个城市之间可以有多条道路相通,也就是说
3 3
1 2
1 2
2 1
这种输入也是合法的
当N为0时,输入结束,该用例不被处理。
解析一下题意就是求有几个独立的联通块,比如有三个独立的联通块,那么我只要修两条路就可以把这三个联通为一个了,那么城镇有几百个,还可能有环,这个怎么判断呢??不要怕!我们有并查集!!!
4.并查集实现的思路:用集合中的某个元素来代表这个集合,该元素称为集合的 代表元
一个集合内的所有元素组织成以代表元为根的树形结构。
对于每一个元素 parent[x]指向x在树形结构上的父亲节点。如果x是根节点,则令parent[x] = x。
对于查找操作,假设需要确定x所在的的集合,也就是确定集合的代表元。可以沿着parent[x]不断在树形结构中向上移动,直到到达根节点。
并查集包含三个部分:1.保存前导的数组pre,用来保存每个节点的前导 2.find函数,用于查找元素的根节点,并且压缩路径  3.join函数,用于合并集合
我们每输入数据,就用find函数查找他们的根节点,然后用join将两根结点连到一起,于是数据就有了共同的根节点,就代表在一个集合里有了共同的代表元素!所以,判断两个元素是否属于同一集合,只需要看他们的代表元是否相同即可。
对于路径压缩:是为了加快查找元素的速度,查找时将x到根节点路径上的所有点的 parent设为根节点,该优化方法称为压缩路径。
5.核心代码:
(1)pre[maxn]  //记录前导点
(2)初始化pre:
    for(int i=1; i<=maxn; i++)
        pre[i]=i;//每个点的前导点设为自己
(3)查找
int finded(int a)
{
    int r=a;
    while(pre[r]!=r)
        r=pre[r];//找到a的根节点r
    int i=a,j;
    while(i!=r)//路径压缩,将a以上的所有节点全部连接到根节点上!
    {
        j=pre[i];
        pre[i]=r;
        i=j;
    }
    return r;//返回根节点
}
(4)合并
void join(int x,int y)
{
    int fx=finded(x);//找到根结点
    int fy=finded(y);
    if(fx!=fy)
    {
        pre[fx]=fy;//合并
        
    }
}

版本二:(为了解决大数据下的退化问题,提高查找效率,使用rank数组记录每个节点为根下的深度,深度小的连接在深度大的上面,防止退化!)

void join(int x,int y)
{
    int fx=finded(x);
    int fy=finded(y);
    if(fx!=fy)
    {
        if(ranked[fx]<ranked[fy])//深度小的连接在深度大的根节点上
            pre[fx]=fy;
        else
        {
            pre[fy]=fx;
            if(ranked[fx]==ranked[fy])
                ranked[fx]++;
        }
    }
}
6.引例解决:好啦,知道了这些以后!我们就能解决引例了,废话不多说,直接上代码:
#include <iostream>
#include<bits/stdc++.h>
using namespace std;
int pre[1001];
int fined(int a)
{
    int r=a;
    while(pre[a]!=a)
        a=pre[a];
    int i=r,j;
    while(i!=a)
    {
        j=pre[i];
        pre[i]=a;
        i=j;
    }
    return a;
}
void join(int x,int y)
{
    int fx=fined(x),fy=fined(y);
    if(fx!=fy)
        pre[fx]=fy;
}
int main()
{
    int n,m;
    while(cin>>n&&n)
    {
        cin>>m;
        int sum=0;
        memset(vis,0,sizeof(vis));
        for(int i=1;i<=n;i++)
            pre[i]=i;
        for(int j=0;j<m;j++)
        {
            int a,b;
            cin>>a>>b;
            join(a,b);
        }
        for(int i=1;i<=n;i++)
        {
            if(pre[i]=i)
                sum++;
        }
        cout<<sum-1<<endl;
    }
    return 0;
}

注意:输入完数据以后,所有节点的pre并不能全部更新为最终根节点,所以如果想求是否在一个集合里,还要跑一边find
7.一个比较有意思的并查集详解:   好玩的并查集详解
8.例题:
(1)天梯赛选拔--部落

在一个社区里,每个人都有自己的小圈子,还可能同时属于很多不同的朋友圈。我们认为朋友的朋友都算在一个部落里,于是要请你统计一下,在一个给定社区中,到底有多少个互不相交的部落?并且检查任意两个人是否属于同一个部落。


输入在第一行给出一个正整数N104),是已知小圈子的个数。随后N行,每行按下列格式给出一个小圈子里的人:

KP[1]P[2]P[K]

其中K是小圈子里的人数,P[i]i=1,,K)是小圈子里每个人的编号。这里所有人的编号从1开始连续编号,最大编号不会超过104

之后一行给出一个非负整数Q104),是查询次数。随后Q行,每行给出一对被查询的人的编号。


————————————————我是分界线君————————————————————————————————————————
思路,每组圈子都与第一个人join建立集合,对于如何查人数,只要统计最大的编号就可以啦!最大的编号数就代表人的个数!对于圈子个数,其实就是畅通工程里的求联通块个数,然后要查询所以跑一边find,如何判断pre是否相等即可!!
废话不说上代码:
#include <iostream>
#include<bits/stdc++.h>
using namespace std;
int pre[10000];
int ranked[10000];
int finded(int a)
{
    int r=a;
    while(pre[r]!=r)
        r=pre[r];
    int i=a,j;
    while(i!=r)
    {
        j=pre[i];
        pre[i]=r;
        i=j;
    }
    return r;
}
void join(int x,int y)
{
    int fx=finded(x);
    int fy=finded(y);
    if(fx!=fy)
    {
        if(ranked[fx]<ranked[fy])
            pre[fx]=fy;
        else
        {
            pre[fy]=fx;
            if(ranked[fx]==ranked[fy])
                ranked[fx]++;
        }
    }
}
int main()
{
    int n;
    cin>>n;
    memset(pre,0,sizeof(pre));
    memset(ranked,0,sizeof(ranked));
    int sum=0;
    for(int i=1; i<=10000; i++)
        pre[i]=i;
    while(n--)
    {
        int a,b,c;
        cin>>a>>b;
        if(b>sum)
            sum=b;
        a--;
        while(a--)
        {
            cin>>c;
            if(c>sum)
                sum=c;
            join(b,c);
        }
    }
    int ans=0;
    for(int i=1; i<=sum; i++)
    {
        finded(i);
        if(pre[i]==i)
            ans++;
    }
    cout<<sum<<" "<<ans<<endl;
    int k;
    cin>>k;
    while(k--)
    {
        int t1,t2;
        cin>>t1>>t2;
        if(pre[t1]==pre[t2])
            cout<<"Y"<<endl;
        else
            cout<<"N"<<endl;
    }
    return 0;
}
(2)山理工--小鑫的城堡
从前有一个国王,他叫小鑫。有一天,他想建一座城堡,于是,设计师给他设计了好多简易图纸,主要是房间的连通的图纸。小鑫希望任意两个房间有且仅有一条路径可以相通。小鑫现在把设计图给你,让你帮忙判断设计图是否符合他的想法。比如下面的例子,第一个是符合条件的,但是,第二个不符合,因为从5到4有两条路径(5-3-4和5-6-4)。

多组输入,每组第一行包含一个整数m(m < 100000),接下来m行,每行两个整数,表示了一条通道连接的两个房间的编号。房间的编号至少为1,且不超过100000。
每组数据输出一行,如果该城堡符合小鑫的想法,那么输出"Yes",否则输出"No"。
————————————————我是分界线君嘤嘤嘤————————————————————————————————————————
思路:首先题意就是给出的所有点是否形成一个两两之间只有一条路的联通整体,而且不能存在环,对于判断是否有一个联通块可以用并查集实现,对于两两之间是否只有一条道路,可以用点数m=k(边数)+1来判断。
代码:
#include <iostream>
#include<bits/stdc++.h>
using namespace std;
int pre[100000];
bool num[100000];
bool judge[100000];
int finded(int a)
{
    int r=a;
    while(r!=pre[r])
        r=pre[r];
    int i=a,j;
    while(i!=r)
    {
        j=pre[i];
        pre[i]=r;
        i=j;
    }
    return r;
}
void join(int x,int y)
{
    int fx=finded(x);
    int fy=finded(y);
    if(fx!=fy)
        pre[fx]=fy;
}
int main()
{
    int m;
    while(cin>>m)
    {
        int pp=m;
        memset(num,0,sizeof(num));
        memset(judge,0,sizeof(judge));
        for(int i=1;i<=100000;i++)
            pre[i]=i;
        int sum=0;
        while(m--)
        {
            int a,b;
            cin>>a>>b;
            join(a,b);
            if(a>sum)
                sum=a;
            if(b>sum)
                sum=b;
            judge[a]=1;
            judge[b]=1;
        }
        int c=0,k=0;
        for(int i=1;i<=sum;i++)
        {
            if(judge[i]==1)
                k++;
            if(judge[i]&&pre[i]==i)
                c++;
        }
        if(c==1&&k==pp+1)//这里容易错,原来写的k==m+1,忘记了m--,好菜啊。//如果c等于1,则说明途中路全通,当路不全通时,c会大于1.                                                                                         //当然c=1;只是其中一个条件,因为当图中点与点之间都联通时,                                                                                       //假设其中有两个点之间有2条路可通,此时的c也等于1,但不满                                                                                       //足"任意两个点有且仅有一条路径可以相通"这一条件,所以还需                                                                                        //加上 k == m + 1 这一条件,(字母含义详见代码)
            cout<<"Yes"<<endl;
        else
            cout<<"No"<<endl;
    }
    return 0;
}
(3)poj2236  Wireless Network

传送门:点击打开链接

题意:有n台计算机,给出他们的坐标,只有距离小于等于d的才可以直接通信,若a与b可通信,b与c可通信,则a与c可通信,以此类推。O x表示维修x号计算机可以通信,S x y表示询问xy之间是否可以通信,是输出SUCCESS,否则输出FAIL。

思路:O时循环所有计算机寻找建立通信(同一集合的+可以直接通信的),不循环的话会漏掉后面直接通信的!

代码:

#include <iostream>
#include<cstring>
#include<cmath>
#include<cstdio>
using namespace std;
bool judge[1005];
int pre[1005];
int ranked[1005];
int N,d;
struct Node
{
    int x;int y;
};
Node node[1005];
int fined(int x)
{
    int v=x;
    while(pre[v]!=v)v=pre[v];
    int i=x,j;
    while(i!=v)
    {
        j=pre[i];
        pre[i]=v;
        i=j;
    }
    return v;
//      return pre[x]==x?x:pre[x]=fined(pre[x]);//递归运行
}
void join(int a,int b)
{
    int fx=fined(a);
    int fy=fined(b);
    if(fx!=fy)
    {
        if(ranked[fx]<ranked[fy])
        {
            pre[fx]=fy;
        }
        else
        {
            pre[fy]=fx;
            if(ranked[fx]==ranked[fy])
                ranked[fx]++;
        }
    }
}
int main()
{
    scanf("%d%d",&N,&d);
    memset(judge,0,sizeof(judge));
    memset(ranked,0,sizeof(ranked));
    for(int i=1;i<=N;i++)
        pre[i]=i;
    for(int i=1;i<=N;i++)
    {
        scanf("%d%d",&node[i].x,&node[i].y);//直接输入
//        node[i].x=dx;
//        node[i].y=dy;
    }
    char instruct[10];
    getchar();
    while(scanf("%s",instruct)!=EOF)
    {
        if(instruct[0]=='O')
        {
            int computer;
            scanf("%d",&computer);
            judge[computer]=1;
            for(int i=1;i<=N;i++)//必须全都循环一边,防止后面能直接交流的没join上!只join上了前面同父亲的!
            {
                if(i!=computer&&judge[i]&&((node[i].x-node[computer].x)*(node[i].x-node[computer].x)+(node[i].y-node[computer].y)*(node[i].y-node[computer].y))<=d*d)
                {
                    join(i,computer);
                    //break;
                }
            }
        }
        else if(instruct[0]=='S')
        {
            int s,e;
            scanf("%d%d",&s,&e);
            int fs=fined(s);
            int fe=fined(e);
            if(fs!=fe)
                printf("FAIL\n");
            else
                printf("SUCCESS\n");
        }
    }
    return 0;
}

二:最小生成树

1.最小生成树概念:在连通网的所有生成树中,所有边的代价和最小的生成树,称为最小生成树(使所有点联通+建立所有边的代价和最小)

2.最小生成树应用:要在n个城市之间铺设光缆,主要目标是要使这 n 个城市的任意两个之间都可以通信,但铺设光缆的费用很高,且各个城市之间铺设光缆的费用不同,因此另一个目标是要使铺设光缆的总费用最低。这就需要找到带权的最小生成树。

3.Prim(普里姆)算法(加点法)

(1)算法思想:以任意一点为树根出发,集合V是已经确定最短路的点集合,集合U是没有确立最短路的集合。初始时只有树根点在V中。

每一次循环就代表要修建一条最短路,到达没到达的点(U),我们只能从已经建成的局部最短路点集V中选取V中所有已确定点能到达的所有其他点里面最小的来建设,有点贪心思想,每次选取代价最小的路,逐渐完善点,知道恰好覆盖所有的点。

(2)核心代码:

int G[1000][1000];//邻接矩阵存图
int dis[1000];//存储 集合V 里面所有点的可到到达其他点总的最小距离
bool judge[1000];//判断该点是否已经加入最小点集合
int pre[1000];//记录每个点的前导,用于输出路径
int n,m;
int prim(int a)
{
    int sum=0;//记录路径总和
    int pos;//记录下一个加入V中的点位置
    int minn;
    judge[a]=1;
    pos=a;
    for(int i=1; i<=n-1; i++)
    {
        minn=INF;
        for(int j=1; j<=n; j++)
        {
            if(!judge[j]&&dis[j]<minn)//寻找集合V中能到达其他所有点的最短路径
            {
                pos=j;
                minn=dis[j];
            }
        }
        judge[pos]=1;//找到下一个加入V中的点
        sum+=minn;//加上最小路径
        cout<<"V"<<pre[pos]<<" -- "<<"V"<<pos<<" is "<<minn<<endl;
        for(int j=1; j<=n; j++)//从新加入的点更新V的最小距离dis,便于下次寻找最小点
        {
            if(dis[j]>G[pos][j]&&!judge[j])
            {
                dis[j]=G[pos][j];
                pre[j]=pos;//记录前导
            }
        }
    }
    return sum;
} 

(3)完整代码

#include <iostream>
#include<bits/stdc++.h>
using namespace std;
#define INF 0x3f3f3f
int G[1000][1000];//邻接矩阵存图
int dis[1000];//存储最小距离(总的集合U里的)
bool judge[1000];//判断该点是否已经加入最小点集合
int pre[1000];//记录每个点的前导,用于输出路径
int n,m;
int prim(int a)
{
    int sum=0;//记录总和
    int pos;//记录位置
    int minn;
    judge[a]=1;
    pos=a;
    for(int i=1; i<=n-1; i++)
    {
        minn=INF;
        for(int j=1; j<=n; j++)
        {
            if(!judge[j]&&dis[j]<minn)
            {
                pos=j;
                minn=dis[j];
            }
        }
        judge[pos]=1;
        sum+=minn;
        cout<<"V"<<pre[pos]<<" -- "<<"V"<<pos<<" is "<<minn<<endl;
        for(int j=1; j<=n; j++)
        {
            if(dis[j]>G[pos][j]&&!judge[j])
            {
                dis[j]=G[pos][j];
                pre[j]=pos;
            }
        }
    }
    return sum;
}
int main()
{
    int T;
    cin>>T;
    while(T--)
    {
        cin>>n>>m;
        for(int i=1; i<=n; i++)
        {
            for(int j=1; j<=n; j++)
            {
                if(i==j)
                    G[i][j]=0;
                else
                    G[i][j]=INF;
            }
        }
        memset(judge,0,sizeof(judge));
        for(int i=0; i<m; i++)
        {
            int a,b,c;
            cin>>a>>b>>c;
            G[a][b]=G[b][a]=c;
        }
        int s;
        cin>>s;
        for(int i=1; i<=n; i++)
        {
            pre[i]=s;
            dis[i]=G[s][i];
        }
        int k=prim(s);
        cout<<k<<endl;
    }
    return 0;
}

(4)邻接表优化

#include <iostream>
#include<bits/stdc++.h>
using namespace std;
#define INF 0x3f3f3f
int dis[1000];//存储最小距离(总的集合U里的)
bool judge[1000];//判断该点是否已经加入最小点集合
int pre[1000];//记录每个点的前导,用于输出
struct Node//记录终点和路径长度
{
    int e,v;
    Node(int a,int b):e(a),v(b) {}
};
int n,m;
vector<Node> num[1000];
int prim(int a)
{
    int sum=0;//记录总和
    int pos;//记录位置
    int minn;
    judge[a]=1;
    pos=a;
    for(int i=1; i<=n-1; i++)
    {
        minn=INF;
        for(int j=1; j<=n; j++)
        {
            if(!judge[j]&&dis[j]<minn)
            {
                pos=j;
                minn=dis[j];
            }
        }
        judge[pos]=1;
        sum+=minn;
        cout<<"V"<<pre[pos]<<" -- "<<"V"<<pos<<" is "<<minn<<endl;
        for(int i=0;i<num[pos].size();i++)
        {
            Node d=num[pos][i];
            if(dis[d.e]>d.v&&!judge[d.e])
            {
                dis[d.e]=d.v;
                pre[d.e]=pos;
            }
        }
    }
    return sum;
}
int main()
{
    int T;
    cin>>T;
    while(T--)
    {
        int s;
        cin>>n>>m>>s;
        for(int i=1;i<=n;i++)
        {
            dis[i]=INF;
            num[i].clear();
            pre[i]=s;
        }
        memset(judge,0,sizeof(judge));
        for(int i=0; i<m; i++)
        {
            int a,b,c;
            cin>>a>>b>>c;
            num[a].push_back(Node(b,c));
            num[b].push_back(Node(a,c));
        }
        for(int i=0;i<num[s].size();i++)
        {
            dis[num[s][i].e]=num[s][i].v;
        }
        int k=prim(s);
        cout<<k<<endl;
    }
    return 0;
}
四.

克鲁斯克尔(Kruskal)算法(加边法)

(1)算法思想:最小生成树最后一定是只有n-1条边!所以我们只要选取最小的n-1条边来吧n个点联通起来即可,但是注意不能产生回路,于是我们就用到了并查集!

  1. 记Graph中有v个顶点,e条边;
  2. 新建图Graphnew,Graphnew中拥有原图中的v个顶点,但没有边;
  3. 将原图Graph中所有e条边按权值从小到大排序;
  4. 循环:从权值最小的边开始,判断并添加每条边,直至添加了n-1条边:

注意:加边的条件是不产生回路!即要连接的两定点不在一个集合里面!(并查集判断是否可以加边)

(2)核心代码:

struct Node//建立边(起点+终点+权值)
{
    int s,e,v;
    bool operator <(const Node &n)const{//排序规则,由小到大权值
        return v<n.v;
    }
};
int Kruskal()
{
    sort(node,node+m);//排序权值
    int sizen=0;//建立路径条数
    int sum=0;//最小值
    for(int i=0;i<m&&sizen!=n-1;i++)
    {
        if(finded(node[i].s)!=finded(node[i].e))//如果一条边的起点终点不在同一个集合,即可连接成为一条最短路,并且把这两个集合join为一个
        {
            join(node[i].s,node[i].e);//join
            sum+=node[i].v;
            sizen++;
        }
    }
    if(sizen<n-1)return -1;//不足n-1
    return sum;
}

(3)完整代码:

#include <iostream>
#include<bits/stdc++.h>
using namespace std;
struct Node
{
    int s,e,v;
    bool operator <(const Node &n)const{
        return v<n.v;
    }
};
Node node[1000];
int pre[1000];
int ranked[1000];
int n,m;
int finded(int v)//查找
{
    int i=v;
    while(i!=pre[i])//return pre[v]=v?v:pre[v]=find(pre[v]);//递归
        i=pre[i];
    int j;
    while(v!=i)
    {
        j=pre[v];
        pre[v]=i;
        v=j;
    }
    return i;
}
void join(int a,int b)//合并
{
    int fx=finded(a);
    int fy=finded(b);
    if(fx!=fy)
    {
        if(ranked[fx]<ranked[fy])
        {
            pre[fx]=fy;
        }
        else
        {
            pre[fy]=fx;
            if(ranked[fx]==ranked[fy])
                ranked[fx]++;
        }
    }
}
int Kruskal()
{
    sort(node,node+m);
    int sizen=0;
    int sum=0;
    for(int i=0;i<m&&sizen!=n-1;i++)
    {
        if(finded(node[i].s)!=finded(node[i].e))
        {
            join(node[i].s,node[i].e);
            sum+=node[i].v;
            sizen++;
        }
    }
    if(sizen<n-1)return -1;
    return sum;
}
int main()
{
    cin>>n>>m;
    memset(ranked,0,sizeof(ranked));
    for(int i=1;i<=n;i++)
    {
        pre[i]=i;
    }
    for(int i=0;i<m;i++)
    {
        cin>>node[i].s>>node[i].e>>node[i].v;
    }
    int k=Kruskal();
    cout<<k<<endl;
    return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_40772692/article/details/79667455
今日推荐