首先还是要描述一下我们所要解决的问题以及基本方法。该问题的来源是来自于普林斯顿算法课程第一讲的内容。为了通过这么课程的第一讲作业内容,本人搜集了一些网上资料以及亲自动手调试,才好不容易得到82分的成绩,勉强达到要求。具体不合格内容,后面有时间补上!
问题描述:渗流模型
模型如下图,由一个n*n的网格表示。
节点状态:网格中的每个节点有两种状态,即1)block封锁状态,2)open开放状态,图中黑色块为block状态,蓝色和白色块为open状态;3)full:某节点状态为open,且能找到一条路径,使得该节点与top(也就是第一行)相通,这个路径只能是四领域路径(也就是上下左右),这个节点状态就是full 。图中白色节点则为非full状态,蓝色节点为full状态。
系统状态:系统状态主要有两种,渗透(percolation)和非渗透状态。渗透状态为,能找到一条路径(当然是open状态的节点组成的!),使得top与bottom相通。如下右图,蓝色区域连通了top与bottom,所以使得系统为渗透状态!
问题:
1)设计并实现一个算法,判断节点以及系统的状态
2)设计并实现一个算法,判断一个n*n的系统需要多少比例的open状态节点,能使得系统为渗透状态
更多有关问题和模型的描述请见渗流模型
并查集算法概述
更多并查集算法的内容请见并查集算法
1)quick-find算法
该算法使用的数据结构类似于线性表中的顺序表结构,利用数组存储每个节点的连接号,如下图,id[]为存储信息的数组,其索引p代表的就是节点的索引,其值id[p]代表的就是节点p的连接号,在下图所示示例中,节点0、5、6连接号相同,故相连通,节点1、2、7相连通,节点3、4、8、9相连通。
若要进行新的连接操作,如将上图的0、1节点相连, 则将所有连接号为id[0]=0的节点的连接号置为id[1]=1
(当然你也可以规定将所有连接号为id[1]=1的节点的连接号置为id[0]=0),如下图所示。
优点:该算法使得确定两个节点是否有连通关系非常方便,算法复杂度仅仅为O(1)。
缺点:当新建一个新的节点连接关系时,需要遍历整个数组,于是该操作算法复杂度为O(n)。
2)quick-union算法
quick-union的方法类似于数据结构中数(tree)的方法。为了能将节点数与连接号关联起来,该算法仍以数组来存储节点的连接信息。每次建立新的连接,我们不是将所有相关的连接号全部进行调整,而是只调整树根(root)之间的连接关系即可。如下图所示,数组索引p表示节点索引,而数组值id[p]表示节点p的父节点。下例中,0、1、7、8均为孤立节点,而3的父节点为4,2、4的父节点和根节点都是9,所以3的根节点也为9,5的父节点和根节点为6。
现在要将3与5建立新的连接关系,则找到3和5的根节点,分别为9和6,所以将9的父节点置为6(当然你也可以将6的父节点置为9),得到
可以看到,当新建连接关系时,只有一个节点的连接号更新了!
优点:在新建连接关系时,只需要改变root的连接号
缺点:新建和查询均要找到root的连接号,这和树的高度有关系,如果树的瘦高的,那么效率就会明显降低!
3)weighted quick-union算法
quick-union的算法,在新建连接关系是,是按照约定的规则对root进行连接,这样难以避免产生一个瘦高的树。很自然的想法就是将小的树连接到大的树上。如下图所示,左为按规则连接,右为weighted方法,如此一来,找到root节点的效率将大大提高!
4)路径压缩的 weighted quick-union算法
若每个节点都直接连接到根节点上,那么查找的算法复杂度就接近O(1)
实际上,在使用find方法查找节点的root时,既可以调整树的大小。
模特卡罗随机模拟
通过大量的随机测验,得到近似答案!
路经压缩的weighted quick-union算法实现(源程序)
UF.java
/** * union-find类,使用的路径压缩的weighted quike-union方法 * Created by lps on 2017/1/23. */ public class UF { private int[] id;//索引数组 private int[] sz;//存储连接块的大小(带权重的union-find算法) private int count;//链接块的个数 /** * 构造函数,初始化id数组 * @param N 数组长度 */ public UF(int N) { id = new int[N]; sz = new int[N]; for(int i = 0;i < N;i ++) { id[i] = i;//每个元素的初始根节点是它自己 sz[i] = 1;//初始情况,每个连接块大小均为1 } count = N;//每一块暂时都是独立的 } /** * 添加连接关系 * @param p 某节点p * @param q 某节点q */ public void union(int p, int q) { int pRoot = find(p);//找到根节点 int qRoot = find(q); if(pRoot == qRoot)//已经连接则直接返回 return; if(sz[pRoot] < sz[qRoot]) { id[pRoot] = qRoot; sz[qRoot] += sz[pRoot]; } else { id[qRoot] = pRoot; sz[pRoot] += sz[qRoot]; } count --;//连接后少一个连接块 } /** * 找到p的根节点 * @param p 某节点p * @return 节点p的根节点 */ public int find(int p) { while(p != id[p]) { id[p] = id[id[p]];//路径压缩 p = id[p]; } return p; } /** * 判断是否两节点是否是连接的 * @param p 节点p * @param q 节点q * @return true 连接 false 不连接 */ public boolean connected(int p, int q) { return find(p) == find(q); } /** * 得到连接块个数 * @return count 连接块个数 */ public int count() { return count; } public void display() { System.out.print("id:"); for(int i = 0;i < id.length;i ++) { System.out.print(id[i]); System.out.print(" "); } System.out.println("end"); } public static void main(String[] args) { UF uf = new UF(10); uf.union(1,2); uf.union(1,3); uf.union(1,4); uf.union(0,9); uf.union(0,5); uf.union(0,1); //uf.display(); //System.out.println(uf.find(5)); } }percolation.java
/** * 这是渗透模型类,生成一个n*n的棋盘模型 * Created by lps on 2017/1/21. */ public class Percolation { private int count;//open site的个数 private int rows, cols;//渗透模型的行列数 private boolean[] isopen;//这是判断是否是开状态的数组 private UF uf,uf2; /** * 渗透模型的构造函数 * @param n 渗透模型棋盘大小为n*n 1 <= n <= n */ public Percolation(int n) { if(n <= 0)//抛出异常 throw new java.lang.IllegalArgumentException("n must be >= 1"); count = 0;//初始情况是相互独立的 rows = n;//保存行列值 cols = n; isopen = new boolean[n*n + 2]; uf = new UF(n*n + 2);//新建一个UF类,使用该类函数进行并查集算法 //注意:第一个为top(0),最后一个为bottom(n*n-1) //top与bottom连接,则为percolation uf2 = new UF(n*n + 1);//不加虚拟节点,为什么要这个数组请见后文 } private int index(int row, int col) { return (1 + (row - 1) * cols + (col - 1)); } /** * 对像素点进行开操作 * @param row 行号 * @param col 列号 */ public void open(int row, int col) { if(row < 0 || row > rows || col < 0 || col > cols)//越界 throw new IndexOutOfBoundsException("row or col must be [1,n]"); //当前元素在数组中的索引:1 + (row - 1) * cols + (col - 1) //如果已经开,则返回 if(isOpen(row, col)) return; //该位置置为开状态 isopen[index(row,col)] = true; //如果是顶部元素,则与顶点union if(row == 1) { isopen[0] = true;//顶部置为开状态 uf.union(index(row,col), 0); uf2.union(index(row,col), 0); } //如果是底部元素,则与底部点union if(row == rows) { isopen[rows * cols + 1] = true;//底部置为开状态 uf.union(index(row,col), rows * cols + 1); } //up if(row > 1 && isOpen(row-1,col)) { //System.out.print("up"); uf.union(index(row,col), index(row-1,col)); uf2.union(index(row,col), index(row-1,col)); } //down if(row < rows && isOpen(row+1,col)) { //System.out.print("down"); uf.union(index(row,col), index(row+1,col)); uf2.union(index(row,col), index(row+1,col)); } //left if(col > 1 && isOpen(row,col-1)) { //System.out.print("left"); uf.union(index(row,col), index(row,col-1)); uf2.union(index(row,col), index(row,col-1)); } //right if(col < cols && isOpen(row,col+1)) { //System.out.print("right"); uf.union(index(row,col), index(row,col+1)); uf2.union(index(row,col), index(row,col+1)); } count ++; } /** * 判断像素点是否开状态 * @param row 行号 * @param col 列号 * @return */ public boolean isOpen(int row, int col) { if(row < 0 || row > rows || col < 0 || col > cols)//越界 throw new IndexOutOfBoundsException("row or col must be [1,n]"); return isopen[index(row, col)]; } public boolean isFull(int row, int col) // is site (row, col) full? { if(row < 0 || row > rows || col < 0 || col > cols)//越界 throw new IndexOutOfBoundsException("row or col must be [1,n]"); if(isOpen(row,col)) return false; return uf2.connected(index(row,col), 0); } /** * 开状态的数量 * @return 开状态数量 */ public int numberOfOpenSites() { return count; } /** * 判断系统是否渗透 * @return */ public boolean percolates() { return uf.connected(0,rows * cols + 1); } /** * 主函数 */ public static void main(String[] args) { Percolation p = new Percolation(6); p.open(1, 6); //p.open(2, 3); //p.open(3, 3); //p.open(3, 1); System.out.println(p.isFull(1,6)); //System.out.println(p.myfind(3,1)); } }percolationStats.java
import java.util.ArrayList; import java.util.OptionalDouble; /** * Created by lps on 2017/1/25. */ public class PercolationStats { private ArrayList<Double> result;//结果数据数组 public PercolationStats(int n, int trials) // perform trials independent experiments on an n-by-n grid { if(n <= 0 || trials <= 0)//抛出异常 throw new java.lang.IllegalArgumentException("n and trials must be >= 1"); result = new ArrayList<>(100); double size = n*n; //开始进行trials次实验 for(int i = 0;i < trials;i ++) { //初始化一个渗流模型 Percolation p = new Percolation(n); //随机增加open site,直到系统达到渗流状态 while (!p.percolates()) { int index_row = (int)(1+Math.random()*(n)); int index_col = (int)(1+Math.random()*(n)); p.open(index_row,index_col); } double x = (double)p.numberOfOpenSites()/size; result.add(x); } } public double mean() // sample mean of percolation threshold { OptionalDouble avrg = result.stream().mapToDouble(r->r).average(); return avrg.getAsDouble(); } public double stddev() // sample standard deviation of percolation threshold { double sum = result.stream().mapToDouble(r->(r-this.mean())*(r-this.mean())).sum(); double avrg = sum / (double)(result.size()-1); return Math.sqrt(avrg); } public double confidenceLo() // low endpoint of 95% confidence interval { return (this.mean()-(1.96*this.stddev()/Math.sqrt((double)result.size()))); } public double confidenceHi() // high endpoint of 95% confidence interval { return (this.mean()+(1.96*this.stddev()/Math.sqrt((double)result.size()))); } public static void main(String[] args) // test client (described below) { int n = 100, T = 200; if(args.length == 2) { n = Integer.parseInt(args[0]); T = Integer.parseInt(args[1]); } PercolationStats ps = new PercolationStats(n,T); System.out.println(ps.mean()); System.out.println(ps.stddev()); System.out.println(ps.confidenceLo()); System.out.println(ps.confidenceHi()); } }
程序中注意事项
并查集算法关于渗透模型中虚拟节点的使用问题
说了这么大半天,终于要说到重点了。在上述代码实现中,为了获得网格内节点与top和bottom的关系,这里引入了虚拟节点,如下图所示,在n*n的网格顶部和底部引入顶部虚拟节点和底部虚拟节点,这样,判断系统是否是percolates的,实际上就是判断顶部虚拟节点和底部虚拟节点是否相连通即可。于是就有了代码段中的
UF uf = new UF(n*n+2),这就是因为加入了顶部和底部两个虚拟节点,也就是多加了两个节点。
然而,仅仅这样是会带来问题。不如考虑一个3*3的系统,我们先后open(1,3)(2,3)(3,3)(3,1),此时我们考虑(3,1)节点的状态。isOpen - true;isFull - true。对于open状态毋庸置疑,但是为什么(3,1)节点是full状态的呢,不难想到,该节点的full状态应该是false的!这就是仅用uf数组带来的问题。当open(3,3)后,(3,3)节点位于底层,所以底部虚拟节点将并入(3,3),而再open(3,1),该节点也是位于底层,所以该节点并入底部虚拟节点,也就是并入了(3,3),而实际上,(3,1)和(3,3)是毫无关系的!
实际上,这样处理就有悖于虚拟节点的“虚拟”二字,而成了实节点!
要想解决这个bug,很容易想到的一个方法就是在建立一个渗透模型,只因入顶部虚拟节点,而不引入底部节点,那么久阻断了因底部虚拟节点而造成了底层节点“假”连接!那么有人会问,顶部虚拟节点呢?的确,顶部也会存在相同的问题,但是它并不影响我们对节点状态(open,full)和系统状态(percolation),所以,我们可以不用管!
树大小计算(weighted方法实现)
树的大小即由树中的节点数来衡量,伴随着id数组,另外创建个树大小量数组sz(见程序),当新建两个节点的连接关系时,只需比较这两个节点根节点的sz大小即可。换句话说,每次更新sz数组,仅更新该树根节点root的sz大小,比较的时候,也用树的根节点的sz来代替该树的大小!如下述程序所示,
public void union(int p, int q) { int pRoot = find(p);//找到根节点 int qRoot = find(q); if(pRoot == qRoot)//已经连接则直接返回 return; if(sz[pRoot] < sz[qRoot]) { id[pRoot] = qRoot; sz[qRoot] += sz[pRoot]; } else { id[qRoot] = pRoot; sz[pRoot] += sz[qRoot]; } count --;//连接后少一个连接块 }
路径压缩实现
路径压缩实际上就是在weighted quick-union方法的基础上,利用find的方法查询的同时,改变树的连接方式-----尽量将节点都直接连接到根节点上。那么如何实现呢?从程序上来看,很简单,如下所示:
public int find(int p) { while(p != id[p]) { id[p] = id[id[p]];//路径压缩 p = id[p]; } return p; }
即在普通的方法上,添加了注释代码部分,不断地将p的爷爷节点赋给p的父节点,那么在找到p的根的同时,已经将根节点赋给了p的父节点(可以判断,在找到根节点的前一个循环,父节点和爷爷节点是同一个节点)。
关于程序中使用的数据结构
并查集程序中,选择的数据结构是顺序表----数组结构,而不是链表结构。理由有下:
1.模型规模固定:一旦模型初始化完成,那么该模型的节点个数就是不变的,所以使用数组可以避开不灵活的弊端;
2.find频繁:并查集算法需要通过find函数来找寻根节点,从而完成各个功能,而顺序表的查找效率为O(1),聊表的查找效率为O(n),顺序表更胜一筹;
该代码结果分析
该代码实现了基于并查集算法的渗透模型解算,但是实现得并不是很完美!在普林斯顿课程作业提交系统中也仅仅获得了82分(80分过关。。。对不起各位),详细评价见下:
可以看到,该代码正确性也并非100%对,而时间和空间效率还有很大的提升空间!希望后面能进一步加强!
并查集的应用
更多并查集应用的例子,见并查集(Union-Find) 应用举例 --- 基础篇
相关推荐
- 在这个实验中,使用了三种不同的并查集算法:`QuickFindUF`, `QuickUnionUF`, 和 `WeightedQuickUnionUF`。 - 并查集是一种数据结构,用于管理一组元素,并判断它们是否属于同一个集合。 - `QuickFindUF` 算法...
开发它是用于在UTF-8 Oracle实例中使用ASCII编码的Oracle 数据库中来正确的传输非ASCII字符。 Java模板语言 Beetl Beetl,是Bee Template Language的缩写,它绝不是简单的另外一种模板引擎,而是新一代的模板引擎,...
Java非对称加密源码实例 1个目标文件 摘要:Java源码,算法相关,非对称加密 Java非对称加密源程序代码实例,本例中使用RSA加密技术,定义加密算法可用 DES,DESede,Blowfish等。 设定字符串为“张三,你好,我是李四”...
开发它是用于在UTF-8 Oracle实例中使用ASCII编码的Oracle 数据库中来正确的传输非ASCII字符。 Java模板语言 Beetl Beetl,是Bee Template Language的缩写,它绝不是简单的另外一种模板引擎,而是新一代的模板引擎,...
开发它是用于在UTF-8 Oracle实例中使用ASCII编码的Oracle 数据库中来正确的传输非ASCII字符。 Java模板语言 Beetl Beetl,是Bee Template Language的缩写,它绝不是简单的另外一种模板引擎,而是新一代的模板引擎,...
开发它是用于在UTF-8 Oracle实例中使用ASCII编码的Oracle 数据库中来正确的传输非ASCII字符。 Java模板语言 Beetl Beetl,是Bee Template Language的缩写,它绝不是简单的另外一种模板引擎,而是新一代的模板引擎,...
开发它是用于在UTF-8 Oracle实例中使用ASCII编码的Oracle 数据库中来正确的传输非ASCII字符。 Java模板语言 Beetl Beetl,是Bee Template Language的缩写,它绝不是简单的另外一种模板引擎,而是新一代的模板引擎,...
开发它是用于在UTF-8 Oracle实例中使用ASCII编码的Oracle 数据库中来正确的传输非ASCII字符。 Java模板语言 Beetl Beetl,是Bee Template Language的缩写,它绝不是简单的另外一种模板引擎,而是新一代的模板引擎,...
开发它是用于在UTF-8 Oracle实例中使用ASCII编码的Oracle 数据库中来正确的传输非ASCII字符。 Java模板语言 Beetl Beetl,是Bee Template Language的缩写,它绝不是简单的另外一种模板引擎,而是新一代的模板引擎,...
开发它是用于在UTF-8 Oracle实例中使用ASCII编码的Oracle 数据库中来正确的传输非ASCII字符。 Java模板语言 Beetl Beetl,是Bee Template Language的缩写,它绝不是简单的另外一种模板引擎,而是新一代的模板引擎,...
开发它是用于在UTF-8 Oracle实例中使用ASCII编码的Oracle 数据库中来正确的传输非ASCII字符。 Java模板语言 Beetl Beetl,是Bee Template Language的缩写,它绝不是简单的另外一种模板引擎,而是新一代的模板引擎,...
开发它是用于在UTF-8 Oracle实例中使用ASCII编码的Oracle 数据库中来正确的传输非ASCII字符。 Java模板语言 Beetl Beetl,是Bee Template Language的缩写,它绝不是简单的另外一种模板引擎,而是新一代的模板引擎,...
开发它是用于在UTF-8 Oracle实例中使用ASCII编码的Oracle 数据库中来正确的传输非ASCII字符。 Java模板语言 Beetl Beetl,是Bee Template Language的缩写,它绝不是简单的另外一种模板引擎,而是新一代的模板引擎,...
开发它是用于在UTF-8 Oracle实例中使用ASCII编码的Oracle 数据库中来正确的传输非ASCII字符。 Java模板语言 Beetl Beetl,是Bee Template Language的缩写,它绝不是简单的另外一种模板引擎,而是新一代的模板引擎,...
开发它是用于在UTF-8 Oracle实例中使用ASCII编码的Oracle 数据库中来正确的传输非ASCII字符。 Java模板语言 Beetl Beetl,是Bee Template Language的缩写,它绝不是简单的另外一种模板引擎,而是新一代的模板引擎,...