• union-find算法


     1.背景

    《算法》一书中提到了关于算法的一些基本思想

    • 优秀的算法因为能够解决实际的问题而变得更为重要;
    • 高效算法的代码可以很简单;
    • 理解某个实现的性能特点是一项有趣而令人满足的挑战;
    • 在解决同一个问题的多种算法之间进行选择时,科学方法是一项重要工具;
    • 迭代式改进能够让算法效率越来越高;

    使用union-find算法解决连通性问题,所谓连通性问题就是在下图网络中可以看到有很多节点,节点与节点之间的连接对称为连通分量,要求编写程序判断网络中有多少组连通分量,随意给出两个节点要求判断这两个节点是否属于同一个分量。在连通图中有如下一些定义,假如一个节点p没有和任何其他节点相连,则此节点属于一个连通分量,如果一个节点p和节点q相连,则p-q为一个连通分量,如果p和q相连,q和r相连,则p-q-r为一个连通分量。

    2.算法分析

    ……

    3.算法实现

    为了解决此问题,需要先设计一个API来封装所需要的基本操作:初始化,连接两个节点,判断包含某个节点的分量,判断两个节点是否属于同一个分量之中,并且返回所有分量的个数。

    public class UF
    UF(int N)                                 整数标示N个节点
    void          union(int p, int q)         在pq之间添加一条连线,标示连接pq
    int           find(int p)                 节点p所在分量的标示符
    Boolean       connected(int p, int q)     如果p和q存在于同一个分量中则返回true
    int           count()                     连通分量的个数

    代码实现一:

    import java.util.Scanner;
    public class UF { private int[] id; //分量的id private int count; //分量的数量 //初始化分量id数组 public UF(int N) { count = N; id = new int[N]; for (int i = 0; i < N; i++) { id[i] = i; } } //返回分量的数量 public int count() { return count; } //判断两个节点是否属于同一个分量 public boolean connected(int p, int q) { return find(p) == find(q); } //求节点p所属的分量id public int find(int p) { return id[p]; } //连接节点pq //首先检查pq是否在同一个分量中,如果是则不做任何操作,否则要求p所在的连通分量中 //所有节点id必须相同,q所在的连通分量中所有节点id也相同但为另外的值,要将二者合 //二为一,则将q所在分量的所有节点id均变为p节点的id或相反 public void union(int p, int q) { int pId = find(p); int qId = find(q); if (pId == qId) { return; } for (int i = 0; i < id.length; i++) { if (id[i] == qId) { id[i] = pId; } } count--; } public static void main(String[] args) { @SuppressWarnings("resource") Scanner scanner = new Scanner(System.in); int N = scanner.nextInt(); UF uf = new UF(N); while (scanner.hasNext()) { int p = scanner.nextInt(); int q = scanner.nextInt(); if (uf.connected(p, q)) { continue; } uf.union(p, q); System.out.println(p + "--" + q); } System.out.println(uf.count() + " components"); } }
    使用这种find和union实现可以看到,对于find操作非常快,但对于union每一对节点输入都可能需要进行对整个id数组进行遍历,对于id数组的访问时间为O(N^2),所以当输入节点很多时用这个算法来统计连通分量就不行了,此时union的时间复杂度为O(N),find的复杂度为O(1)。

    代码实现二:

    public int find2(int p) {
    	while (p != id[p]) {
    		p = id[p];
    	}
    	return p;
    }
    
    public void union2(int p, int q) {
    	int pId = find2(p);
    	int qId = find2(q);
    	
    	if (pId == qId) {
    		return;
    	}
    	
    	id[qId] = pId;
    	count--;
    }

    使用这种find2和union2实现可以看到,对于调用到find2时,在某些情况下对于id数组访问的时间复杂度依然为O(N^2),此时find2的时间复杂度为树的高度N,union2的复杂度也为树的高度N。

    代码实现三:

    public class WeightedQuickUF {
    	private int[] id;     //节点的索引
    	private int[] w;      //每个根节点对应分量的大小
    	private int count;
    	
    	public WeightedQuickUF(int N) {
    		count = N;
    		id = new int[N];
    		w = new int[N];
    		for (int i = 0; i < N; i++) {
    			id[i] = i;
    			w[i] = 1;
    		}
    	}
    	
    	//返回分量的数量
    	public int count() {
    		return count;
    	}
    	
    	//判断两个节点是否属于同一个分量
    	public boolean connected(int p, int q) {
    		return find(p) == find(q);
    	}
    	
    	public int find(int p) {
    		while (p != id[p]) {
    			p = id[p];
    		}
    		return p;
    	}
    	
    	public void union(int p, int q) {
    		int pId = find(p);
    		int qId = find(q);
    		
    		if(pId == qId) {
    			return;
    		}
    		
    		if (w[pId] < w[qId]) {
    			id[pId] = qId;
    			w[qId] += w[pId];
    		} else {
    			id[qId] = pId;
    			w[pId] += w[qId];
    		}
    		count--;
    	}
    }

    对第二种实现做一些改进,使用加权的算法,使用加权算法保证在使用union时总是将小树的根节点连接到大树上,此时find和union的复杂度均为树的高度lgN,所以访问id数组的复杂度最坏情况为cMlgN,此时C为常数,M为连接数,N为节点数。

    ------------------------------- 问道,修仙 -------------------------------
  • 相关阅读:
    ionic新手教程第三课-在项目中使用requirejs分离controller文件和server文件
    高并发測试工具webbench
    内存管理详解
    linux command ---1
    运维方面的软件(系统)
    配置 php-fpm 监听的socket
    FastCgi与PHP-fpm之间是个什么样的关系
    分表处理设计思想和实现
    PHP 命名空间以及自动加载(自动调用的函数,来include文件)
    javascript closure
  • 原文地址:https://www.cnblogs.com/elvalad/p/4068275.html
Copyright © 2020-2023  润新知