并查集入门

一、并查集可以做什么

对于一个新知识,我比较看重它可以用来做什么,可以处理什么问题。否则学了一大堆乱七八糟的,真正到用的时候反而不知所措,这就不好了。那么,并查集可以拿来做什么呢?让我们先通过一道题目来体会它的妙用吧!

Problem Description
某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府“畅通工程”的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问最少还需要建设多少条道路?
Input
测试输入包含若干测试用例。每个测试用例的第1行给出两个正整数,分别是城镇数目N ( < 1000 )和道路数目M;随后的M行对应M条道路,每行给出一对正整数,分别是该条道路直接连通的两个城镇的编号。为简单起见,城镇从1到N编号。
注意:两个城市之间可以有多条道路相通,也就是说
3 3
1 2
1 2
2 1
这种输入也是合法的
当N为0时,输入结束,该用例不被处理。

Output
对每个测试用例,在1行里输出最少还需要建设的道路数目。

此题来源于HDOJ1232

题目稍微有点长,让我们来把题目简化一下吧。与这题目等价的一个问法是:
平面上有N个点,这些点形成了M条线段,问还需要增加多少条线段使得任意两点间都可以连通?
这是一道具有几何色彩的题,如果你不会做的话,在草稿纸上写写画画也能摸索出个规律来。这里用语言来描述一下,假设我是其中一个点,如果我与另一点A相连,那么就说我与A是连通的;如果A又与B是连通的,那么我也就与B连通了,因为我可以通过A进而到达B;同样的,我、A、B还可以和更多的点连通,我们把这些互相连通的点构成的集合称为连通集。这里可以得到一个结论:若连通集的任何一个点与这个集合外的一点X连通,则这个连通集的所有点都与点X连通。这样问题就转化为了求最后连通集的数目了,为什么呢?如果最后剩下2个连通集,那么在2个连通集内各取一点,增加一条线段就可以了。如果剩下Y个连通集,每个连通集派出一个点,让它们连通的话,只需再增加(Y-1)条线段就好了。那么,如何确定最后剩下连通集的数目呢?
事实上,这是一道典型的用并查集解决的问题,但是如果你还不知道什么是并查集,问题也许就会困难许多。这类问题的特点是:①具有很多孤立的散点②部分散点之间会建立联系。具有这样特征的题目一般可以考虑用并查集求解。

二、什么是并查集

说了这么多,是时候隆重介绍一下我们的主角登场啦!并查集在维基百科上是这么定义的:

在计算机科学中,并查集是一种树型的数据结构,用于处理一些不交集(Disjoint Sets)的合并及查询问题。有一个联合-查找算法(union-find algorithm)定义了两个用于此数据结构的操作:
Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。
Union:将两个子集合并成同一个集合。

说白了,并查集正如其名,合并和查找。合并容易理解,从上面的题目中可以看到,散点之间用线段连起来,这就是合并;查找干嘛用的呢?上面遗留了一个问题:如何确定剩下的连通集的数目?没错,这里就要用到查找。它可以帮助我们确定一个点属于哪个集合。

三、并查集详解

下面,我们具体来看一下并查集是怎么运用到解决问题中去的。

  1. 刚开始有N个点,给它们编号1~N。对于其中的每个点i(1<i<N),用pre[i]表示i所属的集合,如pre[i]=1,就表示点i属于集合1。由于刚开始还没有线段产生,所有点都是孤立的一个点,所以每个点都构成一个连通集。为以示区别,我们让每个点i所属的集合编号就等于它自身的值。
//初始化
for(int i=1;i<=N;i++){
	pre[i]=i;	//点i属于集合i
}
  1. 接下来,有M条线段,每条线段都会合并两个点。那么怎么合并呢?很简单,只要把这两个点所属的连通集变成一样的就可以了,即若x所属的集合是pre[x],集合y所属的集合是pre[y],则只需让pre[x]=pre[y]或pre[y]=pre[x]就行了。如果2个点已经是属于同一个连通集了,就不用管他。那么问题来了,怎么知道2个点是不是属于同一个连通集呢?前面说到查找操作find(x)可以查找点X所属的集合,我们暂时先不管它,用着再说,大家知道它的用途就行了。
    合并点X和点Y
