数据结构和算法第一站-----union-find并查集

  • 输入:一列整数对,其中每个整数都表示一个某种类型的对象,一对整数p、q可以理解为“p和q是相连的”。问假设“相连”是一种等价关系即满足:
  • 自反性:p和p是相连的
  • 对称性:如果p和q是相连的那且q和p也是相连的
  • 传递性:如果p和q是相连的且q和r是相连的,那么p和r也是相连的。
  • 目的:当且仅当两个类相连他们才属于同一个等价类,目的是过滤掉序列中的所有等价类。
  • 当程序从输入中读取了整数对p和q,如果已知所有整数对都不能说明p和q是相连的,那么输出p和q,否者忽略p和q继续处理下一个输出。


    数据结构和算法第一站-----union-find并查集_第1张图片
    image
  • 为了达到所期望的效果,我们需要设计一个数据结构来保存程序已知的所有整数对的信息,并用它们来判断这一对新的对象是否相连。我们将这个问题通俗的叫做动态连通性问题
  • union-find算法的API
public class UF{
         UF(int N)           /*以整数标识(0到N-1)初始化N个触点*/
    void union(int p, int q) /*在p和q之间添加一条连接         */
     int find(int p)         /*p(0到N-1)所在的分量的标识符   */
 boolean connected(int p, int q)/*如果p和q存在同一分量中返回true*/
     int count()             /*连通分量的数量*/  
}
  • 如果两个触点不在同一分量中,union()操作将会将两个分量归并。
  • find()操作会返回触点所在分量的标识符。
  • connected()操作能够判断两个触点是否在同一个分量中。
  • count()方法返回所有连通分量的数量。
  • 那么我们所需要做的事情是
  1. 定义一种数据结构表示已知的连接;
  2. 基于此数据结构实现高效的union()、find()、connected()方法。
  • 我们可以用一个以触点为索引的数组id[]作为进本数据结构来表示所有分量。
  • 那么我们很容易想到如下对UF的实现:
import edu.princeton.cs.algs4.StdDraw;
import edu.princeton.cs.algs4.In;
import edu.princeton.cs.algs4.StdIn;
import edu.princeton.cs.algs4.StdOut;
public class UF{
    private int[] id;    /**分量id(以触点为索引) */
    private int count;   /** 分量数量 */
    public UF(int N){
        /** 初始化分量id数组 */
        count = N;
        id = new int[N];
        for(int i=0; i
  • ==注意==:代码中的输入StdIn等都是《算法》这本书的自带的库,java中是没有的,可以去官网下链接。algs4.jar直接导入工程中然后import就可以了,比较简单的也可以自己去实现。这本书是一本很枯燥的算法入门书,而且是用java实现的,但是依旧可以算得上是非常不错的书,需要耐心的去慢慢的读,千万不能因为太厚而放弃。切忌看一些很容易理解的算法书,太容易让自己一知半解了,刷题不必图快,但代码务必规范。
  • 那么并查集核心的内容就是去对find()和union方法的实现了
  • 回忆一下我们所需要做的事情:find()找到两个触点的分量就是找到他们的集合,union()将两个触点放到一个集合中去,那么我们非常容易会想到既然定义了一个以触点为索引的数组id[]来表示一个分量(集合),那么find(p)直接就返回id[p]就行了,而union(p, q)也不过就是将p的集合名改为q的集合而已,但是小心的是这么一种情况:
  • 存在一些触点是1、3、5、7...初始化的时候他们都自己属于自己的集合即:id[1]=1、id[3]=3、id[5]=5、id[7]=7。现在我们得到输入1、3,表示他俩是一个集合,于是我把3并到集合1中去即id[3]=1,然后又得到输入5、7那么,好id[7]=5,这时又得到输入1、5,诶 a[5]=1,这时候我们的数组变成了:id[1]=1 id[3]=1 id[5]=1 id[7]=5。这时候5和7的集合不一样了,这四个都应该在一个集合里面才对因此我们还需在合并的时候将集合中的每一个元素都并到另一个集合里面去,即当合并1、5的时候,把数组内容为[5]的触点id[5]=5,id[7]=5的内容都改写为1.那么算法可以这么来写:
//quick-find算法
public int find(){
        return id[p];
    }
public void union(int p, int q){
    int pID = find(p);
    int qID = find(q);
    /**如果他们在以个集合中就不用操作了 */
    if(pID == qID)    return;
    /**否则将p的集合中所有触点改到q的集合里去*/
    for(int i=0;i
  • 你可能注意到,因为集合名是数组的内容,触点才是下标,因此这个方法find()是非常快的,直接访问数组的下标就可以了,但是合并集合就要遍历这个数组才能行了而看最早的实现代码,union()这个操作是放在while(!StdIn.isEmpty())里面的那么时间复杂度就来到了N的平方了,显然对于几百万甚至更大的数据,这种算法是行不通的,我们需要寻找更好的算法。
  • 欸,有没有不用遍历一遍数组就可以union()的方法呢,上一个算法中的for循环一看就很别扭,直觉告诉我们不用for的方法肯定是有的。
  • 同样是那个1、3、5、7的问题,假设我直接删掉for循环,那么id[7]还是=5
    的,但是当我找7的集合的时候我不直接返回这个5了而再找一找id[5]是不是=5。一直找到他的根触点即1不就可以了吗。那么算法可以这么来写:
//quick-union算法
public int find(){
        while(p != id[p) p = id[p];
        return p;
    }
public void union(int p, int q){
    int pRoot = find(p);
    int qRoot = find(q);
    /**如果他们在以个集合中就不用操作了 */
    if(pRoot == qRoot)    return;
    /**否则将p的集合名改为q的集合*/
    id[pRoot] = qRoot;
    count--; /**合并完两个集合那么集合数量就减一了*/
}
  • 这个算法是不是比之前那个快呢,因为不是每一对输入都要遍历一遍数组,但是如果输入的每一个触点都离根触点比较远的话,比如id[11]和id[13]去合并,从id[11]=9去找id[9]=7然后id[7]=5、然后id[5]=3、然后id[3]=1.....这样一直找下去还是要花费一些时间的,虽然情况有所改观,但不难看出,最差的情况下时间复杂度依旧是平方级的。
  • 为了改善这种一直找下去的情况,我们可以想一些办法让这种情况尽量少的出现,显然的是我们不能直接去掉find()中的while,因为如果不这样找的话,必然要在union()中将集合中的每一个触点迁移去另一个集合,而为了找到这些触点,遍历整个数组是必然的。那么假设尝试着在合并的时候,去考虑到find()的问题。
  • 现在我们有两个集合,我用方框来表示集合的根触点,圆来表示其他触点。如下
    数据结构和算法第一站-----union-find并查集_第2张图片
    image
  • 这个时候我们要将1和13合并,有两个做法:
  • 将1合并到13上去


    数据结构和算法第一站-----union-find并查集_第3张图片
    image

    这样的话1上的每个触点在find()时都要多寻找几次
    数据结构和算法第一站-----union-find并查集_第4张图片
    image
  • 另一种做法是将13合并到1上去


    数据结构和算法第一站-----union-find并查集_第5张图片
    image

    由于13上面的触点更少所以增加的寻找次数显然是更少的:


    数据结构和算法第一站-----union-find并查集_第6张图片
    image
  • 如果我们每次都将少的一方合并到多的一方去,那么必然会减少这种多余的寻找。
  • 具体的算法分析就不做了简单的讲就是:如果用数来表示这些集合的话,我们将两个高度为n触点为2的n次幂的树合并到一起,变成了一个高度为n+1的树,那么可以推广证明这个算法性能是对数级的。具体算法如下:
//加权quick-union
import edu.princeton.cs.algs4.StdDraw;
import edu.princeton.cs.algs4.In;
import edu.princeton.cs.algs4.StdIn;
import edu.princeton.cs.algs4.StdOut;
public class UF{
    private int[] id;    /**父链连接数组(以触点为索引) */
    private int[] sz;    /** (由触点索引的)各个根节点所对应的分量大小*/
    private int count;   /** 连通分量的数量 */
    public UF(int N){
        /** 初始化分量id数组 */
        count = N;
        id = new int[N];
        for(int i=0; i
  • 值得一提的是,这个算法的find()部分很容易在find中使用路径压缩来进一步优化这样的话每次find()的成本都增加了,但union可以非常接近1。
 public int find(int p){
        int temp = p;
        while(p != id[p]) p = id[p];
        while(temp != id[p]){
        int tempId = id[temp];
        id[temp] = id[p];
        temp = tempId;
    }
     return p;
 }

你可能感兴趣的:(数据结构和算法第一站-----union-find并查集)