UF问题

温习以下解决方体的思路:
分析问题并找到解决问题的方案,将解决方案分成多个api。
则要解决的问题就变成了实现api。
(如果某个api难以实现,可能还需要重新改变api。往往好的情况是,在改变api时,会得到比一开始更多的有用的信息)

Union-find API

quick-find & quick-union

quick-find中:每次执行union(p,q)操作时,都要遍历整个并查集数组,将所有与p的标识符相同的结点的标识符都改为q。这样会导致每次调用union都要迭代整个数组,会很慢。 换来的是每次调用find会很快。
quick-union中:这种算法更像是一个树的结构,将结点一个连着一个直到根节点。

public void union(int p, int q) {// add connections between p and q
        int pId = find(p);
        int qId = find(q);
        if (pId != qId) {
            // this.a[q]=p;//ever fault. This way,we will lose the connection of q and a[q]
            // of before
            a[qId] = pId;
            --N;
            StdOut.printf("connecting %d and %d ... \n", p, q);
        } else {
            StdOut.printf("%d and %d has been connected!\n", p, q);
        }
    }

union操作:检测两个site(如,p和q)是否连接,如果不连接,则将这两个点进行连接。理所当然,与这两个点连接的点也产生了连接。我写的第一个版本,就犯了一个错误:仅仅时将p连接q,这样仅仅将p与q(以及连接q的site)连接到了一起,而没有将与p连接的site也连接进来。

改进版本则是将p的根节点连接q:

public void union(int p,int q){//add connections between p and q
        if(!connected(p, q)){
            //this.a[q]=p;//ever fault.  This way,we will lose the connection of q and a[q] of before
            int x=this.find(q);
            a[x]=p;
            --N;
            StdOut.printf("connecting %d and %d ... \n",p,q);
        }else{
            StdOut.printf("%d and %d has been connected!\n",p,q);
        }
    }

并查集又称为并查树,即同属于同一个components的元素,顺着路径最终会到达同一个site。然而,若每次union直接将两棵并查树连接在一起(不考虑各子树的尺寸),则可能会出现一种极端低效的情况:即所有的的结点都依次连接在同一个根节点上。 此时每次的并查复杂度为O(N)(一共N个结点) 。
为避免这种情况,使用quick-union的改进算法:weighted-quick-union

增加一个size数组记录每个并查树的尺寸(结点个数),保证每次都是将小树连接到大叔。这样一来,N个结点的并查树的最大结点深度不会超过log2N;

public void union(int p, int q) {// add connections between p and q
        int pId = find(p);
        int qId = find(q);
        if(size[pId]<size[qId]){
            a[pId]=qId;
            size[qId]++;
        }else{
            a[qId]=pId;
            size[pId]++;
        }
    
        --N;
        
    }

证明:

image.png

两棵并查树连接,比较小的树的所有结点的结点深度会增加1(大的树的结点深度皆不变),因此这里追踪小树的某一个结点x.
设小树的结点数为N,则x的深度每增加1,则总体树的大小至少为N=N*2;
若最终的并查数结点数为N,则可得此时深度增加了log2N;

image.png

ps:也可以使用数学归纳法来证明。

weight-quick-union算法使得并查树更趋扁平化(树的结点深度增长速度为logN)
find()访问数组的此时为log2N
而union操作需要调用两次find
这些基本操作都是基于并查树的深度,所有这样一来,并查算法的效率就被大大提高了。

还有办法进一步改进吗?
有。压缩路径

在find方法中添加一个循环:使得要找的结点到该结点的根节点之间的所有结点都连接到根节点
实现:

public int find(int p){
        int prep=p;
        while(a[p]!=p){
            p=a[p];
        }

        /* 
            path compressoin:to flatten the trees by make each node along the way link directly to the root of corresponding tree
            extra cost:increased the complexity of extra logN;
        */
        while(a[prep]!=prep){
            a[prep]=p;
            prep=a[prep];
        }
        return p;
    }

这样可以使得并查树变得更加扁平化。
带来的时间开销是多少呢?每一次find操作从logN变成了2logN。这又是一次很值得的算法改进。

算法性能实验

现在读取有625个数据的site结点对(即总共要执行625次连接操作)
要统计的数据:每次连接操作对数组的访问次数accessed,数组的当前平均访问次数t/total(t为当前的连接操作数,total为当前的总访问数组次数)
开始:
首先是quick-find
实验代码:

int N=StdIn.readInt();
        QuickFind uf=new QuickFind(N);
        int t=1;//record the number of time of connection.
        StdDraw.setXscale(0,N);
        StdDraw.setYscale(0,2*N+10);
        StdDraw.setPenRadius(.005);
        while(!StdIn.isEmpty()){
            int p=StdIn.readInt();
            int q=StdIn.readInt();
            if(!uf.connected(p, q)){
                StdOut.printf("connecting %d and %d ... \n",p,q);
                uf.union(p,q);
            }else{
                StdOut.printf("%d and %d has been connected!\n",p,q);
            }
            StdDraw.setPenColor(Color.DARK_GRAY);
            StdDraw.point(t, uf.accessed);
            StdOut.println("current accessed is"+uf.accessed);
            StdDraw.setPenColor(Color.red);
            StdDraw.point(t,uf.total/t);
            ++t;
        }

得到实验图像(横轴为当前执行的连接操作数,纵轴的黑色,红色分别为本次连接访问数组次数和平均访问数组次数)

image.png

qucik-union


image.png
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容