//合并
void union(int x,int y)                                                             
{
    int fx=find(x),fy=find(y);	//用fx表示点x所属的集合,fy表示点y所属的集合                    
    if(fx!=fy)                      	//如果点x和点y属于不同的集合,则合并
    pre[fx ]=fy;                		//把点x划到点y所属的集合中(pre[y]=fx也可以,只要能合并就行)
 }
  1. 上面也看到了,合并的操作也是要用到查找的。那么查找究竟是怎样实现的呢?我们再次回到查找操作的功能,即:可以查找点i所属的集合。刚开始有N个点,构成N个连通集,随着线段的加入,一些点被合并,也就是说,连通集内就会包含多个点,我们希望看到,对这个集合内的每个点进行查找,得到的结果都是同一个集合。这要怎么做呢?
    比如说,现在有3个点,点1属于集合1,点2属于集合2,点3属于集合3。union(1,2)得到点1属于集合2,点2属于集合2,点3属于集合3;union(2,3)得到点1属于集合2,点2属于集合3,点3属于集合3。比如说,现在有3个点,点1属于集合1,点2属于集合2,点3属于集合3。下面我们进行2步操作:①union(1,2)②union(2,3)现在1、2、3点都属于同一个集合了,对不对?那实际上如何呢?
123
初始所属集合123
union(1,2)后所属集合223
union(2,3)后所属集合233

可以看到,2次合并操作后1、2、3点本应在同一集合内,但实际上点1却与2、3在不同的集合。我们把union(x,y)用x⊆y表示,那么union(1,2)就是1⊆2,union(2,3)就是2⊆3.由数学知识可知,1⊆3。即是说,经过2次合并后,3个点都在集合3内。因而,我们可以用集合3作为这3个点所属的集合。
在这里插入图片描述
有了上面的小例子,我们大概可以知道,对于一个连通集内的所有点,可以用最外层集合作为它们共同的集合。对于每个点进行查找操作,返回的也是最外层的集合,这样就能保证同一连通集内的所有点都属于同一集合。还有一个要注意的地方是,在这些点中总会存在一个点,它的编号和它所属的集合编号是一样的,即编号为最外层集合集合编号的点。
下面看下具体实现:

//查找
int find(int x){                                                                                                      
    int r=x;	//用r暂存x,并用r来找最外层集合
    while ( pre[r] != r )  	//如果点r的编号和点r所属集合的编号不等,说明r不是最外层集合                                                                         
          r=pre[r];		//让r跳到一个更大的集合中去
    //循环结束后,r即为最外层集合
    //下面把上一步经过的点所属的集合都变成r(这一步叫做路径压缩,是为了缩短查找的时间)
    int i=x,j ;
    while(pre[i] != r ) { //如果i所属的集合不是最外层集合,则把它改为r                                                                                                
        j = pre[i];  	//用j暂存比i大一级的集合
        pre[i]= r ;	//把点i所属的集合改为r
         i=j;			//把j的值再交还给i,使循环得以继续
    }
    return r ;	//返回最大的集合
}

如此一来,不但找到了点x所在的最外层集合,还把点x到点r之间的点所属的集合都变成了最外层集合。

四、实战

最后,让我们完整的写出代码解决问题吧!

import java.util.Scanner;
public class Main{
	static final int MAX=1000;	//城镇最大数目1000
	static int []pre = new int[MAX+1];
	public static void main(String []args){
		Scanner in = new Scanner(System.in);
		int N = in.nextInt(); //N为城镇数目
		while(N!=0){
			int M = in.nextInt();//M为道路数目
			//初始化
			for(int i=1;i<=N;i++){
				pre[i]=i;
			}
			//合并,每输入一组数据,合并一组数据
			for(int i=0;i<M;i++){
				union(in.nextInt(),in.nextInt());
			}
			//cnt计数,利用每个连通集只有一个点的编号与所属连通集编号相同的特性
			int cnt=0;
			for(int i=1;i<=N;i++){
				if(i==pre[i]) cnt++;
			}
			//cnt个连通集连通需要cnt-1个线段
			System.out.println(cnt-1);
			N=in.nextInt();
		}
		in.close();
		}
		//合并
		public static void union(int x,int y){
			int fx=find(x),fy=find(y);
			if(fx!=fy) pre[x]=fy;
		}
		//查找
		public static int find(int x){
			int r=x;
			while(r!=pre[r]){
				r=pre[r];
			}
			int i=x,j;
			while(pre[i]!=r){
				j=pre[i];
				pre[i]=r;
				i=j;
			}
			return r;
		}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值