第6章 并查集
6-1 并查集基础
一种很不一样的树形结构
连接问题 Connectivity Problem
网络中节点间的连接状态
网络是个抽象的概念:用户之间形成的网络
数据中的集合类实现
连接问题和路径问题
比路径问题要回答的问题少
和二分查找做比较
和select作比较
和堆作比较
6-2 Qucik Find
对于一组数据,主要支持两个动作:
union(p,q)
find(p)
用来回答一个问题
isConnected(p,q)
Quick find 下的Union时间复杂度O(n)
// 我们的第一版Union-Find
public class UnionFind1 {
private int[] id; // 我们的第一版Union-Find本质就是一个数组
private int count; // 数据个数
public UnionFind1(int n) {
count = n;
id = new int[n];
// 初始化, 每一个id[i]指向自己, 没有合并的元素
for (int i = 0; i < n; i++)
id[i] = i;
}
// 查找过程, 查找元素p所对应的集合编号
// O(1)复杂度
private int find(int p) {
assert p >= 0 && p < count;
return id[p];
}
// 查看元素p和元素q是否所属一个集合
// O(1)复杂度
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(n) 复杂度
public void unionElements(int p, int q) {
int pID = find(p);
int qID = find(q);
if (pID == qID)
return;
// 合并过程需要遍历一遍所有元素, 将两个元素的所属集合编号合并
for (int i = 0; i < count; i++)
if (id[i] == pID)
id[i] = qID;
}
}
// 测试并查集
public class UnionFindTestHelper {
// 测试第一版本的并查集, 测试元素个数为n
public static void testUF1( int n ){
UnionFind1 uf = new UnionFind1(n);
long startTime = System.currentTimeMillis();
// 进行n次操作, 每次随机选择两个元素进行合并操作
for( int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.unionElements(a,b);
}
// 再进行n次操作, 每次随机选择两个元素, 查询他们是否同属一个集合
for(int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.isConnected(a,b);
}
long endTime = System.currentTimeMillis();
// 打印输出对这2n个操作的耗时
System.out.println("UF1, " + 2*n + " ops, " + (endTime-startTime) + "ms");
}
}
public class Main {
// 测试UF1
public static void main(String[] args) {
// 使用10000的数据规模
int n = 10000;
// 虽然isConnected只需要O(1)的时间, 但由于union操作需要O(n)的时间
// 总体测试过程的算法复杂度是O(n^2)的
UnionFindTestHelper.testUF1(n);
}
}
6-3 Quick Union
并查集的另一种思路
将每一个元素,看做是一个节点
public class Main {
// 对比UF1和UF2的时间性能
public static void main(String[] args) {
// 使用10000的数据规模
int n = 10000;
// 虽然isConnected只需要O(1)的时间, 但由于union操作需要O(n)的时间
// 总体测试过程的算法复杂度是O(n^2)的
UnionFindTestHelper.testUF1(n);
// 对于UF2来说, 其时间性能是O(n*h)的, h为并查集表达的树的最大高度
// 这里严格来讲, h和logn没有关系, 不过大家可以简单这么理解
// 我们后续内容会对h进行优化, 总体而言, 这个h是远小于n的
// 所以我们实现的UF2测试结果远远好于UF1, n越大越明显:)
UnionFindTestHelper.testUF2(n);
}
}
// 我们的第一版Union-Find
public class UnionFind1 {
private int[] id; // 我们的第一版Union-Find本质就是一个数组
private int count; // 数据个数
public UnionFind1(int n) {
count = n;
id = new int[n];
// 初始化, 每一个id[i]指向自己, 没有合并的元素
for (int i = 0; i < n; i++)
id[i] = i;
}
// 查找过程, 查找元素p所对应的集合编号
private int find(int p) {
assert p >= 0 && p < count;
return id[p];
}
// 查看元素p和元素q是否所属一个集合
// O(1)复杂度
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(n) 复杂度
public void unionElements(int p, int q) {
int pID = find(p);
int qID = find(q);
if (pID == qID)
return;
// 合并过程需要遍历一遍所有元素, 将两个元素的所属集合编号合并
for (int i = 0; i < count; i++)
if (id[i] == pID)
id[i] = qID;
}
}
// 我们的第二版Union-Find
public class UnionFind2 {
// 我们的第二版Union-Find, 使用一个数组构建一棵指向父节点的树
// parent[i]表示第一个元素所指向的父节点
private int[] parent;
private int count; // 数据个数
// 构造函数
public UnionFind2(int count){
parent = new int[count];
this.count = count;
// 初始化, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合
for( int i = 0 ; i < count ; i ++ )
parent[i] = i;
}
// 查找过程, 查找元素p所对应的集合编号
// O(h)复杂度, h为树的高度
private int find(int p){
assert( p >= 0 && p < count );
// 不断去查询自己的父亲节点, 直到到达根节点
// 根节点的特点: parent[p] == p
while( p != parent[p] )
p = parent[p];
return p;
}
// 查看元素p和元素q是否所属一个集合
// O(h)复杂度, h为树的高度
public boolean isConnected( int p , int q ){
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(h)复杂度, h为树的高度
public void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if( pRoot == qRoot )
return;
parent[pRoot] = qRoot;
}
}
// 测试并查集
public class UnionFindTestHelper {
// 测试第一版本的并查集, 测试元素个数为n
public static void testUF1( int n ){
UnionFind1 uf = new UnionFind1(n);
long startTime = System.currentTimeMillis();
// 进行n次操作, 每次随机选择两个元素进行合并操作
for( int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.unionElements(a,b);
}
// 再进行n次操作, 每次随机选择两个元素, 查询他们是否同属一个集合
for(int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.isConnected(a,b);
}
long endTime = System.currentTimeMillis();
// 打印输出对这2n个操作的耗时
System.out.println("UF1, " + 2*n + " ops, " + (endTime-startTime) + "ms");
}
// 测试第二版本的并查集, 测试元素个数为n, 测试逻辑和之前是完全一样的
// 思考一下: 这样的冗余代码如何消除?
// 由于这个课程不是设计模式课程, 在这里就不过多引入相关的问题讲解了。留作给大家的思考题:)
public static void testUF2( int n ){
UnionFind2 uf = new UnionFind2(n);
long startTime = System.currentTimeMillis();
// 进行n次操作, 每次随机选择两个元素进行合并操作
for( int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.unionElements(a,b);
}
// 再进行n次操作, 每次随机选择两个元素, 查询他们是否同属一个集合
for(int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.isConnected(a,b);
}
long endTime = System.currentTimeMillis();
// 打印输出对这2n个操作的耗时
System.out.println("UF2, " + 2*n + " ops, " + (endTime-startTime) + "ms");
}
}
6-4 基于size的优化
public class Main {
// 对比UF1, UF2和UF3的时间性能
public static void main(String[] args) {
// 使用10000的数据规模
int n = 10000;
// 虽然isConnected只需要O(1)的时间, 但由于union操作需要O(n)的时间
// 总体测试过程的算法复杂度是O(n^2)的
UnionFindTestHelper.testUF1(n);
// 对于UF2来说, 其时间性能是O(n*h)的, h为并查集表达的树的最大高度
// 这里严格来讲, h和logn没有关系, 不过大家可以简单这么理解
// 我们后续内容会对h进行优化, 总体而言, 这个h是远小于n的
// 所以我们实现的UF2测试结果远远好于UF1, n越大越明显:)
UnionFindTestHelper.testUF2(n);
// 对于UF3来说, 其时间性能依然是O(n*h)的, h为并查集表达的树的最大高度
// 但由于UF3能更高概率的保证树的平衡, 所以性能更优
UnionFindTestHelper.testUF3(n);
}
}
// 我们的第一版Union-Find
public class UnionFind1 {
private int[] id; // 我们的第一版Union-Find本质就是一个数组
private int count; // 数据个数
public UnionFind1(int n) {
count = n;
id = new int[n];
// 初始化, 每一个id[i]指向自己, 没有合并的元素
for (int i = 0; i < n; i++)
id[i] = i;
}
// 查找过程, 查找元素p所对应的集合编号
private int find(int p) {
assert p >= 0 && p < count;
return id[p];
}
// 查看元素p和元素q是否所属一个集合
// O(1)复杂度
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(n) 复杂度
public void unionElements(int p, int q) {
int pID = find(p);
int qID = find(q);
if (pID == qID)
return;
// 合并过程需要遍历一遍所有元素, 将两个元素的所属集合编号合并
for (int i = 0; i < count; i++)
if (id[i] == pID)
id[i] = qID;
}
}
// 我们的第二版Union-Find
public class UnionFind2 {
// 我们的第二版Union-Find, 使用一个数组构建一棵指向父节点的树
// parent[i]表示第一个元素所指向的父节点
private int[] parent;
private int count; // 数据个数
// 构造函数
public UnionFind2(int count){
parent = new int[count];
this.count = count;
// 初始化, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合
for( int i = 0 ; i < count ; i ++ )
parent[i] = i;
}
// 查找过程, 查找元素p所对应的集合编号
// O(h)复杂度, h为树的高度
private int find(int p){
assert( p >= 0 && p < count );
// 不断去查询自己的父亲节点, 直到到达根节点
// 根节点的特点: parent[p] == p
while( p != parent[p] )
p = parent[p];
return p;
}
// 查看元素p和元素q是否所属一个集合
// O(h)复杂度, h为树的高度
public boolean isConnected( int p , int q ){
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(h)复杂度, h为树的高度
public void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if( pRoot == qRoot )
return;
parent[pRoot] = qRoot;
}
}
// 我们的第三版Union-Find
public class UnionFind3 {
private int[] parent; // parent[i]表示第一个元素所指向的父节点
private int[] sz; // sz[i]表示以i为根的集合中元素个数
private int count; // 数据个数
// 构造函数
public UnionFind3(int count){
parent = new int[count];
sz = new int[count];
this.count = count;
// 初始化, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合
for( int i = 0 ; i < count ; i ++ ){
parent[i] = i;
sz[i] = 1;
}
}
// 查找过程, 查找元素p所对应的集合编号
// O(h)复杂度, h为树的高度
private int find(int p){
assert( p >= 0 && p < count );
// 不断去查询自己的父亲节点, 直到到达根节点
// 根节点的特点: parent[p] == p
while( p != parent[p] )
p = parent[p];
return p;
}
// 查看元素p和元素q是否所属一个集合
// O(h)复杂度, h为树的高度
public boolean isConnected( int p , int q ){
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(h)复杂度, h为树的高度
public void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if( pRoot == qRoot )
return;
// 根据两个元素所在树的元素个数不同判断合并方向
// 将元素个数少的集合合并到元素个数多的集合上
if( sz[pRoot] < sz[qRoot] ){
parent[pRoot] = qRoot;
sz[qRoot] += sz[pRoot];
}
else{
parent[qRoot] = pRoot;
sz[pRoot] += sz[qRoot];
}
}
}
// 测试并查集
public class UnionFindTestHelper {
// 我们的测试类不允许创建实例
private UnionFindTestHelper(){}
// 测试第一版本的并查集, 测试元素个数为n
public static void testUF1( int n ){
UnionFind1 uf = new UnionFind1(n);
long startTime = System.currentTimeMillis();
// 进行n次操作, 每次随机选择两个元素进行合并操作
for( int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.unionElements(a,b);
}
// 再进行n次操作, 每次随机选择两个元素, 查询他们是否同属一个集合
for(int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.isConnected(a,b);
}
long endTime = System.currentTimeMillis();
// 打印输出对这2n个操作的耗时
System.out.println("UF1, " + 2*n + " ops, " + (endTime-startTime) + "ms");
}
// 测试第二版本的并查集, 测试元素个数为n, 测试逻辑和之前是完全一样的
// 思考一下: 这样的冗余代码如何消除?
// 由于这个课程不是设计模式课程, 在这里就不过多引入相关的问题讲解了。留作给大家的思考题:)
public static void testUF2( int n ){
UnionFind2 uf = new UnionFind2(n);
long startTime = System.currentTimeMillis();
// 进行n次操作, 每次随机选择两个元素进行合并操作
for( int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.unionElements(a,b);
}
// 再进行n次操作, 每次随机选择两个元素, 查询他们是否同属一个集合
for(int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.isConnected(a,b);
}
long endTime = System.currentTimeMillis();
// 打印输出对这2n个操作的耗时
System.out.println("UF2, " + 2*n + " ops, " + (endTime-startTime) + "ms");
}
// 测试第三版本的并查集, 测试元素个数为n, 测试逻辑和之前是完全一样的
public static void testUF3( int n ){
UnionFind3 uf = new UnionFind3(n);
long startTime = System.currentTimeMillis();
// 进行n次操作, 每次随机选择两个元素进行合并操作
for( int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.unionElements(a,b);
}
// 再进行n次操作, 每次随机选择两个元素, 查询他们是否同属一个集合
for(int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.isConnected(a,b);
}
long endTime = System.currentTimeMillis();
// 打印输出对这2n个操作的耗时
System.out.println("UF3, " + 2*n + " ops, " + (endTime-startTime) + "ms");
}
}
6-5 基于rank的优化
基于rank的优化
rank[i]表示根节点为i的树的高度
public class Main {
// 对比UF1, UF2, UF3和UF4的时间性能
public static void main(String[] args) {
// 使用1000000的数据规模
int n = 1000000;
// 虽然isConnected只需要O(1)的时间, 但由于union操作需要O(n)的时间
// 总体测试过程的算法复杂度是O(n^2)的
// 100万数据对于UF1来说太慢了, 不再测试
//UnionFindTestHelper.testUF1(n);
// 对于UF2来说, 其时间性能是O(n*h)的, h为并查集表达的树的最大高度
// 这里严格来讲, h和logn没有关系, 不过大家可以简单这么理解
// 我们后续内容会对h进行优化, 总体而言, 这个h是远小于n的
// 所以我们实现的UF2测试结果远远好于UF1, n越大越明显:)
// 100万数据对于UF2来说也是很慢的, 不再测试
//UnionFindTestHelper.testUF2(n);
// 对于UF3来说, 其时间性能依然是O(n*h)的, h为并查集表达的树的最大高度
// 但由于UF3能更高概率的保证树的平衡, 所以性能更优
UnionFindTestHelper.testUF3(n);
// UF4虽然相对UF3进行有了优化, 但优化的地方出现的情况较少,
// 所以性能更优表现的不明显, 甚至在一些数据下性能会更差
UnionFindTestHelper.testUF4(n);
}
}
// 我们的第一版Union-Find
public class UnionFind1 {
private int[] id; // 我们的第一版Union-Find本质就是一个数组
private int count; // 数据个数
public UnionFind1(int n) {
count = n;
id = new int[n];
// 初始化, 每一个id[i]指向自己, 没有合并的元素
for (int i = 0; i < n; i++)
id[i] = i;
}
// 查找过程, 查找元素p所对应的集合编号
private int find(int p) {
assert p >= 0 && p < count;
return id[p];
}
// 查看元素p和元素q是否所属一个集合
// O(1)复杂度
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(n) 复杂度
public void unionElements(int p, int q) {
int pID = find(p);
int qID = find(q);
if (pID == qID)
return;
// 合并过程需要遍历一遍所有元素, 将两个元素的所属集合编号合并
for (int i = 0; i < count; i++)
if (id[i] == pID)
id[i] = qID;
}
}
// 我们的第二版Union-Find
public class UnionFind2 {
// 我们的第二版Union-Find, 使用一个数组构建一棵指向父节点的树
// parent[i]表示第一个元素所指向的父节点
private int[] parent;
private int count; // 数据个数
// 构造函数
public UnionFind2(int count){
parent = new int[count];
this.count = count;
// 初始化, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合
for( int i = 0 ; i < count ; i ++ )
parent[i] = i;
}
// 查找过程, 查找元素p所对应的集合编号
// O(h)复杂度, h为树的高度
private int find(int p){
assert( p >= 0 && p < count );
// 不断去查询自己的父亲节点, 直到到达根节点
// 根节点的特点: parent[p] == p
while( p != parent[p] )
p = parent[p];
return p;
}
// 查看元素p和元素q是否所属一个集合
// O(h)复杂度, h为树的高度
public boolean isConnected( int p , int q ){
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(h)复杂度, h为树的高度
public void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if( pRoot == qRoot )
return;
parent[pRoot] = qRoot;
}
}
// 我们的第三版Union-Find
public class UnionFind3 {
private int[] parent; // parent[i]表示第一个元素所指向的父节点
private int[] sz; // sz[i]表示以i为根的集合中元素个数
private int count; // 数据个数
// 构造函数
public UnionFind3(int count){
parent = new int[count];
sz = new int[count];
this.count = count;
// 初始化, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合
for( int i = 0 ; i < count ; i ++ ){
parent[i] = i;
sz[i] = 1;
}
}
// 查找过程, 查找元素p所对应的集合编号
// O(h)复杂度, h为树的高度
private int find(int p){
assert( p >= 0 && p < count );
// 不断去查询自己的父亲节点, 直到到达根节点
// 根节点的特点: parent[p] == p
while( p != parent[p] )
p = parent[p];
return p;
}
// 查看元素p和元素q是否所属一个集合
// O(h)复杂度, h为树的高度
public boolean isConnected( int p , int q ){
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(h)复杂度, h为树的高度
public void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if( pRoot == qRoot )
return;
// 根据两个元素所在树的元素个数不同判断合并方向
// 将元素个数少的集合合并到元素个数多的集合上
if( sz[pRoot] < sz[qRoot] ){
parent[pRoot] = qRoot;
sz[qRoot] += sz[pRoot];
}
else{
parent[qRoot] = pRoot;
sz[pRoot] += sz[qRoot];
}
}
}
// 我们的第四版Union-Find
public class UnionFind4 {
private int[] rank; // rank[i]表示以i为根的集合所表示的树的层数
private int[] parent; // parent[i]表示第i个元素所指向的父节点
private int count; // 数据个数
// 构造函数
public UnionFind4(int count){
rank = new int[count];
parent = new int[count];
this.count = count;
// 初始化, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合
for( int i = 0 ; i < count ; i ++ ){
parent[i] = i;
rank[i] = 1;
}
}
// 查找过程, 查找元素p所对应的集合编号
// O(h)复杂度, h为树的高度
private int find(int p){
assert( p >= 0 && p < count );
// 不断去查询自己的父亲节点, 直到到达根节点
// 根节点的特点: parent[p] == p
while( p != parent[p] )
p = parent[p];
return p;
}
// 查看元素p和元素q是否所属一个集合
// O(h)复杂度, h为树的高度
public boolean isConnected( int p , int q ){
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(h)复杂度, h为树的高度
public void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if( pRoot == qRoot )
return;
// 根据两个元素所在树的元素个数不同判断合并方向
// 将元素个数少的集合合并到元素个数多的集合上
if( rank[pRoot] < rank[qRoot] ){
parent[pRoot] = qRoot;
}
else if( rank[qRoot] < rank[pRoot]){
parent[qRoot] = pRoot;
}
else{ // rank[pRoot] == rank[qRoot]
parent[pRoot] = qRoot;
rank[qRoot] += 1; // 此时, 我维护rank的值
}
}
}
// 测试并查集
public class UnionFindTestHelper {
// 我们的测试类不允许创建实例
private UnionFindTestHelper(){}
// 测试第一版本的并查集, 测试元素个数为n
public static void testUF1( int n ){
UnionFind1 uf = new UnionFind1(n);
long startTime = System.currentTimeMillis();
// 进行n次操作, 每次随机选择两个元素进行合并操作
for( int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.unionElements(a,b);
}
// 再进行n次操作, 每次随机选择两个元素, 查询他们是否同属一个集合
for(int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.isConnected(a,b);
}
long endTime = System.currentTimeMillis();
// 打印输出对这2n个操作的耗时
System.out.println("UF1, " + 2*n + " ops, " + (endTime-startTime) + "ms");
}
// 测试第二版本的并查集, 测试元素个数为n, 测试逻辑和之前是完全一样的
// 思考一下: 这样的冗余代码如何消除?
// 由于这个课程不是设计模式课程, 在这里就不过多引入相关的问题讲解了。留作给大家的思考题:)
public static void testUF2( int n ){
UnionFind2 uf = new UnionFind2(n);
long startTime = System.currentTimeMillis();
// 进行n次操作, 每次随机选择两个元素进行合并操作
for( int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.unionElements(a,b);
}
// 再进行n次操作, 每次随机选择两个元素, 查询他们是否同属一个集合
for(int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.isConnected(a,b);
}
long endTime = System.currentTimeMillis();
// 打印输出对这2n个操作的耗时
System.out.println("UF2, " + 2*n + " ops, " + (endTime-startTime) + "ms");
}
// 测试第三版本的并查集, 测试元素个数为n, 测试逻辑和之前是完全一样的
public static void testUF3( int n ){
UnionFind3 uf = new UnionFind3(n);
long startTime = System.currentTimeMillis();
// 进行n次操作, 每次随机选择两个元素进行合并操作
for( int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.unionElements(a,b);
}
// 再进行n次操作, 每次随机选择两个元素, 查询他们是否同属一个集合
for(int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.isConnected(a,b);
}
long endTime = System.currentTimeMillis();
// 打印输出对这2n个操作的耗时
System.out.println("UF3, " + 2*n + " ops, " + (endTime-startTime) + "ms");
}
// 测试第四版本的并查集, 测试元素个数为n, 测试逻辑和之前是完全一样的
public static void testUF4( int n ){
UnionFind4 uf = new UnionFind4(n);
long startTime = System.currentTimeMillis();
// 进行n次操作, 每次随机选择两个元素进行合并操作
for( int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.unionElements(a,b);
}
// 再进行n次操作, 每次随机选择两个元素, 查询他们是否同属一个集合
for(int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.isConnected(a,b);
}
long endTime = System.currentTimeMillis();
// 打印输出对这2n个操作的耗时
System.out.println("UF4, " + 2*n + " ops, " + (endTime-startTime) + "ms");
}
}
6-6 路径压缩
并查集的操作,时间复杂度近乎是O(1)的前驱
public class Main {
// 对比UF1, UF2, UF3, UF4和UF5的时间性能
public static void main(String[] args) {
// 使用1000000的数据规模
int n = 1000000;
// 虽然isConnected只需要O(1)的时间, 但由于union操作需要O(n)的时间
// 总体测试过程的算法复杂度是O(n^2)的
// 100万数据对于UF1来说太慢了, 不再测试
//UnionFindTestHelper.testUF1(n);
// 对于UF2来说, 其时间性能是O(n*h)的, h为并查集表达的树的最大高度
// 这里严格来讲, h和logn没有关系, 不过大家可以简单这么理解
// 我们后续内容会对h进行优化, 总体而言, 这个h是远小于n的
// 所以我们实现的UF2测试结果远远好于UF1, n越大越明显:)
// 100万数据对于UF2来说也是很慢的, 不再测试
//UnionFindTestHelper.testUF2(n);
// 对于UF3来说, 其时间性能依然是O(n*h)的, h为并查集表达的树的最大高度
// 但由于UF3能更高概率的保证树的平衡, 所以性能更优
UnionFindTestHelper.testUF3(n);
// UF4虽然相对UF3进行有了优化, 但优化的地方出现的情况较少,
// 所以性能更优表现的不明显, 甚至在一些数据下性能会更差
UnionFindTestHelper.testUF4(n);
// UF5虽然相对UF4进行有了优化, 但优化的地方出现的情况较少,
// 所以性能更优表现的不明显, 甚至在一些数据下性能会更差
UnionFindTestHelper.testUF5(n);
}
}
// 我们的第一版Union-Find
public class UnionFind1 {
private int[] id; // 我们的第一版Union-Find本质就是一个数组
private int count; // 数据个数
public UnionFind1(int n) {
count = n;
id = new int[n];
// 初始化, 每一个id[i]指向自己, 没有合并的元素
for (int i = 0; i < n; i++)
id[i] = i;
}
// 查找过程, 查找元素p所对应的集合编号
private int find(int p) {
assert p >= 0 && p < count;
return id[p];
}
// 查看元素p和元素q是否所属一个集合
// O(1)复杂度
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(n) 复杂度
public void unionElements(int p, int q) {
int pID = find(p);
int qID = find(q);
if (pID == qID)
return;
// 合并过程需要遍历一遍所有元素, 将两个元素的所属集合编号合并
for (int i = 0; i < count; i++)
if (id[i] == pID)
id[i] = qID;
}
}
// 我们的第二版Union-Find
public class UnionFind2 {
// 我们的第二版Union-Find, 使用一个数组构建一棵指向父节点的树
// parent[i]表示第一个元素所指向的父节点
private int[] parent;
private int count; // 数据个数
// 构造函数
public UnionFind2(int count){
parent = new int[count];
this.count = count;
// 初始化, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合
for( int i = 0 ; i < count ; i ++ )
parent[i] = i;
}
// 查找过程, 查找元素p所对应的集合编号
// O(h)复杂度, h为树的高度
private int find(int p){
assert( p >= 0 && p < count );
// 不断去查询自己的父亲节点, 直到到达根节点
// 根节点的特点: parent[p] == p
while( p != parent[p] )
p = parent[p];
return p;
}
// 查看元素p和元素q是否所属一个集合
// O(h)复杂度, h为树的高度
public boolean isConnected( int p , int q ){
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(h)复杂度, h为树的高度
public void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if( pRoot == qRoot )
return;
parent[pRoot] = qRoot;
}
}
// 我们的第三版Union-Find
public class UnionFind3 {
private int[] parent; // parent[i]表示第一个元素所指向的父节点
private int[] sz; // sz[i]表示以i为根的集合中元素个数
private int count; // 数据个数
// 构造函数
public UnionFind3(int count){
parent = new int[count];
sz = new int[count];
this.count = count;
// 初始化, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合
for( int i = 0 ; i < count ; i ++ ){
parent[i] = i;
sz[i] = 1;
}
}
// 查找过程, 查找元素p所对应的集合编号
// O(h)复杂度, h为树的高度
private int find(int p){
assert( p >= 0 && p < count );
// 不断去查询自己的父亲节点, 直到到达根节点
// 根节点的特点: parent[p] == p
while( p != parent[p] )
p = parent[p];
return p;
}
// 查看元素p和元素q是否所属一个集合
// O(h)复杂度, h为树的高度
public boolean isConnected( int p , int q ){
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(h)复杂度, h为树的高度
public void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if( pRoot == qRoot )
return;
// 根据两个元素所在树的元素个数不同判断合并方向
// 将元素个数少的集合合并到元素个数多的集合上
if( sz[pRoot] < sz[qRoot] ){
parent[pRoot] = qRoot;
sz[qRoot] += sz[pRoot];
}
else{
parent[qRoot] = pRoot;
sz[pRoot] += sz[qRoot];
}
}
}
// 我们的第四版Union-Find
public class UnionFind4 {
private int[] rank; // rank[i]表示以i为根的集合所表示的树的层数
private int[] parent; // parent[i]表示第i个元素所指向的父节点
private int count; // 数据个数
// 构造函数
public UnionFind4(int count){
rank = new int[count];
parent = new int[count];
this.count = count;
// 初始化, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合
for( int i = 0 ; i < count ; i ++ ){
parent[i] = i;
rank[i] = 1;
}
}
// 查找过程, 查找元素p所对应的集合编号
// O(h)复杂度, h为树的高度
private int find(int p){
assert( p >= 0 && p < count );
// 不断去查询自己的父亲节点, 直到到达根节点
// 根节点的特点: parent[p] == p
while( p != parent[p] )
p = parent[p];
return p;
}
// 查看元素p和元素q是否所属一个集合
// O(h)复杂度, h为树的高度
public boolean isConnected( int p , int q ){
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(h)复杂度, h为树的高度
public void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if( pRoot == qRoot )
return;
// 根据两个元素所在树的元素个数不同判断合并方向
// 将元素个数少的集合合并到元素个数多的集合上
if( rank[pRoot] < rank[qRoot] ){
parent[pRoot] = qRoot;
}
else if( rank[qRoot] < rank[pRoot]){
parent[qRoot] = pRoot;
}
else{ // rank[pRoot] == rank[qRoot]
parent[pRoot] = qRoot;
rank[qRoot] += 1; // 此时, 我维护rank的值
}
}
}
// 我们的第五版Union-Find
public class UnionFind5 {
// rank[i]表示以i为根的集合所表示的树的层数
// 在后续的代码中, 我们并不会维护rank的语意, 也就是rank的值在路径压缩的过程中, 有可能不在是树的层数值
// 这也是我们的rank不叫height或者depth的原因, 他只是作为比较的一个标准
// 关于这个问题,可以参考问答区:http://coding.imooc.com/learn/questiondetail/7287.html
private int[] rank;
private int[] parent; // parent[i]表示第i个元素所指向的父节点
private int count; // 数据个数
// 构造函数
public UnionFind5(int count){
rank = new int[count];
parent = new int[count];
this.count = count;
// 初始化, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合
for( int i = 0 ; i < count ; i ++ ){
parent[i] = i;
rank[i] = 1;
}
}
// 查找过程, 查找元素p所对应的集合编号
// O(h)复杂度, h为树的高度
private int find(int p){
assert( p >= 0 && p < count );
// path compression 1
while( p != parent[p] ){
parent[p] = parent[parent[p]];
p = parent[p];
}
return p;
// path compression 2, 递归算法
// if( p != parent[p] )
// parent[p] = find( parent[p] );
// return parent[p];
}
// 查看元素p和元素q是否所属一个集合
// O(h)复杂度, h为树的高度
public boolean isConnected( int p , int q ){
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(h)复杂度, h为树的高度
public void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if( pRoot == qRoot )
return;
// 根据两个元素所在树的元素个数不同判断合并方向
// 将元素个数少的集合合并到元素个数多的集合上
if( rank[pRoot] < rank[qRoot] ){
parent[pRoot] = qRoot;
}
else if( rank[qRoot] < rank[pRoot]){
parent[qRoot] = pRoot;
}
else{ // rank[pRoot] == rank[qRoot]
parent[pRoot] = qRoot;
rank[qRoot] += 1; // 此时, 我维护rank的值
}
}
}
// 测试并查集
public class UnionFindTestHelper {
// 我们的测试类不允许创建实例
private UnionFindTestHelper(){}
// 测试第一版本的并查集, 测试元素个数为n
public static void testUF1( int n ){
UnionFind1 uf = new UnionFind1(n);
long startTime = System.currentTimeMillis();
// 进行n次操作, 每次随机选择两个元素进行合并操作
for( int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.unionElements(a,b);
}
// 再进行n次操作, 每次随机选择两个元素, 查询他们是否同属一个集合
for(int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.isConnected(a,b);
}
long endTime = System.currentTimeMillis();
// 打印输出对这2n个操作的耗时
System.out.println("UF1, " + 2*n + " ops, " + (endTime-startTime) + "ms");
}
// 测试第二版本的并查集, 测试元素个数为n, 测试逻辑和之前是完全一样的
// 思考一下: 这样的冗余代码如何消除?
// 由于这个课程不是设计模式课程, 在这里就不过多引入相关的问题讲解了。留作给大家的思考题:)
public static void testUF2( int n ){
UnionFind2 uf = new UnionFind2(n);
long startTime = System.currentTimeMillis();
// 进行n次操作, 每次随机选择两个元素进行合并操作
for( int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.unionElements(a,b);
}
// 再进行n次操作, 每次随机选择两个元素, 查询他们是否同属一个集合
for(int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.isConnected(a,b);
}
long endTime = System.currentTimeMillis();
// 打印输出对这2n个操作的耗时
System.out.println("UF2, " + 2*n + " ops, " + (endTime-startTime) + "ms");
}
// 测试第三版本的并查集, 测试元素个数为n, 测试逻辑和之前是完全一样的
public static void testUF3( int n ){
UnionFind3 uf = new UnionFind3(n);
long startTime = System.currentTimeMillis();
// 进行n次操作, 每次随机选择两个元素进行合并操作
for( int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.unionElements(a,b);
}
// 再进行n次操作, 每次随机选择两个元素, 查询他们是否同属一个集合
for(int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.isConnected(a,b);
}
long endTime = System.currentTimeMillis();
// 打印输出对这2n个操作的耗时
System.out.println("UF3, " + 2*n + " ops, " + (endTime-startTime) + "ms");
}
// 测试第四版本的并查集, 测试元素个数为n, 测试逻辑和之前是完全一样的
public static void testUF4( int n ){
UnionFind4 uf = new UnionFind4(n);
long startTime = System.currentTimeMillis();
// 进行n次操作, 每次随机选择两个元素进行合并操作
for( int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.unionElements(a,b);
}
// 再进行n次操作, 每次随机选择两个元素, 查询他们是否同属一个集合
for(int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.isConnected(a,b);
}
long endTime = System.currentTimeMillis();
// 打印输出对这2n个操作的耗时
System.out.println("UF4, " + 2*n + " ops, " + (endTime-startTime) + "ms");
}
// 测试第五版本的并查集, 测试元素个数为n, 测试逻辑和之前是完全一样的
public static void testUF5( int n ){
UnionFind5 uf = new UnionFind5(n);
long startTime = System.currentTimeMillis();
// 进行n次操作, 每次随机选择两个元素进行合并操作
for( int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.unionElements(a,b);
}
// 再进行n次操作, 每次随机选择两个元素, 查询他们是否同属一个集合
for(int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.isConnected(a,b);
}
long endTime = System.currentTimeMillis();
// 打印输出对这2n个操作的耗时
System.out.println("UF4, " + 2*n + " ops, " + (endTime-startTime) + "ms");
}
}
补充1 使用相同的测试用例测试不同的UF实现
public class Main {
// 对比UF1, UF2, UF3, UF4, UF5和UF6的时间性能
// 在这里, 我们对于不同的UnionFind的实现, 使用相同的测试用例, 让测试结果更加准确
public static void main(String[] args) {
// 使用5000000的数据规模
int n = 5000000;
// 生成unionElements的测试用例
Pair<Integer, Integer>[] unionTest = new Pair[n];
for(int i = 0 ; i < n ; i ++){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
unionTest[i] = new Pair<Integer, Integer>(a, b);
}
// 生成isConnected的测试用例
Pair<Integer, Integer>[] connectTest = new Pair[n];
for(int i = 0 ; i < n ; i ++){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
connectTest[i] = new Pair<Integer, Integer>(a, b);
}
// 测试我们的UF1 ~ UF6
// 100万数据对于UF1和UF2来说太慢了, 不再测试
// UnionFind1 uf1 = new UnionFind1(n);
// UnionFindTestHelper.testUF("UF1", uf1, unionTest, connectTest);
//
// UnionFind2 uf2 = new UnionFind2(n);
// UnionFindTestHelper.testUF("UF2", uf2, unionTest, connectTest);
UnionFind3 uf3 = new UnionFind3(n);
UnionFindTestHelper.testUF("UF3", uf3, unionTest, connectTest);
UnionFind4 uf4 = new UnionFind4(n);
UnionFindTestHelper.testUF("UF4", uf4, unionTest, connectTest);
UnionFind5 uf5 = new UnionFind5(n);
UnionFindTestHelper.testUF("UF5", uf5, unionTest, connectTest);
UnionFind6 uf6 = new UnionFind6(n);
UnionFindTestHelper.testUF("UF6", uf6, unionTest, connectTest);
}
}
public class Pair<A, B> {
private A a;
private B b;
public Pair(A a, B b) {
this.a = a;
this.b = b;
}
public A a() {
return a;
}
public B b() {
return b;
}
}
// 我们设计一个UF的接口,让不同的UF实现具体实现这个接口
public interface UF {
public boolean isConnected( int p , int q );
public void unionElements(int p, int q);
}
// 我们的第一版Union-Find
public class UnionFind1 implements UF {
private int[] id; // 我们的第一版Union-Find本质就是一个数组
private int count; // 数据个数
public UnionFind1(int n) {
count = n;
id = new int[n];
// 初始化, 每一个id[i]指向自己, 没有合并的元素
for (int i = 0; i < n; i++)
id[i] = i;
}
// 查找过程, 查找元素p所对应的集合编号
private int find(int p) {
assert p >= 0 && p < count;
return id[p];
}
// 查看元素p和元素q是否所属一个集合
// O(1)复杂度
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(n) 复杂度
public void unionElements(int p, int q) {
int pID = find(p);
int qID = find(q);
if (pID == qID)
return;
// 合并过程需要遍历一遍所有元素, 将两个元素的所属集合编号合并
for (int i = 0; i < count; i++)
if (id[i] == pID)
id[i] = qID;
}
}
// 我们的第二版Union-Find
public class UnionFind2 implements UF {
// 我们的第二版Union-Find, 使用一个数组构建一棵指向父节点的树
// parent[i]表示第一个元素所指向的父节点
private int[] parent;
private int count; // 数据个数
// 构造函数
public UnionFind2(int count){
parent = new int[count];
this.count = count;
// 初始化, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合
for( int i = 0 ; i < count ; i ++ )
parent[i] = i;
}
// 查找过程, 查找元素p所对应的集合编号
// O(h)复杂度, h为树的高度
private int find(int p){
assert( p >= 0 && p < count );
// 不断去查询自己的父亲节点, 直到到达根节点
// 根节点的特点: parent[p] == p
while( p != parent[p] )
p = parent[p];
return p;
}
// 查看元素p和元素q是否所属一个集合
// O(h)复杂度, h为树的高度
public boolean isConnected( int p , int q ){
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(h)复杂度, h为树的高度
public void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if( pRoot == qRoot )
return;
parent[pRoot] = qRoot;
}
}
// 我们的第三版Union-Find
public class UnionFind3 implements UF {
private int[] parent; // parent[i]表示第一个元素所指向的父节点
private int[] sz; // sz[i]表示以i为根的集合中元素个数
private int count; // 数据个数
// 构造函数
public UnionFind3(int count){
parent = new int[count];
sz = new int[count];
this.count = count;
// 初始化, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合
for( int i = 0 ; i < count ; i ++ ){
parent[i] = i;
sz[i] = 1;
}
}
// 查找过程, 查找元素p所对应的集合编号
// O(h)复杂度, h为树的高度
private int find(int p){
assert( p >= 0 && p < count );
// 不断去查询自己的父亲节点, 直到到达根节点
// 根节点的特点: parent[p] == p
while( p != parent[p] )
p = parent[p];
return p;
}
// 查看元素p和元素q是否所属一个集合
// O(h)复杂度, h为树的高度
public boolean isConnected( int p , int q ){
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(h)复杂度, h为树的高度
public void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if( pRoot == qRoot )
return;
// 根据两个元素所在树的元素个数不同判断合并方向
// 将元素个数少的集合合并到元素个数多的集合上
if( sz[pRoot] < sz[qRoot] ){
parent[pRoot] = qRoot;
sz[qRoot] += sz[pRoot];
}
else{
parent[qRoot] = pRoot;
sz[pRoot] += sz[qRoot];
}
}
}
// 我们的第四版Union-Find
public class UnionFind4 implements UF {
private int[] rank; // rank[i]表示以i为根的集合所表示的树的层数
private int[] parent; // parent[i]表示第i个元素所指向的父节点
private int count; // 数据个数
// 构造函数
public UnionFind4(int count){
rank = new int[count];
parent = new int[count];
this.count = count;
// 初始化, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合
for( int i = 0 ; i < count ; i ++ ){
parent[i] = i;
rank[i] = 1;
}
}
// 查找过程, 查找元素p所对应的集合编号
// O(h)复杂度, h为树的高度
private int find(int p){
assert( p >= 0 && p < count );
// 不断去查询自己的父亲节点, 直到到达根节点
// 根节点的特点: parent[p] == p
while( p != parent[p] )
p = parent[p];
return p;
}
// 查看元素p和元素q是否所属一个集合
// O(h)复杂度, h为树的高度
public boolean isConnected( int p , int q ){
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(h)复杂度, h为树的高度
public void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if( pRoot == qRoot )
return;
// 根据两个元素所在树的元素个数不同判断合并方向
// 将元素个数少的集合合并到元素个数多的集合上
if( rank[pRoot] < rank[qRoot] ){
parent[pRoot] = qRoot;
}
else if( rank[qRoot] < rank[pRoot]){
parent[qRoot] = pRoot;
}
else{ // rank[pRoot] == rank[qRoot]
parent[pRoot] = qRoot;
rank[qRoot] += 1; // 此时, 我维护rank的值
}
}
}
// 我们的第五版Union-Find, 路径压缩使用迭代实现
public class UnionFind5 implements UF {
// rank[i]表示以i为根的集合所表示的树的层数
// 在后续的代码中, 我们并不会维护rank的语意, 也就是rank的值在路径压缩的过程中, 有可能不在是树的层数值
// 这也是我们的rank不叫height或者depth的原因, 他只是作为比较的一个标准
// 关于这个问题,可以参考问答区:http://coding.imooc.com/learn/questiondetail/7287.html
private int[] rank;
private int[] parent; // parent[i]表示第i个元素所指向的父节点
private int count; // 数据个数
// 构造函数
public UnionFind5(int count){
rank = new int[count];
parent = new int[count];
this.count = count;
// 初始化, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合
for( int i = 0 ; i < count ; i ++ ){
parent[i] = i;
rank[i] = 1;
}
}
// 查找过程, 查找元素p所对应的集合编号
// O(h)复杂度, h为树的高度
private int find(int p){
assert( p >= 0 && p < count );
// path compression 1
while( p != parent[p] ){
parent[p] = parent[parent[p]];
p = parent[p];
}
return p;
}
// 查看元素p和元素q是否所属一个集合
// O(h)复杂度, h为树的高度
public boolean isConnected( int p , int q ){
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(h)复杂度, h为树的高度
public void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if( pRoot == qRoot )
return;
// 根据两个元素所在树的元素个数不同判断合并方向
// 将元素个数少的集合合并到元素个数多的集合上
if( rank[pRoot] < rank[qRoot] ){
parent[pRoot] = qRoot;
}
else if( rank[qRoot] < rank[pRoot]){
parent[qRoot] = pRoot;
}
else{ // rank[pRoot] == rank[qRoot]
parent[pRoot] = qRoot;
rank[qRoot] += 1; // 此时, 我维护rank的值
}
}
}
// 我们的第六版Union-Find, 路径压缩使用递归实现
public class UnionFind6 implements UF {
// rank[i]表示以i为根的集合所表示的树的层数
// 在后续的代码中, 我们并不会维护rank的语意, 也就是rank的值在路径压缩的过程中, 有可能不在是树的层数值
// 这也是我们的rank不叫height或者depth的原因, 他只是作为比较的一个标准
// 关于这个问题,可以参考问答区:http://coding.imooc.com/learn/questiondetail/7287.html
private int[] rank;
private int[] parent; // parent[i]表示第i个元素所指向的父节点
private int count; // 数据个数
// 构造函数
public UnionFind6(int count){
rank = new int[count];
parent = new int[count];
this.count = count;
// 初始化, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合
for( int i = 0 ; i < count ; i ++ ){
parent[i] = i;
rank[i] = 1;
}
}
// 查找过程, 查找元素p所对应的集合编号
// O(h)复杂度, h为树的高度
private int find(int p){
assert( p >= 0 && p < count );
// path compression 2, 递归算法
if( p != parent[p] )
parent[p] = find( parent[p] );
return parent[p];
}
// 查看元素p和元素q是否所属一个集合
// O(h)复杂度, h为树的高度
public boolean isConnected( int p , int q ){
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(h)复杂度, h为树的高度
public void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if( pRoot == qRoot )
return;
// 根据两个元素所在树的元素个数不同判断合并方向
// 将元素个数少的集合合并到元素个数多的集合上
if( rank[pRoot] < rank[qRoot] ){
parent[pRoot] = qRoot;
}
else if( rank[qRoot] < rank[pRoot]){
parent[qRoot] = pRoot;
}
else{ // rank[pRoot] == rank[qRoot]
parent[pRoot] = qRoot;
rank[qRoot] += 1; // 此时, 我维护rank的值
}
}
}
// 测试并查集
public class UnionFindTestHelper {
// 我们的测试类不允许创建实例
private UnionFindTestHelper(){}
// 测试第一版本的并查集, 测试元素个数为n
public static void testUF1( int n ){
UnionFind1 uf = new UnionFind1(n);
long startTime = System.currentTimeMillis();
// 进行n次操作, 每次随机选择两个元素进行合并操作
for( int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.unionElements(a,b);
}
// 再进行n次操作, 每次随机选择两个元素, 查询他们是否同属一个集合
for(int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.isConnected(a,b);
}
long endTime = System.currentTimeMillis();
// 打印输出对这2n个操作的耗时
System.out.println("UF1, " + 2*n + " ops, " + (endTime-startTime) + "ms");
}
// 测试第二版本的并查集, 测试元素个数为n, 测试逻辑和之前是完全一样的
// 思考一下: 这样的冗余代码如何消除?
// 由于这个课程不是设计模式课程, 在这里就不过多引入相关的问题讲解了。留作给大家的思考题:)
public static void testUF2( int n ){
UnionFind2 uf = new UnionFind2(n);
long startTime = System.currentTimeMillis();
// 进行n次操作, 每次随机选择两个元素进行合并操作
for( int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.unionElements(a,b);
}
// 再进行n次操作, 每次随机选择两个元素, 查询他们是否同属一个集合
for(int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.isConnected(a,b);
}
long endTime = System.currentTimeMillis();
// 打印输出对这2n个操作的耗时
System.out.println("UF2, " + 2*n + " ops, " + (endTime-startTime) + "ms");
}
// 测试第三版本的并查集, 测试元素个数为n, 测试逻辑和之前是完全一样的
public static void testUF3( int n ){
UnionFind3 uf = new UnionFind3(n);
long startTime = System.currentTimeMillis();
// 进行n次操作, 每次随机选择两个元素进行合并操作
for( int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.unionElements(a,b);
}
// 再进行n次操作, 每次随机选择两个元素, 查询他们是否同属一个集合
for(int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.isConnected(a,b);
}
long endTime = System.currentTimeMillis();
// 打印输出对这2n个操作的耗时
System.out.println("UF3, " + 2*n + " ops, " + (endTime-startTime) + "ms");
}
// 测试第四版本的并查集, 测试元素个数为n, 测试逻辑和之前是完全一样的
public static void testUF4( int n ){
UnionFind4 uf = new UnionFind4(n);
long startTime = System.currentTimeMillis();
// 进行n次操作, 每次随机选择两个元素进行合并操作
for( int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.unionElements(a,b);
}
// 再进行n次操作, 每次随机选择两个元素, 查询他们是否同属一个集合
for(int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.isConnected(a,b);
}
long endTime = System.currentTimeMillis();
// 打印输出对这2n个操作的耗时
System.out.println("UF4, " + 2*n + " ops, " + (endTime-startTime) + "ms");
}
// 测试第五版本的并查集, 测试元素个数为n, 测试逻辑和之前是完全一样的
public static void testUF5( int n ){
UnionFind5 uf = new UnionFind5(n);
long startTime = System.currentTimeMillis();
// 进行n次操作, 每次随机选择两个元素进行合并操作
for( int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.unionElements(a,b);
}
// 再进行n次操作, 每次随机选择两个元素, 查询他们是否同属一个集合
for(int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.isConnected(a,b);
}
long endTime = System.currentTimeMillis();
// 打印输出对这2n个操作的耗时
System.out.println("UF5, " + 2*n + " ops, " + (endTime-startTime) + "ms");
}
// 测试第六版本的并查集, 测试元素个数为n, 测试逻辑和之前是完全一样的
public static void testUF6( int n ){
UnionFind6 uf = new UnionFind6(n);
long startTime = System.currentTimeMillis();
// 进行n次操作, 每次随机选择两个元素进行合并操作
for( int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.unionElements(a,b);
}
// 再进行n次操作, 每次随机选择两个元素, 查询他们是否同属一个集合
for(int i = 0 ; i < n ; i ++ ){
int a = (int)(Math.random()*n);
int b = (int)(Math.random()*n);
uf.isConnected(a,b);
}
long endTime = System.currentTimeMillis();
// 打印输出对这2n个操作的耗时
System.out.println("UF6, " + 2*n + " ops, " + (endTime-startTime) + "ms");
}
// 使用相同的测试数据测试UF的执行效率
public static void testUF(String ufName, UF uf, Pair<Integer,Integer>[] unionTest, Pair<Integer,Integer>[] connectTest){
long startTime = System.currentTimeMillis();
for( int i = 0 ; i < unionTest.length ; i ++ ){
int a = unionTest[i].a();
int b = unionTest[i].b();
uf.unionElements(a,b);
}
for(int i = 0 ; i < connectTest.length ; i ++ ){
int a = connectTest[i].a();
int b = connectTest[i].b();
uf.isConnected(a,b);
}
long endTime = System.currentTimeMillis();
System.out.print( ufName + " with " + unionTest.length + " unionElements ops, ");
System.out.print( connectTest.length + " isConnected ops, ");
System.out.println( (endTime-startTime) + "ms");
}
}
补充2 迭代和递归实现两种路径压缩的区别
public class Main {
public static void main(String[] args) {
// 为了能够方便地看出两种路径压缩算法的不同,我们只使用有5个元素的并查集进行试验
int n = 5;
UnionFind5 uf5 = new UnionFind5(n);
UnionFind6 uf6 = new UnionFind6(n);
// 我们将我们的并查集初始设置成如下的样子
// 0
// /
// 1
// /
// 2
// /
// 3
// /
// 4
for(int i = 1 ; i < n ; i ++){
uf5.parent[i] = i-1;
uf6.parent[i] = i-1;
}
// 现在, 我们对两个并查集调用find(4)操作
uf5.find(n-1);
uf6.find(n-1);
// 通过show, 我们可以看出, 使用迭代的路径压缩, 我们的并查集变成这个样子:
// 0
// / \
// 1 2
// / \
// 3 4
System.out.println("UF implements Path Compression by recursion:");
uf5.show();
System.out.println();
// 使用递归的路径压缩, 我们的并查集变成这个样子:
// 0
// / / \ \
// 1 2 3 4
System.out.println("UF implements Path Compression without recursion:");
uf6.show();
// 大家也可以调大n的值, 看看结果的不同:)
}
}
// 我们设计一个UF的接口,让不同的UF实现具体实现这个接口
public interface UF {
public boolean isConnected( int p , int q );
public void unionElements(int p, int q);
}
// 我们的第五版Union-Find, 路径压缩使用迭代实现
public class UnionFind5 implements UF {
// rank[i]表示以i为根的集合所表示的树的层数
// 在后续的代码中, 我们并不会维护rank的语意, 也就是rank的值在路径压缩的过程中, 有可能不在是树的层数值
// 这也是我们的rank不叫height或者depth的原因, 他只是作为比较的一个标准
// 关于这个问题,可以参考问答区:http://coding.imooc.com/learn/questiondetail/7287.html
private int[] rank;
public int[] parent; // parent[i]表示第i个元素所指向的父节点
// 后续, 我们要在外部操控并查集的数据, 在这里使用public
private int count; // 数据个数
// 构造函数
public UnionFind5(int count){
rank = new int[count];
parent = new int[count];
this.count = count;
// 初始化, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合
for( int i = 0 ; i < count ; i ++ ){
parent[i] = i;
rank[i] = 1;
}
}
// 查找过程, 查找元素p所对应的集合编号
// O(h)复杂度, h为树的高度
public int find(int p){
assert( p >= 0 && p < count );
// path compression 1
while( p != parent[p] ){
parent[p] = parent[parent[p]];
p = parent[p];
}
return p;
}
// 查看元素p和元素q是否所属一个集合
// O(h)复杂度, h为树的高度
public boolean isConnected( int p , int q ){
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(h)复杂度, h为树的高度
public void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if( pRoot == qRoot )
return;
// 根据两个元素所在树的元素个数不同判断合并方向
// 将元素个数少的集合合并到元素个数多的集合上
if( rank[pRoot] < rank[qRoot] ){
parent[pRoot] = qRoot;
}
else if( rank[qRoot] < rank[pRoot]){
parent[qRoot] = pRoot;
}
else{ // rank[pRoot] == rank[qRoot]
parent[pRoot] = qRoot;
rank[qRoot] += 1; // 此时, 我维护rank的值
}
}
// 打印输出并查集中的parent数据
public void show(){
for( int i = 0 ; i < count ; i ++ )
System.out.print( parent[i] + " ");
System.out.println();
}
}
// 我们的第六版Union-Find, 路径压缩使用递归实现
public class UnionFind6 implements UF {
// rank[i]表示以i为根的集合所表示的树的层数
// 在后续的代码中, 我们并不会维护rank的语意, 也就是rank的值在路径压缩的过程中, 有可能不在是树的层数值
// 这也是我们的rank不叫height或者depth的原因, 他只是作为比较的一个标准
// 关于这个问题,可以参考问答区:http://coding.imooc.com/learn/questiondetail/7287.html
private int[] rank;
public int[] parent; // parent[i]表示第i个元素所指向的父节点
// 后续, 我们要在外部操控并查集的数据, 在这里使用public
private int count; // 数据个数
// 构造函数
public UnionFind6(int count){
rank = new int[count];
parent = new int[count];
this.count = count;
// 初始化, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合
for( int i = 0 ; i < count ; i ++ ){
parent[i] = i;
rank[i] = 1;
}
}
// 查找过程, 查找元素p所对应的集合编号
// O(h)复杂度, h为树的高度
public int find(int p){
assert( p >= 0 && p < count );
// path compression 2, 递归算法
if( p != parent[p] )
parent[p] = find( parent[p] );
return parent[p];
}
// 查看元素p和元素q是否所属一个集合
// O(h)复杂度, h为树的高度
public boolean isConnected( int p , int q ){
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(h)复杂度, h为树的高度
public void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if( pRoot == qRoot )
return;
// 根据两个元素所在树的元素个数不同判断合并方向
// 将元素个数少的集合合并到元素个数多的集合上
if( rank[pRoot] < rank[qRoot] ){
parent[pRoot] = qRoot;
}
else if( rank[qRoot] < rank[pRoot]){
parent[qRoot] = pRoot;
}
else{ // rank[pRoot] == rank[qRoot]
parent[pRoot] = qRoot;
rank[qRoot] += 1; // 此时, 我维护rank的值
}
}
// 打印输出并查集中的parent数据
public void show(){
for( int i = 0 ; i < count ; i ++ )
System.out.print( parent[i] + " ");
System.out.println();
}
}
第7章 图的基础
7-1 图论基础
节点(Vertex)
边(Edge)
交通运输
社交网络
互联网
工作安排
脑区活动
程序状态执行
无向图
有向图
无向图是一种特殊的有向图
无权图(Unweighted Graph)
有权图(Weighted Graph)
图的连通性
简单图(Simple Graph)
自还变(self-loop)
平行边(parallel-edges)
7-2 图的表示
邻接矩阵(Adjacency Matrix)
邻接表(Adjacency Lists)
邻接表适合表示稀疏图(Sparse Graph)
邻接矩阵适合表示稠密图(Dense Graph)
// 稠密图 - 邻接矩阵
public class DenseGraph {
private int n; // 节点数
private int m; // 边数
private boolean directed; // 是否为有向图
private boolean[][] g; // 图的具体数据
// 构造函数
public DenseGraph( int n , boolean directed ){
assert n >= 0;
this.n = n;
this.m = 0; // 初始化没有任何边
this.directed = directed;
// g初始化为n*n的布尔矩阵, 每一个g[i][j]均为false, 表示没有任和边
// false为boolean型变量的默认值
g = new boolean[n][n];
}
public int V(){ return n;} // 返回节点个数
public int E(){ return m;} // 返回边的个数
// 向图中添加一个边
public void addEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
if( hasEdge( v , w ) )
return;
g[v][w] = true;
if( !directed )
g[w][v] = true;
m ++;
}
// 验证图中是否有从v到w的边
boolean hasEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
return g[v][w];
}
}
// 稀疏图 - 邻接表
public class SparseGraph {
private int n; // 节点数
private int m; // 边数
private boolean directed; // 是否为有向图
private Vector<Integer>[] g; // 图的具体数据
// 构造函数
public SparseGraph( int n , boolean directed ){
assert n >= 0;
this.n = n;
this.m = 0; // 初始化没有任何边
this.directed = directed;
// g初始化为n个空的vector, 表示每一个g[i]都为空, 即没有任和边
g = (Vector<Integer>[])new Vector[n];
for(int i = 0 ; i < n ; i ++)
g[i] = new Vector<Integer>();
}
public int V(){ return n;} // 返回节点个数
public int E(){ return m;} // 返回边的个数
// 向图中添加一个边
public void addEdge( int v, int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
g[v].add(w);
if( v != w && !directed )
g[w].add(v);
m ++;
}
// 验证图中是否有从v到w的边
boolean hasEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
for( int i = 0 ; i < g[v].size() ; i ++ )
if( g[v].elementAt(i) == w )
return true;
return false;
}
}
7-3 相邻点迭代器
// 稠密图 - 邻接矩阵
public class DenseGraph {
private int n; // 节点数
private int m; // 边数
private boolean directed; // 是否为有向图
private boolean[][] g; // 图的具体数据
// 构造函数
public DenseGraph( int n , boolean directed ){
assert n >= 0;
this.n = n;
this.m = 0; // 初始化没有任何边
this.directed = directed;
// g初始化为n*n的布尔矩阵, 每一个g[i][j]均为false, 表示没有任和边
// false为boolean型变量的默认值
g = new boolean[n][n];
}
public int V(){ return n;} // 返回节点个数
public int E(){ return m;} // 返回边的个数
// 向图中添加一个边
public void addEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
if( hasEdge( v , w ) )
return;
g[v][w] = true;
if( !directed )
g[w][v] = true;
m ++;
}
// 验证图中是否有从v到w的边
boolean hasEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
return g[v][w];
}
// 返回图中一个顶点的所有邻边
// 由于java使用引用机制,返回一个Vector不会带来额外开销,
public Iterable<Integer> adj(int v) {
assert v >= 0 && v < n;
Vector<Integer> adjV = new Vector<Integer>();
for(int i = 0 ; i < n ; i ++ )
if( g[v][i] )
adjV.add(i);
return adjV;
}
}
// 稀疏图 - 邻接表
public class SparseGraph {
private int n; // 节点数
private int m; // 边数
private boolean directed; // 是否为有向图
private Vector<Integer>[] g; // 图的具体数据
// 构造函数
public SparseGraph( int n , boolean directed ){
assert n >= 0;
this.n = n;
this.m = 0; // 初始化没有任何边
this.directed = directed;
// g初始化为n个空的vector, 表示每一个g[i]都为空, 即没有任和边
g = (Vector<Integer>[])new Vector[n];
for(int i = 0 ; i < n ; i ++)
g[i] = new Vector<Integer>();
}
public int V(){ return n;} // 返回节点个数
public int E(){ return m;} // 返回边的个数
// 向图中添加一个边
public void addEdge( int v, int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
g[v].add(w);
if( v != w && !directed )
g[w].add(v);
m ++;
}
// 验证图中是否有从v到w的边
boolean hasEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
for( int i = 0 ; i < g[v].size() ; i ++ )
if( g[v].elementAt(i) == w )
return true;
return false;
}
// 返回图中一个顶点的所有邻边
// 由于java使用引用机制,返回一个Vector不会带来额外开销,
public Iterable<Integer> adj(int v) {
assert v >= 0 && v < n;
return g[v];
}
}
7-4 图的算法框架
// 稠密图 - 邻接矩阵
public class DenseGraph implements Graph{
private int n; // 节点数
private int m; // 边数
private boolean directed; // 是否为有向图
private boolean[][] g; // 图的具体数据
// 构造函数
public DenseGraph( int n , boolean directed ){
assert n >= 0;
this.n = n;
this.m = 0; // 初始化没有任何边
this.directed = directed;
// g初始化为n*n的布尔矩阵, 每一个g[i][j]均为false, 表示没有任和边
// false为boolean型变量的默认值
g = new boolean[n][n];
}
public int V(){ return n;} // 返回节点个数
public int E(){ return m;} // 返回边的个数
// 向图中添加一个边
public void addEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
if( hasEdge( v , w ) )
return;
g[v][w] = true;
if( !directed )
g[w][v] = true;
m ++;
}
// 验证图中是否有从v到w的边
public boolean hasEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
return g[v][w];
}
// 显示图的信息
public void show(){
for( int i = 0 ; i < n ; i ++ ){
for( int j = 0 ; j < n ; j ++ )
System.out.print(g[i][j]+"\t");
System.out.println();
}
}
// 返回图中一个顶点的所有邻边
// 由于java使用引用机制,返回一个Vector不会带来额外开销,
public Iterable<Integer> adj(int v) {
assert v >= 0 && v < n;
Vector<Integer> adjV = new Vector<Integer>();
for(int i = 0 ; i < n ; i ++ )
if( g[v][i] )
adjV.add(i);
return adjV;
}
}
// 图的接口
public interface Graph {
public int V();
public int E();
public void addEdge( int v , int w );
boolean hasEdge( int v , int w );
void show();
public Iterable<Integer> adj(int v);
}
// 测试通过文件读取图的信息
public class Main {
public static void main(String[] args) {
// 使用两种图的存储方式读取testG1.txt文件
String filename = "E:\\workspace\\Play-with-Algorithms\\src\\com\\imooc\\ch7_4\\testG1.txt";
SparseGraph g1 = new SparseGraph(13, false);
ReadGraph readGraph1 = new ReadGraph(g1, filename);
System.out.println("test G1 in Sparse Graph:");
g1.show();
System.out.println();
DenseGraph g2 = new DenseGraph(13, false);
ReadGraph readGraph2 = new ReadGraph(g2 , filename );
System.out.println("test G1 in Dense Graph:");
g2.show();
System.out.println();
// 使用两种图的存储方式读取testG2.txt文件
filename = "E:\\workspace\\Play-with-Algorithms\\src\\com\\imooc\\ch7_4\\testG2.txt";
SparseGraph g3 = new SparseGraph(6, false);
ReadGraph readGraph3 = new ReadGraph(g3, filename);
System.out.println("test G2 in Sparse Graph:");
g3.show();
System.out.println();
DenseGraph g4 = new DenseGraph(6, false);
ReadGraph readGraph4 = new ReadGraph(g4, filename);
System.out.println("test G2 in Dense Graph:");
g4.show();
}
}
public class ReadGraph {
private Scanner scanner;
public ReadGraph(Graph graph, String filename){
readFile(filename);
try {
int V = scanner.nextInt();
if (V < 0)
throw new IllegalArgumentException("number of vertices in a Graph must be nonnegative");
assert V == graph.V();
int E = scanner.nextInt();
if (E < 0)
throw new IllegalArgumentException("number of edges in a Graph must be nonnegative");
for (int i = 0; i < E; i++) {
int v = scanner.nextInt();
int w = scanner.nextInt();
assert v >= 0 && v < V;
assert w >= 0 && w < V;
graph.addEdge(v, w);
}
}
catch (InputMismatchException e) {
String token = scanner.next();
throw new InputMismatchException("attempts to read an 'int' value from input stream, but the next token is \"" + token + "\"");
}
catch (NoSuchElementException e) {
throw new NoSuchElementException("attemps to read an 'int' value from input stream, but there are no more tokens available");
}
}
private void readFile(String filename){
assert filename != null;
try {
File file = new File(filename);
if (file.exists()) {
FileInputStream fis = new FileInputStream(file);
scanner = new Scanner(new BufferedInputStream(fis), "UTF-8");
scanner.useLocale(Locale.ENGLISH);
}
else
throw new IllegalArgumentException(filename + "doesn't exist.");
}
catch (IOException ioe) {
throw new IllegalArgumentException("Could not open " + filename, ioe);
}
}
}
// 稀疏图 - 邻接表
public class SparseGraph implements Graph {
private int n; // 节点数
private int m; // 边数
private boolean directed; // 是否为有向图
private Vector<Integer>[] g; // 图的具体数据
// 构造函数
public SparseGraph( int n , boolean directed ){
assert n >= 0;
this.n = n;
this.m = 0; // 初始化没有任何边
this.directed = directed;
// g初始化为n个空的vector, 表示每一个g[i]都为空, 即没有任和边
g = (Vector<Integer>[])new Vector[n];
for(int i = 0 ; i < n ; i ++)
g[i] = new Vector<Integer>();
}
public int V(){ return n;} // 返回节点个数
public int E(){ return m;} // 返回边的个数
// 向图中添加一个边
public void addEdge( int v, int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
g[v].add(w);
if( v != w && !directed )
g[w].add(v);
m ++;
}
// 验证图中是否有从v到w的边
public boolean hasEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
for( int i = 0 ; i < g[v].size() ; i ++ )
if( g[v].elementAt(i) == w )
return true;
return false;
}
// 显示图的信息
public void show(){
for( int i = 0 ; i < n ; i ++ ){
System.out.print("vertex " + i + ":\t");
for( int j = 0 ; j < g[i].size() ; j ++ )
System.out.print(g[i].elementAt(j) + "\t");
System.out.println();
}
}
// 返回图中一个顶点的所有邻边
// 由于java使用引用机制,返回一个Vector不会带来额外开销,
public Iterable<Integer> adj(int v) {
assert v >= 0 && v < n;
return g[v];
}
}
7-5 深度优先遍历和联通分量
// 求无权图的联通分量
public class Components {
Graph G; // 图的引用
private boolean[] visited; // 记录dfs的过程中节点是否被访问
private int ccount; // 记录联通分量个数
private int[] id; // 每个节点所对应的联通分量标记
// 图的深度优先遍历
void dfs( int v ){
visited[v] = true;
id[v] = ccount;
for( int i: G.adj(v) ){
if( !visited[i] )
dfs(i);
}
}
// 构造函数, 求出无权图的联通分量
public Components(Graph graph){
// 算法初始化
G = graph;
visited = new boolean[G.V()];
id = new int[G.V()];
ccount = 0;
for( int i = 0 ; i < G.V() ; i ++ ){
visited[i] = false;
id[i] = -1;
}
// 求图的联通分量
for( int i = 0 ; i < G.V() ; i ++ )
if( !visited[i] ){
dfs(i);
ccount ++;
}
}
// 返回图的联通分量个数
int count(){
return ccount;
}
// 查询点v和点w是否联通
boolean isConnected( int v , int w ){
assert v >= 0 && v < G.V();
assert w >= 0 && w < G.V();
return id[v] == id[w];
}
}
// 稠密图 - 邻接矩阵
public class DenseGraph implements Graph{
private int n; // 节点数
private int m; // 边数
private boolean directed; // 是否为有向图
private boolean[][] g; // 图的具体数据
// 构造函数
public DenseGraph( int n , boolean directed ){
assert n >= 0;
this.n = n;
this.m = 0; // 初始化没有任何边
this.directed = directed;
// g初始化为n*n的布尔矩阵, 每一个g[i][j]均为false, 表示没有任和边
// false为boolean型变量的默认值
g = new boolean[n][n];
}
public int V(){ return n;} // 返回节点个数
public int E(){ return m;} // 返回边的个数
// 向图中添加一个边
public void addEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
if( hasEdge( v , w ) )
return;
g[v][w] = true;
if( !directed )
g[w][v] = true;
m ++;
}
// 验证图中是否有从v到w的边
public boolean hasEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
return g[v][w];
}
// 显示图的信息
public void show(){
for( int i = 0 ; i < n ; i ++ ){
for( int j = 0 ; j < n ; j ++ )
System.out.print(g[i][j]+"\t");
System.out.println();
}
}
// 返回图中一个顶点的所有邻边
// 由于java使用引用机制,返回一个Vector不会带来额外开销,
public Iterable<Integer> adj(int v) {
assert v >= 0 && v < n;
Vector<Integer> adjV = new Vector<Integer>();
for(int i = 0 ; i < n ; i ++ )
if( g[v][i] )
adjV.add(i);
return adjV;
}
}
// 图的接口
public interface Graph {
public int V();
public int E();
public void addEdge( int v , int w );
boolean hasEdge( int v , int w );
void show();
public Iterable<Integer> adj(int v);
}
// 测试图的联通分量算法
public class Main {
public static void main(String[] args) {
// TestG1.txt
String filename1 = "E:\\workspace\\Play-with-Algorithms\\src\\com\\imooc\\ch7_5\\testG1.txt";
SparseGraph g1 = new SparseGraph(13, false);
ReadGraph readGraph1 = new ReadGraph(g1, filename1);
Components component1 = new Components(g1);
System.out.println("TestG1.txt, Component Count: " + component1.count());
System.out.println();
// TestG2.txt
String filename2 = "E:\\workspace\\Play-with-Algorithms\\src\\com\\imooc\\ch7_5\\testG2.txt";
SparseGraph g2 = new SparseGraph(6, false);
ReadGraph readGraph2 = new ReadGraph(g2, filename2);
Components component2 = new Components(g2);
System.out.println("TestG2.txt, Component Count: " + component2.count());
}
}
public class ReadGraph {
private Scanner scanner;
public ReadGraph(Graph graph, String filename){
readFile(filename);
try {
int V = scanner.nextInt();
if (V < 0)
throw new IllegalArgumentException("number of vertices in a Graph must be nonnegative");
assert V == graph.V();
int E = scanner.nextInt();
if (E < 0)
throw new IllegalArgumentException("number of edges in a Graph must be nonnegative");
for (int i = 0; i < E; i++) {
int v = scanner.nextInt();
int w = scanner.nextInt();
assert v >= 0 && v < V;
assert w >= 0 && w < V;
graph.addEdge(v, w);
}
}
catch (InputMismatchException e) {
String token = scanner.next();
throw new InputMismatchException("attempts to read an 'int' value from input stream, but the next token is \"" + token + "\"");
}
catch (NoSuchElementException e) {
throw new NoSuchElementException("attemps to read an 'int' value from input stream, but there are no more tokens available");
}
}
private void readFile(String filename){
assert filename != null;
try {
File file = new File(filename);
if (file.exists()) {
FileInputStream fis = new FileInputStream(file);
scanner = new Scanner(new BufferedInputStream(fis), "UTF-8");
scanner.useLocale(Locale.ENGLISH);
}
else
throw new IllegalArgumentException(filename + "doesn't exist.");
}
catch (IOException ioe) {
throw new IllegalArgumentException("Could not open " + filename, ioe);
}
}
}
// 稀疏图 - 邻接表
public class SparseGraph implements Graph {
private int n; // 节点数
private int m; // 边数
private boolean directed; // 是否为有向图
private Vector<Integer>[] g; // 图的具体数据
// 构造函数
public SparseGraph( int n , boolean directed ){
assert n >= 0;
this.n = n;
this.m = 0; // 初始化没有任何边
this.directed = directed;
// g初始化为n个空的vector, 表示每一个g[i]都为空, 即没有任和边
g = (Vector<Integer>[])new Vector[n];
for(int i = 0 ; i < n ; i ++)
g[i] = new Vector<Integer>();
}
public int V(){ return n;} // 返回节点个数
public int E(){ return m;} // 返回边的个数
// 向图中添加一个边
public void addEdge( int v, int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
g[v].add(w);
if( v != w && !directed )
g[w].add(v);
m ++;
}
// 验证图中是否有从v到w的边
public boolean hasEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
for( int i = 0 ; i < g[v].size() ; i ++ )
if( g[v].elementAt(i) == w )
return true;
return false;
}
// 显示图的信息
public void show(){
for( int i = 0 ; i < n ; i ++ ){
System.out.print("vertex " + i + ":\t");
for( int j = 0 ; j < g[i].size() ; j ++ )
System.out.print(g[i].elementAt(j) + "\t");
System.out.println();
}
}
// 返回图中一个顶点的所有邻边
// 由于java使用引用机制,返回一个Vector不会带来额外开销,
public Iterable<Integer> adj(int v) {
assert v >= 0 && v < n;
return g[v];
}
}
7-6 寻路
稀疏图(邻接表):O(V+E)
稠密图(邻接矩阵):O(V^2)
// 稠密图 - 邻接矩阵
public class DenseGraph implements Graph{
private int n; // 节点数
private int m; // 边数
private boolean directed; // 是否为有向图
private boolean[][] g; // 图的具体数据
// 构造函数
public DenseGraph( int n , boolean directed ){
assert n >= 0;
this.n = n;
this.m = 0; // 初始化没有任何边
this.directed = directed;
// g初始化为n*n的布尔矩阵, 每一个g[i][j]均为false, 表示没有任和边
// false为boolean型变量的默认值
g = new boolean[n][n];
}
public int V(){ return n;} // 返回节点个数
public int E(){ return m;} // 返回边的个数
// 向图中添加一个边
public void addEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
if( hasEdge( v , w ) )
return;
g[v][w] = true;
if( !directed )
g[w][v] = true;
m ++;
}
// 验证图中是否有从v到w的边
public boolean hasEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
return g[v][w];
}
// 显示图的信息
public void show(){
for( int i = 0 ; i < n ; i ++ ){
for( int j = 0 ; j < n ; j ++ )
System.out.print(g[i][j]+"\t");
System.out.println();
}
}
// 返回图中一个顶点的所有邻边
// 由于java使用引用机制,返回一个Vector不会带来额外开销,
public Iterable<Integer> adj(int v) {
assert v >= 0 && v < n;
Vector<Integer> adjV = new Vector<Integer>();
for(int i = 0 ; i < n ; i ++ )
if( g[v][i] )
adjV.add(i);
return adjV;
}
}
// 图的接口
public interface Graph {
public int V();
public int E();
public void addEdge( int v , int w );
boolean hasEdge( int v , int w );
void show();
public Iterable<Integer> adj(int v);
}
public class Main {
// 测试寻路算法
public static void main(String[] args) {
String filename = "E:\\workspace\\Play-with-Algorithms\\src\\com\\imooc\\ch7_6\\testG.txt";
SparseGraph g = new SparseGraph(7, false);
ReadGraph readGraph = new ReadGraph(g, filename);
g.show();
System.out.println();
Path path = new Path(g,0);
System.out.println("Path from 0 to 6 : ");
path.showPath(6);
}
}
public class Path {
private Graph G; // 图的引用
private int s; // 起始点
private boolean[] visited; // 记录dfs的过程中节点是否被访问
private int[] from; // 记录路径, from[i]表示查找的路径上i的上一个节点
// 图的深度优先遍历
private void dfs( int v ){
visited[v] = true;
for( int i : G.adj(v) )
if( !visited[i] ){
from[i] = v;
dfs(i);
}
}
// 构造函数, 寻路算法, 寻找图graph从s点到其他点的路径
public Path(Graph graph, int s){
// 算法初始化
G = graph;
assert s >= 0 && s < G.V();
visited = new boolean[G.V()];
from = new int[G.V()];
for( int i = 0 ; i < G.V() ; i ++ ){
visited[i] = false;
from[i] = -1;
}
this.s = s;
// 寻路算法
dfs(s);
}
// 查询从s点到w点是否有路径
boolean hasPath(int w){
assert w >= 0 && w < G.V();
return visited[w];
}
// 查询从s点到w点的路径, 存放在vec中
Vector<Integer> path(int w){
assert hasPath(w) ;
Stack<Integer> s = new Stack<Integer>();
// 通过from数组逆向查找到从s到w的路径, 存放到栈中
int p = w;
while( p != -1 ){
s.push(p);
p = from[p];
}
// 从栈中依次取出元素, 获得顺序的从s到w的路径
Vector<Integer> res = new Vector<Integer>();
while( !s.empty() )
res.add( s.pop() );
return res;
}
// 打印出从s点到w点的路径
void showPath(int w){
assert hasPath(w) ;
Vector<Integer> vec = path(w);
for( int i = 0 ; i < vec.size() ; i ++ ){
System.out.print(vec.elementAt(i));
if( i == vec.size() - 1 )
System.out.println();
else
System.out.print(" -> ");
}
}
}
public class ReadGraph {
private Scanner scanner;
public ReadGraph(Graph graph, String filename){
readFile(filename);
try {
int V = scanner.nextInt();
if (V < 0)
throw new IllegalArgumentException("number of vertices in a Graph must be nonnegative");
assert V == graph.V();
int E = scanner.nextInt();
if (E < 0)
throw new IllegalArgumentException("number of edges in a Graph must be nonnegative");
for (int i = 0; i < E; i++) {
int v = scanner.nextInt();
int w = scanner.nextInt();
assert v >= 0 && v < V;
assert w >= 0 && w < V;
graph.addEdge(v, w);
}
}
catch (InputMismatchException e) {
String token = scanner.next();
throw new InputMismatchException("attempts to read an 'int' value from input stream, but the next token is \"" + token + "\"");
}
catch (NoSuchElementException e) {
throw new NoSuchElementException("attemps to read an 'int' value from input stream, but there are no more tokens available");
}
}
private void readFile(String filename){
assert filename != null;
try {
File file = new File(filename);
if (file.exists()) {
FileInputStream fis = new FileInputStream(file);
scanner = new Scanner(new BufferedInputStream(fis), "UTF-8");
scanner.useLocale(Locale.ENGLISH);
}
else
throw new IllegalArgumentException(filename + "doesn't exist.");
}
catch (IOException ioe) {
throw new IllegalArgumentException("Could not open " + filename, ioe);
}
}
}
// 稀疏图 - 邻接表
public class SparseGraph implements Graph {
private int n; // 节点数
private int m; // 边数
private boolean directed; // 是否为有向图
private Vector<Integer>[] g; // 图的具体数据
// 构造函数
public SparseGraph( int n , boolean directed ){
assert n >= 0;
this.n = n;
this.m = 0; // 初始化没有任何边
this.directed = directed;
// g初始化为n个空的vector, 表示每一个g[i]都为空, 即没有任和边
g = (Vector<Integer>[])new Vector[n];
for(int i = 0 ; i < n ; i ++)
g[i] = new Vector<Integer>();
}
public int V(){ return n;} // 返回节点个数
public int E(){ return m;} // 返回边的个数
// 向图中添加一个边
public void addEdge( int v, int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
g[v].add(w);
if( v != w && !directed )
g[w].add(v);
m ++;
}
// 验证图中是否有从v到w的边
public boolean hasEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
for( int i = 0 ; i < g[v].size() ; i ++ )
if( g[v].elementAt(i) == w )
return true;
return false;
}
// 显示图的信息
public void show(){
for( int i = 0 ; i < n ; i ++ ){
System.out.print("vertex " + i + ":\t");
for( int j = 0 ; j < g[i].size() ; j ++ )
System.out.print(g[i].elementAt(j) + "\t");
System.out.println();
}
}
// 返回图中一个顶点的所有邻边
// 由于java使用引用机制,返回一个Vector不会带来额外开销,
public Iterable<Integer> adj(int v) {
assert v >= 0 && v < n;
return g[v];
}
}
7-7 广度优先遍历和最短路径
// 稠密图 - 邻接矩阵
public class DenseGraph implements Graph{
private int n; // 节点数
private int m; // 边数
private boolean directed; // 是否为有向图
private boolean[][] g; // 图的具体数据
// 构造函数
public DenseGraph( int n , boolean directed ){
assert n >= 0;
this.n = n;
this.m = 0; // 初始化没有任何边
this.directed = directed;
// g初始化为n*n的布尔矩阵, 每一个g[i][j]均为false, 表示没有任和边
// false为boolean型变量的默认值
g = new boolean[n][n];
}
public int V(){ return n;} // 返回节点个数
public int E(){ return m;} // 返回边的个数
// 向图中添加一个边
public void addEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
if( hasEdge( v , w ) )
return;
g[v][w] = true;
if( !directed )
g[w][v] = true;
m ++;
}
// 验证图中是否有从v到w的边
public boolean hasEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
return g[v][w];
}
// 显示图的信息
public void show(){
for( int i = 0 ; i < n ; i ++ ){
for( int j = 0 ; j < n ; j ++ )
System.out.print(g[i][j]+"\t");
System.out.println();
}
}
// 返回图中一个顶点的所有邻边
// 由于java使用引用机制,返回一个Vector不会带来额外开销,
public Iterable<Integer> adj(int v) {
assert v >= 0 && v < n;
Vector<Integer> adjV = new Vector<Integer>();
for(int i = 0 ; i < n ; i ++ )
if( g[v][i] )
adjV.add(i);
return adjV;
}
}
// 图的接口
public interface Graph {
public int V();
public int E();
public void addEdge( int v , int w );
boolean hasEdge( int v , int w );
void show();
public Iterable<Integer> adj(int v);
}
public class Main {
// 测试无权图最短路径算法
public static void main(String[] args) {
String filename = "E:\\workspace\\Play-with-Algorithms\\src\\com\\imooc\\ch7_7\\testG.txt";
SparseGraph g = new SparseGraph(7, false);
ReadGraph readGraph = new ReadGraph(g, filename);
g.show();
System.out.println();
// 比较使用深度优先遍历和广度优先遍历获得路径的不同
// 广度优先遍历获得的是无权图的最短路径
Path dfs = new Path(g,0);
System.out.print("DFS : ");
dfs.showPath(6);
ShortestPath bfs = new ShortestPath(g,0);
System.out.print("BFS : ");
bfs.showPath(6);
}
}
public class Path {
private Graph G; // 图的引用
private int s; // 起始点
private boolean[] visited; // 记录dfs的过程中节点是否被访问
private int[] from; // 记录路径, from[i]表示查找的路径上i的上一个节点
// 图的深度优先遍历
private void dfs( int v ){
visited[v] = true;
for( int i : G.adj(v) )
if( !visited[i] ){
from[i] = v;
dfs(i);
}
}
// 构造函数, 寻路算法, 寻找图graph从s点到其他点的路径
public Path(Graph graph, int s){
// 算法初始化
G = graph;
assert s >= 0 && s < G.V();
visited = new boolean[G.V()];
from = new int[G.V()];
for( int i = 0 ; i < G.V() ; i ++ ){
visited[i] = false;
from[i] = -1;
}
this.s = s;
// 寻路算法
dfs(s);
}
// 查询从s点到w点是否有路径
boolean hasPath(int w){
assert w >= 0 && w < G.V();
return visited[w];
}
// 查询从s点到w点的路径, 存放在vec中
Vector<Integer> path(int w){
assert hasPath(w) ;
Stack<Integer> s = new Stack<Integer>();
// 通过from数组逆向查找到从s到w的路径, 存放到栈中
int p = w;
while( p != -1 ){
s.push(p);
p = from[p];
}
// 从栈中依次取出元素, 获得顺序的从s到w的路径
Vector<Integer> res = new Vector<Integer>();
while( !s.empty() )
res.add( s.pop() );
return res;
}
// 打印出从s点到w点的路径
void showPath(int w){
assert hasPath(w) ;
Vector<Integer> vec = path(w);
for( int i = 0 ; i < vec.size() ; i ++ ){
System.out.print(vec.elementAt(i));
if( i == vec.size() - 1 )
System.out.println();
else
System.out.print(" -> ");
}
}
}
public class ReadGraph {
private Scanner scanner;
public ReadGraph(Graph graph, String filename){
readFile(filename);
try {
int V = scanner.nextInt();
if (V < 0)
throw new IllegalArgumentException("number of vertices in a Graph must be nonnegative");
assert V == graph.V();
int E = scanner.nextInt();
if (E < 0)
throw new IllegalArgumentException("number of edges in a Graph must be nonnegative");
for (int i = 0; i < E; i++) {
int v = scanner.nextInt();
int w = scanner.nextInt();
assert v >= 0 && v < V;
assert w >= 0 && w < V;
graph.addEdge(v, w);
}
}
catch (InputMismatchException e) {
String token = scanner.next();
throw new InputMismatchException("attempts to read an 'int' value from input stream, but the next token is \"" + token + "\"");
}
catch (NoSuchElementException e) {
throw new NoSuchElementException("attemps to read an 'int' value from input stream, but there are no more tokens available");
}
}
private void readFile(String filename){
assert filename != null;
try {
File file = new File(filename);
if (file.exists()) {
FileInputStream fis = new FileInputStream(file);
scanner = new Scanner(new BufferedInputStream(fis), "UTF-8");
scanner.useLocale(Locale.ENGLISH);
}
else
throw new IllegalArgumentException(filename + " doesn't exist.");
}
catch (IOException ioe) {
throw new IllegalArgumentException("Could not open " + filename, ioe);
}
}
}
public class ShortestPath {
private Graph G; // 图的引用
private int s; // 起始点
private boolean[] visited; // 记录dfs的过程中节点是否被访问
private int[] from; // 记录路径, from[i]表示查找的路径上i的上一个节点
private int[] ord; // 记录路径中节点的次序。ord[i]表示i节点在路径中的次序。
// 构造函数, 寻路算法, 寻找图graph从s点到其他点的路径
public ShortestPath(Graph graph, int s){
// 算法初始化
G = graph;
assert s >= 0 && s < G.V();
visited = new boolean[G.V()];
from = new int[G.V()];
ord = new int[G.V()];
for( int i = 0 ; i < G.V() ; i ++ ){
visited[i] = false;
from[i] = -1;
ord[i] = -1;
}
this.s = s;
// 无向图最短路径算法, 从s开始广度优先遍历整张图
LinkedList<Integer> q = new LinkedList<Integer>();
q.push( s );
visited[s] = true;
ord[s] = 0;
while( !q.isEmpty() ){
int v = q.pop();
for( int i : G.adj(v) )
if( !visited[i] ){
q.push(i);
visited[i] = true;
from[i] = v;
ord[i] = ord[v] + 1;
}
}
}
// 查询从s点到w点是否有路径
public boolean hasPath(int w){
assert w >= 0 && w < G.V();
return visited[w];
}
// 查询从s点到w点的路径, 存放在vec中
public Vector<Integer> path(int w){
assert hasPath(w) ;
Stack<Integer> s = new Stack<Integer>();
// 通过from数组逆向查找到从s到w的路径, 存放到栈中
int p = w;
while( p != -1 ){
s.push(p);
p = from[p];
}
// 从栈中依次取出元素, 获得顺序的从s到w的路径
Vector<Integer> res = new Vector<Integer>();
while( !s.empty() )
res.add( s.pop() );
return res;
}
// 打印出从s点到w点的路径
public void showPath(int w){
assert hasPath(w) ;
Vector<Integer> vec = path(w);
for( int i = 0 ; i < vec.size() ; i ++ ){
System.out.print(vec.elementAt(i));
if( i == vec.size() - 1 )
System.out.println();
else
System.out.print(" -> ");
}
}
// 查看从s点到w点的最短路径长度
// 若从s到w不可达,返回-1
public int length(int w){
assert w >= 0 && w < G.V();
return ord[w];
}
}
// 稀疏图 - 邻接表
public class SparseGraph implements Graph {
private int n; // 节点数
private int m; // 边数
private boolean directed; // 是否为有向图
private Vector<Integer>[] g; // 图的具体数据
// 构造函数
public SparseGraph( int n , boolean directed ){
assert n >= 0;
this.n = n;
this.m = 0; // 初始化没有任何边
this.directed = directed;
// g初始化为n个空的vector, 表示每一个g[i]都为空, 即没有任和边
g = (Vector<Integer>[])new Vector[n];
for(int i = 0 ; i < n ; i ++)
g[i] = new Vector<Integer>();
}
public int V(){ return n;} // 返回节点个数
public int E(){ return m;} // 返回边的个数
// 向图中添加一个边
public void addEdge( int v, int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
g[v].add(w);
if( v != w && !directed )
g[w].add(v);
m ++;
}
// 验证图中是否有从v到w的边
public boolean hasEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
for( int i = 0 ; i < g[v].size() ; i ++ )
if( g[v].elementAt(i) == w )
return true;
return false;
}
// 显示图的信息
public void show(){
for( int i = 0 ; i < n ; i ++ ){
System.out.print("vertex " + i + ":\t");
for( int j = 0 ; j < g[i].size() ; j ++ )
System.out.print(g[i].elementAt(j) + "\t");
System.out.println();
}
}
// 返回图中一个顶点的所有邻边
// 由于java使用引用机制,返回一个Vector不会带来额外开销,
public Iterable<Integer> adj(int v) {
assert v >= 0 && v < n;
return g[v];
}
}
7-8 迷宫生成,ps抠图–更多无权图的应用
第8章 最小生成树
8-1 有权图
// 稠密图 - 邻接矩阵
public class DenseWeightedGraph<Weight extends Number & Comparable>
implements WeightedGraph{
private int n; // 节点数
private int m; // 边数
private boolean directed; // 是否为有向图
private Edge<Weight>[][] g; // 图的具体数据
// 构造函数
public DenseWeightedGraph( int n , boolean directed ){
assert n >= 0;
this.n = n;
this.m = 0; // 初始化没有任何边
this.directed = directed;
// g初始化为n*n的布尔矩阵, 每一个g[i][j]均为null, 表示没有任和边
// false为boolean型变量的默认值
g = new Edge[n][n];
for(int i = 0 ; i < n ; i ++)
for(int j = 0 ; j < n ; j ++)
g[i][j] = null;
}
public int V(){ return n;} // 返回节点个数
public int E(){ return m;} // 返回边的个数
// 向图中添加一个边
public void addEdge(Edge e){
assert e.v() >= 0 && e.v() < n ;
assert e.w() >= 0 && e.w() < n ;
if( hasEdge( e.v() , e.w() ) )
return;
g[e.v()][e.w()] = new Edge(e);
if( e.v() != e.w() && !directed )
g[e.w()][e.v()] = new Edge(e.w(), e.v(), e.wt());
m ++;
}
// 验证图中是否有从v到w的边
public boolean hasEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
return g[v][w] != null;
}
// 显示图的信息
public void show(){
for( int i = 0 ; i < n ; i ++ ){
for( int j = 0 ; j < n ; j ++ )
if( g[i][j] != null )
System.out.print(g[i][j].wt()+"\t");
else
System.out.print("NULL\t");
System.out.println();
}
}
// 返回图中一个顶点的所有邻边
// 由于java使用引用机制,返回一个Vector不会带来额外开销,
public Iterable<Edge<Weight>> adj(int v) {
assert v >= 0 && v < n;
Vector<Edge<Weight>> adjV = new Vector<Edge<Weight>>();
for(int i = 0 ; i < n ; i ++ )
if( g[v][i] != null )
adjV.add( g[v][i] );
return adjV;
}
}
// 边
public class Edge<Weight extends Number & Comparable> implements Comparable<Edge>{
private int a, b; // 边的两个端点
private Weight weight; // 边的权值
public Edge(int a, int b, Weight weight)
{
this.a = a;
this.b = b;
this.weight = weight;
}
public Edge(Edge<Weight> e)
{
this.a = e.a;
this.b = e.b;
this.weight = e.weight;
}
public int v(){ return a;} // 返回第一个顶点
public int w(){ return b;} // 返回第二个顶点
public Weight wt(){ return weight;} // 返回权值
// 给定一个顶点, 返回另一个顶点
public int other(int x){
assert x == a || x == b;
return x == a ? b : a;
}
// 输出边的信息
public String toString(){
return "" + a + "-" + b + ": " + weight;
}
// 边之间的比较
public int compareTo(Edge that)
{
if( weight.compareTo(that.wt()) < 0 )
return -1;
else if ( weight.compareTo(that.wt()) > 0 )
return +1;
else
return 0;
}
}
public class Main {
// 测试通过文件读取图的信息
public static void main(String[] args) {
// 使用两种图的存储方式读取testG1.txt文件
String filename = "E:\\workspace\\Play-with-Algorithms\\src\\com\\imooc\\ch8_1\\testG1.txt";
SparseWeightedGraph<Double> g1 = new SparseWeightedGraph<Double>(8, false);
ReadWeightedGraph readGraph1 = new ReadWeightedGraph(g1, filename);
System.out.println("test G1 in Sparse Weighted Graph:");
g1.show();
System.out.println();
DenseWeightedGraph<Double> g2 = new DenseWeightedGraph<Double>(8, false);
ReadWeightedGraph readGraph2 = new ReadWeightedGraph(g2 , filename );
System.out.println("test G1 in Dense Graph:");
g2.show();
System.out.println();
}
}
// 通过文件读取有全图的信息
public class ReadWeightedGraph{
private Scanner scanner;
// 由于文件格式的限制,我们的文件读取类只能读取权值为Double类型的图
public ReadWeightedGraph(WeightedGraph<Double> graph, String filename){
readFile(filename);
try {
int V = scanner.nextInt();
if (V < 0)
throw new IllegalArgumentException("number of vertices in a Graph must be nonnegative");
assert V == graph.V();
int E = scanner.nextInt();
if (E < 0)
throw new IllegalArgumentException("number of edges in a Graph must be nonnegative");
for (int i = 0; i < E; i++) {
int v = scanner.nextInt();
int w = scanner.nextInt();
Double weight = scanner.nextDouble();
assert v >= 0 && v < V;
assert w >= 0 && w < V;
graph.addEdge(new Edge<Double>(v, w, weight));
}
}
catch (InputMismatchException e) {
String token = scanner.next();
throw new InputMismatchException("attempts to read an 'int' value from input stream, but the next token is \"" + token + "\"");
}
catch (NoSuchElementException e) {
throw new NoSuchElementException("attemps to read an 'int' value from input stream, but there are no more tokens available");
}
}
private void readFile(String filename){
assert filename != null;
try {
File file = new File(filename);
if (file.exists()) {
FileInputStream fis = new FileInputStream(file);
scanner = new Scanner(new BufferedInputStream(fis), "UTF-8");
scanner.useLocale(Locale.ENGLISH);
}
else
throw new IllegalArgumentException(filename + " doesn't exist.");
}
catch (IOException ioe) {
throw new IllegalArgumentException("Could not open " + filename, ioe);
}
}
}
// 稀疏图 - 邻接表
public class SparseWeightedGraph<Weight extends Number & Comparable>
implements WeightedGraph {
private int n; // 节点数
private int m; // 边数
private boolean directed; // 是否为有向图
private Vector<Edge<Weight>>[] g; // 图的具体数据
// 构造函数
public SparseWeightedGraph( int n , boolean directed ){
assert n >= 0;
this.n = n;
this.m = 0; // 初始化没有任何边
this.directed = directed;
// g初始化为n个空的vector, 表示每一个g[i]都为空, 即没有任和边
g = (Vector<Edge<Weight>>[])new Vector[n];
for(int i = 0 ; i < n ; i ++)
g[i] = new Vector<Edge<Weight>>();
}
public int V(){ return n;} // 返回节点个数
public int E(){ return m;} // 返回边的个数
// 向图中添加一个边, 权值为weight
public void addEdge(Edge e){
assert e.v() >= 0 && e.v() < n ;
assert e.w() >= 0 && e.w() < n ;
// 注意, 由于在邻接表的情况, 查找是否有重边需要遍历整个链表
// 我们的程序允许重边的出现
g[e.v()].add(new Edge(e));
if( e.v() != e.w() && !directed )
g[e.w()].add(new Edge(e.w(), e.v(), e.wt()));
m ++;
}
// 验证图中是否有从v到w的边
public boolean hasEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
for( int i = 0 ; i < g[v].size() ; i ++ )
if( g[v].elementAt(i).other(v) == w )
return true;
return false;
}
// 显示图的信息
public void show(){
for( int i = 0 ; i < n ; i ++ ){
System.out.print("vertex " + i + ":\t");
for( int j = 0 ; j < g[i].size() ; j ++ ){
Edge e = g[i].elementAt(j);
System.out.print( "( to:" + e.other(i) + ",wt:" + e.wt() + ")\t");
}
System.out.println();
}
}
// 返回图中一个顶点的所有邻边
// 由于java使用引用机制,返回一个Vector不会带来额外开销,
public Iterable<Edge<Weight>> adj(int v) {
assert v >= 0 && v < n;
return g[v];
}
}
interface WeightedGraph<Weight extends Number & Comparable> {
public int V();
public int E();
public void addEdge(Edge<Weight> e);
boolean hasEdge( int v , int w );
void show();
public Iterable<Edge<Weight>> adj(int v);
}
8-2 最小生成树问题和切分定理
电缆布线设计
网络设计
电路设计
针对带权无向图
针对连通图
找V-1条边
连接V个顶点
总权值最小
把图中的节点分成两部分,成为一个切点(Cut)。
如果一个边的两个端点,属于切分(Cut)不同的两边,
这个边称为横切边(Crossing Edge).
切分定理:
给定任意切分,横切边中权值最小的边必然属于最小生成树。
8-3 Prim算法的第一个实现
// 稠密图 - 邻接矩阵
public class DenseWeightedGraph<Weight extends Number & Comparable>
implements WeightedGraph{
private int n; // 节点数
private int m; // 边数
private boolean directed; // 是否为有向图
private Edge<Weight>[][] g; // 图的具体数据
// 构造函数
public DenseWeightedGraph( int n , boolean directed ){
assert n >= 0;
this.n = n;
this.m = 0; // 初始化没有任何边
this.directed = directed;
// g初始化为n*n的布尔矩阵, 每一个g[i][j]均为null, 表示没有任和边
// false为boolean型变量的默认值
g = new Edge[n][n];
for(int i = 0 ; i < n ; i ++)
for(int j = 0 ; j < n ; j ++)
g[i][j] = null;
}
public int V(){ return n;} // 返回节点个数
public int E(){ return m;} // 返回边的个数
// 向图中添加一个边
public void addEdge(Edge e){
assert e.v() >= 0 && e.v() < n ;
assert e.w() >= 0 && e.w() < n ;
if( hasEdge( e.v() , e.w() ) )
return;
g[e.v()][e.w()] = new Edge(e);
if( e.v() != e.w() && !directed )
g[e.w()][e.v()] = new Edge(e.w(), e.v(), e.wt());
m ++;
}
// 验证图中是否有从v到w的边
public boolean hasEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
return g[v][w] != null;
}
// 显示图的信息
public void show(){
for( int i = 0 ; i < n ; i ++ ){
for( int j = 0 ; j < n ; j ++ )
if( g[i][j] != null )
System.out.print(g[i][j].wt()+"\t");
else
System.out.print("NULL\t");
System.out.println();
}
}
// 返回图中一个顶点的所有邻边
// 由于java使用引用机制,返回一个Vector不会带来额外开销,
public Iterable<Edge<Weight>> adj(int v) {
assert v >= 0 && v < n;
Vector<Edge<Weight>> adjV = new Vector<Edge<Weight>>();
for(int i = 0 ; i < n ; i ++ )
if( g[v][i] != null )
adjV.add( g[v][i] );
return adjV;
}
}
// 边
public class Edge<Weight extends Number & Comparable> implements Comparable<Edge<Weight>>{
private int a, b; // 边的两个端点
private Weight weight; // 边的权值
public Edge(int a, int b, Weight weight)
{
this.a = a;
this.b = b;
this.weight = weight;
}
public Edge(Edge<Weight> e)
{
this.a = e.a;
this.b = e.b;
this.weight = e.weight;
}
public int v(){ return a;} // 返回第一个顶点
public int w(){ return b;} // 返回第二个顶点
public Weight wt(){ return weight;} // 返回权值
// 给定一个顶点, 返回另一个顶点
public int other(int x){
assert x == a || x == b;
return x == a ? b : a;
}
// 输出边的信息
public String toString(){
return "" + a + "-" + b + ": " + weight;
}
// 边之间的比较
public int compareTo(Edge<Weight> that)
{
if( weight.compareTo(that.wt()) < 0 )
return -1;
else if ( weight.compareTo(that.wt()) > 0 )
return +1;
else
return 0;
}
}
// 使用Prim算法求图的最小生成树
public class LazyPrimMST<Weight extends Number & Comparable> {
private WeightedGraph<Weight> G; // 图的引用
private MinHeap<Edge<Weight>> pq; // 最小堆, 算法辅助数据结构
private boolean[] marked; // 标记数组, 在算法运行过程中标记节点i是否被访问
private Vector<Edge<Weight>> mst; // 最小生成树所包含的所有边
private Number mstWeight; // 最小生成树的权值
// 构造函数, 使用Prim算法求图的最小生成树
public LazyPrimMST(WeightedGraph<Weight> graph){
// 算法初始化
G = graph;
pq = new MinHeap<Edge<Weight>>(G.E());
marked = new boolean[G.V()];
mst = new Vector<Edge<Weight>>();
// Lazy Prim
visit(0);
while( !pq.isEmpty() ){
// 使用最小堆找出已经访问的边中权值最小的边
Edge<Weight> e = pq.extractMin();
// 如果这条边的两端都已经访问过了, 则扔掉这条边
if( marked[e.v()] == marked[e.w()] )
continue;
// 否则, 这条边则应该存在在最小生成树中
mst.add( e );
// 访问和这条边连接的还没有被访问过的节点
if( !marked[e.v()] )
visit( e.v() );
else
visit( e.w() );
}
// 计算最小生成树的权值
mstWeight = mst.elementAt(0).wt();
for( int i = 1 ; i < mst.size() ; i ++ )
mstWeight = mstWeight.doubleValue() + mst.elementAt(i).wt().doubleValue();
}
// 访问节点v
private void visit(int v){
assert !marked[v];
marked[v] = true;
// 将和节点v相连接的所有未访问的边放入最小堆中
for( Edge<Weight> e : G.adj(v) )
if( !marked[e.other(v)] )
pq.insert(e);
}
// 返回最小生成树的所有边
Vector<Edge<Weight>> mstEdges(){
return mst;
};
// 返回最小生成树的权值
Number result(){
return mstWeight;
};
}
public class Main {
public static void main(String[] args) {
String filename = "E:\\workspace\\Play-with-Algorithms\\src\\com\\imooc\\ch8_3\\testG1.txt";
int V = 8;
SparseWeightedGraph<Double> g = new SparseWeightedGraph<Double>(V, false);
ReadWeightedGraph readGraph = new ReadWeightedGraph(g, filename);
// Test Lazy Prim MST
System.out.println("Test Lazy Prim MST:");
LazyPrimMST<Double> lazyPrimMST = new LazyPrimMST<Double>(g);
Vector<Edge<Double>> mst = lazyPrimMST.mstEdges();
for( int i = 0 ; i < mst.size() ; i ++ )
System.out.println(mst.elementAt(i));
System.out.println("The MST weight is: " + lazyPrimMST.result());
System.out.println();
}
}
// 在堆的有关操作中,需要比较堆中元素的大小,所以Item需要extends Comparable
public class MinHeap<Item extends Comparable> {
protected Item[] data;
protected int count;
protected int capacity;
// 构造函数, 构造一个空堆, 可容纳capacity个元素
public MinHeap(int capacity){
data = (Item[])new Comparable[capacity+1];
count = 0;
this.capacity = capacity;
}
// 构造函数, 通过一个给定数组创建一个最小堆
// 该构造堆的过程, 时间复杂度为O(n)
public MinHeap(Item arr[]){
int n = arr.length;
data = (Item[])new Comparable[n+1];
capacity = n;
for( int i = 0 ; i < n ; i ++ )
data[i+1] = arr[i];
count = n;
for( int i = count/2 ; i >= 1 ; i -- )
shiftDown(i);
}
// 返回堆中的元素个数
public int size(){
return count;
}
// 返回一个布尔值, 表示堆中是否为空
public boolean isEmpty(){
return count == 0;
}
// 向最小堆中插入一个新的元素 item
public void insert(Item item){
assert count + 1 <= capacity;
data[count+1] = item;
count ++;
shiftUp(count);
}
// 从最小堆中取出堆顶元素, 即堆中所存储的最小数据
public Item extractMin(){
assert count > 0;
Item ret = data[1];
swap( 1 , count );
count --;
shiftDown(1);
return ret;
}
// 获取最小堆中的堆顶元素
public Item getMin(){
assert( count > 0 );
return data[1];
}
// 交换堆中索引为i和j的两个元素
private void swap(int i, int j){
Item t = data[i];
data[i] = data[j];
data[j] = t;
}
//********************
//* 最小堆核心辅助函数
//********************
private void shiftUp(int k){
while( k > 1 && data[k/2].compareTo(data[k]) > 0 ){
swap(k, k/2);
k /= 2;
}
}
private void shiftDown(int k){
while( 2*k <= count ){
int j = 2*k; // 在此轮循环中,data[k]和data[j]交换位置
if( j+1 <= count && data[j+1].compareTo(data[j]) < 0 )
j ++;
// data[j] 是 data[2*k]和data[2*k+1]中的最小值
if( data[k].compareTo(data[j]) <= 0 ) break;
swap(k, j);
k = j;
}
}
// 测试 MinHeap
public static void main(String[] args) {
MinHeap<Integer> minHeap = new MinHeap<Integer>(100);
int N = 100; // 堆中元素个数
int M = 100; // 堆中元素取值范围[0, M)
for( int i = 0 ; i < N ; i ++ )
minHeap.insert( new Integer((int)(Math.random() * M)) );
Integer[] arr = new Integer[N];
// 将minheap中的数据逐渐使用extractMin取出来
// 取出来的顺序应该是按照从小到大的顺序取出来的
for( int i = 0 ; i < N ; i ++ ){
arr[i] = minHeap.extractMin();
System.out.print(arr[i] + " ");
}
System.out.println();
// 确保arr数组是从小到大排列的
for( int i = 1 ; i < N ; i ++ )
assert arr[i-1] <= arr[i];
}
}
// 通过文件读取有全图的信息
public class ReadWeightedGraph {
private Scanner scanner;
// 由于文件格式的限制,我们的文件读取类只能读取权值为Double类型的图
public ReadWeightedGraph(WeightedGraph<Double> graph, String filename){
readFile(filename);
try {
int V = scanner.nextInt();
if (V < 0)
throw new IllegalArgumentException("number of vertices in a Graph must be nonnegative");
assert V == graph.V();
int E = scanner.nextInt();
if (E < 0)
throw new IllegalArgumentException("number of edges in a Graph must be nonnegative");
for (int i = 0; i < E; i++) {
int v = scanner.nextInt();
int w = scanner.nextInt();
Double weight = scanner.nextDouble();
assert v >= 0 && v < V;
assert w >= 0 && w < V;
graph.addEdge(new Edge<Double>(v, w, weight));
}
}
catch (InputMismatchException e) {
String token = scanner.next();
throw new InputMismatchException("attempts to read an 'int' value from input stream, but the next token is \"" + token + "\"");
}
catch (NoSuchElementException e) {
throw new NoSuchElementException("attemps to read an 'int' value from input stream, but there are no more tokens available");
}
}
private void readFile(String filename){
assert filename != null;
try {
File file = new File(filename);
if (file.exists()) {
FileInputStream fis = new FileInputStream(file);
scanner = new Scanner(new BufferedInputStream(fis), "UTF-8");
scanner.useLocale(Locale.ENGLISH);
}
else
throw new IllegalArgumentException(filename + " doesn't exist.");
}
catch (IOException ioe) {
throw new IllegalArgumentException("Could not open " + filename, ioe);
}
}
}
// 稀疏图 - 邻接表
public class SparseWeightedGraph<Weight extends Number & Comparable>
implements WeightedGraph {
private int n; // 节点数
private int m; // 边数
private boolean directed; // 是否为有向图
private Vector<Edge<Weight>>[] g; // 图的具体数据
// 构造函数
public SparseWeightedGraph( int n , boolean directed ){
assert n >= 0;
this.n = n;
this.m = 0; // 初始化没有任何边
this.directed = directed;
// g初始化为n个空的vector, 表示每一个g[i]都为空, 即没有任和边
g = (Vector<Edge<Weight>>[])new Vector[n];
for(int i = 0 ; i < n ; i ++)
g[i] = new Vector<Edge<Weight>>();
}
public int V(){ return n;} // 返回节点个数
public int E(){ return m;} // 返回边的个数
// 向图中添加一个边, 权值为weight
public void addEdge(Edge e){
assert e.v() >= 0 && e.v() < n ;
assert e.w() >= 0 && e.w() < n ;
// 注意, 由于在邻接表的情况, 查找是否有重边需要遍历整个链表
// 我们的程序允许重边的出现
g[e.v()].add(new Edge(e));
if( e.v() != e.w() && !directed )
g[e.w()].add(new Edge(e.w(), e.v(), e.wt()));
m ++;
}
// 验证图中是否有从v到w的边
public boolean hasEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
for( int i = 0 ; i < g[v].size() ; i ++ )
if( g[v].elementAt(i).other(v) == w )
return true;
return false;
}
// 显示图的信息
public void show(){
for( int i = 0 ; i < n ; i ++ ){
System.out.print("vertex " + i + ":\t");
for( int j = 0 ; j < g[i].size() ; j ++ ){
Edge e = g[i].elementAt(j);
System.out.print( "( to:" + e.other(i) + ",wt:" + e.wt() + ")\t");
}
System.out.println();
}
}
// 返回图中一个顶点的所有邻边
// 由于java使用引用机制,返回一个Vector不会带来额外开销,
public Iterable<Edge<Weight>> adj(int v) {
assert v >= 0 && v < n;
return g[v];
}
}
interface WeightedGraph<Weight extends Number & Comparable> {
public int V();
public int E();
public void addEdge(Edge<Weight> e);
boolean hasEdge( int v , int w );
void show();
public Iterable<Edge<Weight>> adj(int v);
}
8-4 Prim算法的优化
Lazy Prim的时间复杂度O(ElogE)
Prim的时间复杂度O(ElogV)
8-5 优化后的Prim算法的实现
// 稠密图 - 邻接矩阵
public class DenseWeightedGraph<Weight extends Number & Comparable>
implements WeightedGraph{
private int n; // 节点数
private int m; // 边数
private boolean directed; // 是否为有向图
private Edge<Weight>[][] g; // 图的具体数据
// 构造函数
public DenseWeightedGraph( int n , boolean directed ){
assert n >= 0;
this.n = n;
this.m = 0; // 初始化没有任何边
this.directed = directed;
// g初始化为n*n的布尔矩阵, 每一个g[i][j]均为null, 表示没有任和边
// false为boolean型变量的默认值
g = new Edge[n][n];
for(int i = 0 ; i < n ; i ++)
for(int j = 0 ; j < n ; j ++)
g[i][j] = null;
}
public int V(){ return n;} // 返回节点个数
public int E(){ return m;} // 返回边的个数
// 向图中添加一个边
public void addEdge(Edge e){
assert e.v() >= 0 && e.v() < n ;
assert e.w() >= 0 && e.w() < n ;
if( hasEdge( e.v() , e.w() ) )
return;
g[e.v()][e.w()] = new Edge(e);
if( e.v() != e.w() && !directed )
g[e.w()][e.v()] = new Edge(e.w(), e.v(), e.wt());
m ++;
}
// 验证图中是否有从v到w的边
public boolean hasEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
return g[v][w] != null;
}
// 显示图的信息
public void show(){
for( int i = 0 ; i < n ; i ++ ){
for( int j = 0 ; j < n ; j ++ )
if( g[i][j] != null )
System.out.print(g[i][j].wt()+"\t");
else
System.out.print("NULL\t");
System.out.println();
}
}
// 返回图中一个顶点的所有邻边
// 由于java使用引用机制,返回一个Vector不会带来额外开销,
public Iterable<Edge<Weight>> adj(int v) {
assert v >= 0 && v < n;
Vector<Edge<Weight>> adjV = new Vector<Edge<Weight>>();
for(int i = 0 ; i < n ; i ++ )
if( g[v][i] != null )
adjV.add( g[v][i] );
return adjV;
}
}
// 边
public class Edge<Weight extends Number & Comparable> implements Comparable<Edge<Weight>>{
private int a, b; // 边的两个端点
private Weight weight; // 边的权值
public Edge(int a, int b, Weight weight)
{
this.a = a;
this.b = b;
this.weight = weight;
}
public Edge(Edge<Weight> e)
{
this.a = e.a;
this.b = e.b;
this.weight = e.weight;
}
public int v(){ return a;} // 返回第一个顶点
public int w(){ return b;} // 返回第二个顶点
public Weight wt(){ return weight;} // 返回权值
// 给定一个顶点, 返回另一个顶点
public int other(int x){
assert x == a || x == b;
return x == a ? b : a;
}
// 输出边的信息
public String toString(){
return "" + a + "-" + b + ": " + weight;
}
// 边之间的比较
public int compareTo(Edge<Weight> that)
{
if( weight.compareTo(that.wt()) < 0 )
return -1;
else if ( weight.compareTo(that.wt()) > 0 )
return +1;
else
return 0;
}
}
// 最小索引堆
public class IndexMinHeap<Item extends Comparable> {
protected Item[] data; // 最小索引堆中的数据
protected int[] indexes; // 最小索引堆中的索引, indexes[x] = i 表示索引i在x的位置
protected int[] reverse; // 最小索引堆中的反向索引, reverse[i] = x 表示索引i在x的位置
protected int count;
protected int capacity;
// 构造函数, 构造一个空堆, 可容纳capacity个元素
public IndexMinHeap(int capacity){
data = (Item[])new Comparable[capacity+1];
indexes = new int[capacity+1];
reverse = new int[capacity+1];
for( int i = 0 ; i <= capacity ; i ++ )
reverse[i] = 0;
count = 0;
this.capacity = capacity;
}
// 返回索引堆中的元素个数
public int size(){
return count;
}
// 返回一个布尔值, 表示索引堆中是否为空
public boolean isEmpty(){
return count == 0;
}
// 向最小索引堆中插入一个新的元素, 新元素的索引为i, 元素为item
// 传入的i对用户而言,是从0索引的
public void insert(int i, Item item){
assert count + 1 <= capacity;
assert i + 1 >= 1 && i + 1 <= capacity;
// 再插入一个新元素前,还需要保证索引i所在的位置是没有元素的。
assert !contain(i);
i += 1;
data[i] = item;
indexes[count+1] = i;
reverse[i] = count + 1;
count ++;
shiftUp(count);
}
// 从最小索引堆中取出堆顶元素, 即索引堆中所存储的最小数据
public Item extractMin(){
assert count > 0;
Item ret = data[indexes[1]];
swapIndexes( 1 , count );
reverse[indexes[count]] = 0;
count --;
shiftDown(1);
return ret;
}
// 从最小索引堆中取出堆顶元素的索引
public int extractMinIndex(){
assert count > 0;
int ret = indexes[1] - 1;
swapIndexes( 1 , count );
reverse[indexes[count]] = 0;
count --;
shiftDown(1);
return ret;
}
// 获取最小索引堆中的堆顶元素
public Item getMin(){
assert count > 0;
return data[indexes[1]];
}
// 获取最小索引堆中的堆顶元素的索引
public int getMinIndex(){
assert count > 0;
return indexes[1]-1;
}
// 看索引i所在的位置是否存在元素
boolean contain( int i ){
assert i + 1 >= 1 && i + 1 <= capacity;
return reverse[i+1] != 0;
}
// 获取最小索引堆中索引为i的元素
public Item getItem( int i ){
assert contain(i);
return data[i+1];
}
// 将最小索引堆中索引为i的元素修改为newItem
public void change( int i , Item newItem ){
assert contain(i);
i += 1;
data[i] = newItem;
// 有了 reverse 之后,
// 我们可以非常简单的通过reverse直接定位索引i在indexes中的位置
shiftUp( reverse[i] );
shiftDown( reverse[i] );
}
// 交换索引堆中的索引i和j
// 由于有了反向索引reverse数组,
// indexes数组发生改变以后, 相应的就需要维护reverse数组
private void swapIndexes(int i, int j){
int t = indexes[i];
indexes[i] = indexes[j];
indexes[j] = t;
reverse[indexes[i]] = i;
reverse[indexes[j]] = j;
}
//********************
//* 最小索引堆核心辅助函数
//********************
// 索引堆中, 数据之间的比较根据data的大小进行比较, 但实际操作的是索引
private void shiftUp(int k){
while( k > 1 && data[indexes[k/2]].compareTo(data[indexes[k]]) > 0 ){
swapIndexes(k, k/2);
k /= 2;
}
}
// 索引堆中, 数据之间的比较根据data的大小进行比较, 但实际操作的是索引
private void shiftDown(int k){
while( 2*k <= count ){
int j = 2*k;
if( j+1 <= count && data[indexes[j+1]].compareTo(data[indexes[j]]) < 0 )
j ++;
if( data[indexes[k]].compareTo(data[indexes[j]]) <= 0 )
break;
swapIndexes(k, j);
k = j;
}
}
// 测试 IndexMinHeap
public static void main(String[] args) {
int N = 1000000;
IndexMinHeap<Integer> indexMinHeap = new IndexMinHeap<Integer>(N);
for( int i = 0 ; i < N ; i ++ )
indexMinHeap.insert( i , (int)(Math.random()*N) );
}
}
// 使用Prim算法求图的最小生成树
public class LazyPrimMST<Weight extends Number & Comparable> {
private WeightedGraph<Weight> G; // 图的引用
private MinHeap<Edge<Weight>> pq; // 最小堆, 算法辅助数据结构
private boolean[] marked; // 标记数组, 在算法运行过程中标记节点i是否被访问
private Vector<Edge<Weight>> mst; // 最小生成树所包含的所有边
private Number mstWeight; // 最小生成树的权值
// 构造函数, 使用Prim算法求图的最小生成树
public LazyPrimMST(WeightedGraph<Weight> graph){
// 算法初始化
G = graph;
pq = new MinHeap<Edge<Weight>>(G.E());
marked = new boolean[G.V()];
mst = new Vector<Edge<Weight>>();
// Lazy Prim
visit(0);
while( !pq.isEmpty() ){
// 使用最小堆找出已经访问的边中权值最小的边
Edge<Weight> e = pq.extractMin();
// 如果这条边的两端都已经访问过了, 则扔掉这条边
if( marked[e.v()] == marked[e.w()] )
continue;
// 否则, 这条边则应该存在在最小生成树中
mst.add( e );
// 访问和这条边连接的还没有被访问过的节点
if( !marked[e.v()] )
visit( e.v() );
else
visit( e.w() );
}
// 计算最小生成树的权值
mstWeight = mst.elementAt(0).wt();
for( int i = 1 ; i < mst.size() ; i ++ )
mstWeight = mstWeight.doubleValue() + mst.elementAt(i).wt().doubleValue();
}
// 访问节点v
private void visit(int v){
assert !marked[v];
marked[v] = true;
// 将和节点v相连接的所有未访问的边放入最小堆中
for( Edge<Weight> e : G.adj(v) )
if( !marked[e.other(v)] )
pq.insert(e);
}
// 返回最小生成树的所有边
Vector<Edge<Weight>> mstEdges(){
return mst;
}
// 返回最小生成树的权值
Number result(){
return mstWeight;
}
// 测试 Lazy Prim
public static void main(String[] args) {
String filename = "testG1.txt";
int V = 8;
SparseWeightedGraph<Double> g = new SparseWeightedGraph<Double>(V, false);
ReadWeightedGraph readGraph = new ReadWeightedGraph(g, filename);
// Test Lazy Prim MST
System.out.println("Test Lazy Prim MST:");
LazyPrimMST<Double> lazyPrimMST = new LazyPrimMST<Double>(g);
Vector<Edge<Double>> mst = lazyPrimMST.mstEdges();
for( int i = 0 ; i < mst.size() ; i ++ )
System.out.println(mst.elementAt(i));
System.out.println("The MST weight is: " + lazyPrimMST.result());
System.out.println();
}
}
public class Main {
// 测试我们实现的两种Prim算法的性能差距
// 可以看出这一节使用索引堆实现的Prim算法优于上一小节的Lazy Prim算法
public static void main(String[] args) {
String filename1 = "E:\\workspace\\Play-with-Algorithms\\src\\com\\imooc\\ch8_5\\testG1.txt";
int V1 = 8;
String filename2 = "E:\\workspace\\Play-with-Algorithms\\src\\com\\imooc\\ch8_5\\testG2.txt";
int V2 = 250;
String filename3 = "E:\\workspace\\Play-with-Algorithms\\src\\com\\imooc\\ch8_5\\testG3.txt";
int V3 = 1000;
String filename4 = "E:\\workspace\\Play-with-Algorithms\\src\\com\\imooc\\ch8_5\\testG4.txt";
int V4 = 10000;
//String filename5 = "testG5.txt";
//int V5 = 1000000;
// 文件读取
SparseWeightedGraph<Double> g1 = new SparseWeightedGraph<Double>(V1, false);
ReadWeightedGraph readGraph1 = new ReadWeightedGraph(g1, filename1);
System.out.println( filename1 + " load successfully.");
SparseWeightedGraph<Double> g2 = new SparseWeightedGraph<Double>(V2, false);
ReadWeightedGraph readGraph2 = new ReadWeightedGraph(g2, filename2);
System.out.println( filename2 + " load successfully.");
SparseWeightedGraph<Double> g3 = new SparseWeightedGraph<Double>(V3, false);
ReadWeightedGraph readGraph3 = new ReadWeightedGraph(g3, filename3);
System.out.println( filename3 + " load successfully.");
SparseWeightedGraph<Double> g4 = new SparseWeightedGraph<Double>(V4, false);
ReadWeightedGraph readGraph4 = new ReadWeightedGraph(g4, filename4);
System.out.println( filename4 + " load successfully.");
// SparseWeightedGraph<Double> g5 = new SparseWeightedGraph<Double>(V5, false);
// ReadWeightedGraph readGraph5 = new ReadWeightedGraph(g5, filename5);
// System.out.println( filename5 + " load successfully.");
System.out.println();
long startTime, endTime;
// Test Lazy Prim MST
System.out.println("Test Lazy Prim MST:");
startTime = System.currentTimeMillis();
LazyPrimMST<Double> lazyPrimMST1 = new LazyPrimMST<Double>(g1);
endTime = System.currentTimeMillis();
System.out.println("Test for G1: " + (endTime-startTime) + "ms.");
startTime = System.currentTimeMillis();
LazyPrimMST<Double> lazyPrimMST2 = new LazyPrimMST<Double>(g2);
endTime = System.currentTimeMillis();
System.out.println("Test for G2: " + (endTime-startTime) + "ms.");
startTime = System.currentTimeMillis();
LazyPrimMST<Double> lazyPrimMST3 = new LazyPrimMST<Double>(g3);
endTime = System.currentTimeMillis();
System.out.println("Test for G3: " + (endTime-startTime) + "ms.");
startTime = System.currentTimeMillis();
LazyPrimMST<Double> lazyPrimMST4 = new LazyPrimMST<Double>(g4);
endTime = System.currentTimeMillis();
System.out.println("Test for G4: " + (endTime-startTime) + "ms.");
// startTime = System.currentTimeMillis();
// LazyPrimMST<Double> lazyPrimMST5 = new LazyPrimMST<Double>(g5);
// endTime = System.currentTimeMillis();
// System.out.println("Test for G5: " + (endTime-startTime) + "ms.");
System.out.println();
// Test Prim MST
System.out.println("Test Prim MST:");
startTime = System.currentTimeMillis();
PrimMST<Double> primMST1 = new PrimMST<Double>(g1);
endTime = System.currentTimeMillis();
System.out.println("Test for G1: " + (endTime-startTime) + "ms.");
startTime = System.currentTimeMillis();
PrimMST<Double> primMST2 = new PrimMST<Double>(g2);
endTime = System.currentTimeMillis();
System.out.println("Test for G2: " + (endTime-startTime) + "ms.");
startTime = System.currentTimeMillis();
PrimMST<Double> primMST3 = new PrimMST<Double>(g3);
endTime = System.currentTimeMillis();
System.out.println("Test for G3: " + (endTime-startTime) + "ms.");
startTime = System.currentTimeMillis();
PrimMST<Double> primMST4 = new PrimMST<Double>(g4);
endTime = System.currentTimeMillis();
System.out.println("Test for G4: " + (endTime-startTime) + "ms.");
// startTime = System.currentTimeMillis();
// PrimMST<Double> primMST5 = new PrimMST<Double>(g5);
// endTime = System.currentTimeMillis();
// System.out.println("Test for G5: " + (endTime-startTime) + "ms.");
System.out.println();
}
}
// 在堆的有关操作中,需要比较堆中元素的大小,所以Item需要extends Comparable
public class MinHeap<Item extends Comparable> {
protected Item[] data;
protected int count;
protected int capacity;
// 构造函数, 构造一个空堆, 可容纳capacity个元素
public MinHeap(int capacity){
data = (Item[])new Comparable[capacity+1];
count = 0;
this.capacity = capacity;
}
// 构造函数, 通过一个给定数组创建一个最小堆
// 该构造堆的过程, 时间复杂度为O(n)
public MinHeap(Item arr[]){
int n = arr.length;
data = (Item[])new Comparable[n+1];
capacity = n;
for( int i = 0 ; i < n ; i ++ )
data[i+1] = arr[i];
count = n;
for( int i = count/2 ; i >= 1 ; i -- )
shiftDown(i);
}
// 返回堆中的元素个数
public int size(){
return count;
}
// 返回一个布尔值, 表示堆中是否为空
public boolean isEmpty(){
return count == 0;
}
// 向最小堆中插入一个新的元素 item
public void insert(Item item){
assert count + 1 <= capacity;
data[count+1] = item;
count ++;
shiftUp(count);
}
// 从最小堆中取出堆顶元素, 即堆中所存储的最小数据
public Item extractMin(){
assert count > 0;
Item ret = data[1];
swap( 1 , count );
count --;
shiftDown(1);
return ret;
}
// 获取最小堆中的堆顶元素
public Item getMin(){
assert( count > 0 );
return data[1];
}
// 交换堆中索引为i和j的两个元素
private void swap(int i, int j){
Item t = data[i];
data[i] = data[j];
data[j] = t;
}
//********************
//* 最小堆核心辅助函数
//********************
private void shiftUp(int k){
while( k > 1 && data[k/2].compareTo(data[k]) > 0 ){
swap(k, k/2);
k /= 2;
}
}
private void shiftDown(int k){
while( 2*k <= count ){
int j = 2*k; // 在此轮循环中,data[k]和data[j]交换位置
if( j+1 <= count && data[j+1].compareTo(data[j]) < 0 )
j ++;
// data[j] 是 data[2*k]和data[2*k+1]中的最小值
if( data[k].compareTo(data[j]) <= 0 ) break;
swap(k, j);
k = j;
}
}
// 测试 MinHeap
public static void main(String[] args) {
MinHeap<Integer> minHeap = new MinHeap<Integer>(100);
int N = 100; // 堆中元素个数
int M = 100; // 堆中元素取值范围[0, M)
for( int i = 0 ; i < N ; i ++ )
minHeap.insert( new Integer((int)(Math.random() * M)) );
Integer[] arr = new Integer[N];
// 将minheap中的数据逐渐使用extractMin取出来
// 取出来的顺序应该是按照从小到大的顺序取出来的
for( int i = 0 ; i < N ; i ++ ){
arr[i] = minHeap.extractMin();
System.out.print(arr[i] + " ");
}
System.out.println();
// 确保arr数组是从小到大排列的
for( int i = 1 ; i < N ; i ++ )
assert arr[i-1] <= arr[i];
}
}
// 使用优化的Prim算法求图的最小生成树
public class PrimMST<Weight extends Number & Comparable> {
private WeightedGraph G; // 图的引用
private IndexMinHeap<Weight> ipq; // 最小索引堆, 算法辅助数据结构
private Edge<Weight>[] edgeTo; // 访问的点所对应的边, 算法辅助数据结构
private boolean[] marked; // 标记数组, 在算法运行过程中标记节点i是否被访问
private Vector<Edge<Weight>> mst; // 最小生成树所包含的所有边
private Number mstWeight; // 最小生成树的权值
// 构造函数, 使用Prim算法求图的最小生成树
public PrimMST(WeightedGraph graph){
G = graph;
assert( graph.E() >= 1 );
ipq = new IndexMinHeap<Weight>(graph.V());
// 算法初始化
marked = new boolean[G.V()];
edgeTo = new Edge[G.V()];
for( int i = 0 ; i < G.V() ; i ++ ){
marked[i] = false;
edgeTo[i] = null;
}
mst = new Vector<Edge<Weight>>();
// Prim
visit(0);
while( !ipq.isEmpty() ){
// 使用最小索引堆找出已经访问的边中权值最小的边
// 最小索引堆中存储的是点的索引, 通过点的索引找到相对应的边
int v = ipq.extractMinIndex();
assert( edgeTo[v] != null );
mst.add( edgeTo[v] );
visit( v );
}
// 计算最小生成树的权值
mstWeight = mst.elementAt(0).wt();
for( int i = 1 ; i < mst.size() ; i ++ )
mstWeight = mstWeight.doubleValue() + mst.elementAt(i).wt().doubleValue();
}
// 访问节点v
void visit(int v){
assert !marked[v];
marked[v] = true;
// 将和节点v相连接的未访问的另一端点, 和与之相连接的边, 放入最小堆中
for( Object item : G.adj(v) ){
Edge<Weight> e = (Edge<Weight>)item;
int w = e.other(v);
// 如果边的另一端点未被访问
if( !marked[w] ){
// 如果从没有考虑过这个端点, 直接将这个端点和与之相连接的边加入索引堆
if( edgeTo[w] == null ){
edgeTo[w] = e;
ipq.insert(w, e.wt());
}
// 如果曾经考虑这个端点, 但现在的边比之前考虑的边更短, 则进行替换
else if( e.wt().compareTo(edgeTo[w].wt()) < 0 ){
edgeTo[w] = e;
ipq.change(w, e.wt());
}
}
}
}
// 返回最小生成树的所有边
Vector<Edge<Weight>> mstEdges(){
return mst;
}
// 返回最小生成树的权值
Number result(){
return mstWeight;
}
// 测试 Prim
public static void main(String[] args) {
String filename = "testG1.txt";
int V = 8;
SparseWeightedGraph<Double> g = new SparseWeightedGraph<Double>(V, false);
ReadWeightedGraph readGraph = new ReadWeightedGraph(g, filename);
// Test Prim MST
System.out.println("Test Prim MST:");
PrimMST<Double> primMST = new PrimMST<Double>(g);
Vector<Edge<Double>> mst = primMST.mstEdges();
for( int i = 0 ; i < mst.size() ; i ++ )
System.out.println(mst.elementAt(i));
System.out.println("The MST weight is: " + primMST.result());
System.out.println();
}
}
// 通过文件读取有全图的信息
public class ReadWeightedGraph {
private Scanner scanner;
// 由于文件格式的限制,我们的文件读取类只能读取权值为Double类型的图
public ReadWeightedGraph(WeightedGraph<Double> graph, String filename){
readFile(filename);
try {
int V = scanner.nextInt();
if (V < 0)
throw new IllegalArgumentException("number of vertices in a Graph must be nonnegative");
assert V == graph.V();
int E = scanner.nextInt();
if (E < 0)
throw new IllegalArgumentException("number of edges in a Graph must be nonnegative");
for (int i = 0; i < E; i++) {
int v = scanner.nextInt();
int w = scanner.nextInt();
Double weight = scanner.nextDouble();
assert v >= 0 && v < V;
assert w >= 0 && w < V;
graph.addEdge(new Edge<Double>(v, w, weight));
}
}
catch (InputMismatchException e) {
String token = scanner.next();
throw new InputMismatchException("attempts to read an 'int' value from input stream, but the next token is \"" + token + "\"");
}
catch (NoSuchElementException e) {
throw new NoSuchElementException("attemps to read an 'int' value from input stream, but there are no more tokens available");
}
}
private void readFile(String filename){
assert filename != null;
try {
File file = new File(filename);
if (file.exists()) {
FileInputStream fis = new FileInputStream(file);
scanner = new Scanner(new BufferedInputStream(fis), "UTF-8");
scanner.useLocale(Locale.ENGLISH);
}
else
throw new IllegalArgumentException(filename + " doesn't exist.");
}
catch (IOException ioe) {
throw new IllegalArgumentException("Could not open " + filename, ioe);
}
}
}
// 稀疏图 - 邻接表
public class SparseWeightedGraph<Weight extends Number & Comparable>
implements WeightedGraph {
private int n; // 节点数
private int m; // 边数
private boolean directed; // 是否为有向图
private Vector<Edge<Weight>>[] g; // 图的具体数据
// 构造函数
public SparseWeightedGraph( int n , boolean directed ){
assert n >= 0;
this.n = n;
this.m = 0; // 初始化没有任何边
this.directed = directed;
// g初始化为n个空的vector, 表示每一个g[i]都为空, 即没有任和边
g = (Vector<Edge<Weight>>[])new Vector[n];
for(int i = 0 ; i < n ; i ++)
g[i] = new Vector<Edge<Weight>>();
}
public int V(){ return n;} // 返回节点个数
public int E(){ return m;} // 返回边的个数
// 向图中添加一个边, 权值为weight
public void addEdge(Edge e){
assert e.v() >= 0 && e.v() < n ;
assert e.w() >= 0 && e.w() < n ;
// 注意, 由于在邻接表的情况, 查找是否有重边需要遍历整个链表
// 我们的程序允许重边的出现
g[e.v()].add(new Edge(e));
if( e.v() != e.w() && !directed )
g[e.w()].add(new Edge(e.w(), e.v(), e.wt()));
m ++;
}
// 验证图中是否有从v到w的边
public boolean hasEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
for( int i = 0 ; i < g[v].size() ; i ++ )
if( g[v].elementAt(i).other(v) == w )
return true;
return false;
}
// 显示图的信息
public void show(){
for( int i = 0 ; i < n ; i ++ ){
System.out.print("vertex " + i + ":\t");
for( int j = 0 ; j < g[i].size() ; j ++ ){
Edge e = g[i].elementAt(j);
System.out.print( "( to:" + e.other(i) + ",wt:" + e.wt() + ")\t");
}
System.out.println();
}
}
// 返回图中一个顶点的所有邻边
// 由于java使用引用机制,返回一个Vector不会带来额外开销,
public Iterable<Edge<Weight>> adj(int v) {
assert v >= 0 && v < n;
return g[v];
}
}
interface WeightedGraph<Weight extends Number & Comparable> {
public int V();
public int E();
public void addEdge(Edge<Weight> e);
boolean hasEdge( int v , int w );
void show();
public Iterable<Edge<Weight>> adj(int v);
}
8-6 Krusk算法
// 稠密图 - 邻接矩阵
public class DenseWeightedGraph<Weight extends Number & Comparable>
implements WeightedGraph{
private int n; // 节点数
private int m; // 边数
private boolean directed; // 是否为有向图
private Edge<Weight>[][] g; // 图的具体数据
// 构造函数
public DenseWeightedGraph( int n , boolean directed ){
assert n >= 0;
this.n = n;
this.m = 0; // 初始化没有任何边
this.directed = directed;
// g初始化为n*n的布尔矩阵, 每一个g[i][j]均为null, 表示没有任和边
// false为boolean型变量的默认值
g = new Edge[n][n];
for(int i = 0 ; i < n ; i ++)
for(int j = 0 ; j < n ; j ++)
g[i][j] = null;
}
public int V(){ return n;} // 返回节点个数
public int E(){ return m;} // 返回边的个数
// 向图中添加一个边
public void addEdge(Edge e){
assert e.v() >= 0 && e.v() < n ;
assert e.w() >= 0 && e.w() < n ;
if( hasEdge( e.v() , e.w() ) )
return;
g[e.v()][e.w()] = new Edge(e);
if( e.v() != e.w() && !directed )
g[e.w()][e.v()] = new Edge(e.w(), e.v(), e.wt());
m ++;
}
// 验证图中是否有从v到w的边
public boolean hasEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
return g[v][w] != null;
}
// 显示图的信息
public void show(){
for( int i = 0 ; i < n ; i ++ ){
for( int j = 0 ; j < n ; j ++ )
if( g[i][j] != null )
System.out.print(g[i][j].wt()+"\t");
else
System.out.print("NULL\t");
System.out.println();
}
}
// 返回图中一个顶点的所有邻边
// 由于java使用引用机制,返回一个Vector不会带来额外开销,
public Iterable<Edge<Weight>> adj(int v) {
assert v >= 0 && v < n;
Vector<Edge<Weight>> adjV = new Vector<Edge<Weight>>();
for(int i = 0 ; i < n ; i ++ )
if( g[v][i] != null )
adjV.add( g[v][i] );
return adjV;
}
}
// 边
public class Edge<Weight extends Number & Comparable> implements Comparable<Edge<Weight>>{
private int a, b; // 边的两个端点
private Weight weight; // 边的权值
public Edge(int a, int b, Weight weight)
{
this.a = a;
this.b = b;
this.weight = weight;
}
public Edge(Edge<Weight> e)
{
this.a = e.a;
this.b = e.b;
this.weight = e.weight;
}
public int v(){ return a;} // 返回第一个顶点
public int w(){ return b;} // 返回第二个顶点
public Weight wt(){ return weight;} // 返回权值
// 给定一个顶点, 返回另一个顶点
public int other(int x){
assert x == a || x == b;
return x == a ? b : a;
}
// 输出边的信息
public String toString(){
return "" + a + "-" + b + ": " + weight;
}
// 边之间的比较
public int compareTo(Edge<Weight> that)
{
if( weight.compareTo(that.wt()) < 0 )
return -1;
else if ( weight.compareTo(that.wt()) > 0 )
return +1;
else
return 0;
}
}
// 最小索引堆
public class IndexMinHeap<Item extends Comparable> {
protected Item[] data; // 最小索引堆中的数据
protected int[] indexes; // 最小索引堆中的索引, indexes[x] = i 表示索引i在x的位置
protected int[] reverse; // 最小索引堆中的反向索引, reverse[i] = x 表示索引i在x的位置
protected int count;
protected int capacity;
// 构造函数, 构造一个空堆, 可容纳capacity个元素
public IndexMinHeap(int capacity){
data = (Item[])new Comparable[capacity+1];
indexes = new int[capacity+1];
reverse = new int[capacity+1];
for( int i = 0 ; i <= capacity ; i ++ )
reverse[i] = 0;
count = 0;
this.capacity = capacity;
}
// 返回索引堆中的元素个数
public int size(){
return count;
}
// 返回一个布尔值, 表示索引堆中是否为空
public boolean isEmpty(){
return count == 0;
}
// 向最小索引堆中插入一个新的元素, 新元素的索引为i, 元素为item
// 传入的i对用户而言,是从0索引的
public void insert(int i, Item item){
assert count + 1 <= capacity;
assert i + 1 >= 1 && i + 1 <= capacity;
// 再插入一个新元素前,还需要保证索引i所在的位置是没有元素的。
assert !contain(i);
i += 1;
data[i] = item;
indexes[count+1] = i;
reverse[i] = count + 1;
count ++;
shiftUp(count);
}
// 从最小索引堆中取出堆顶元素, 即索引堆中所存储的最小数据
public Item extractMin(){
assert count > 0;
Item ret = data[indexes[1]];
swapIndexes( 1 , count );
reverse[indexes[count]] = 0;
count --;
shiftDown(1);
return ret;
}
// 从最小索引堆中取出堆顶元素的索引
public int extractMinIndex(){
assert count > 0;
int ret = indexes[1] - 1;
swapIndexes( 1 , count );
reverse[indexes[count]] = 0;
count --;
shiftDown(1);
return ret;
}
// 获取最小索引堆中的堆顶元素
public Item getMin(){
assert count > 0;
return data[indexes[1]];
}
// 获取最小索引堆中的堆顶元素的索引
public int getMinIndex(){
assert count > 0;
return indexes[1]-1;
}
// 看索引i所在的位置是否存在元素
boolean contain( int i ){
assert i + 1 >= 1 && i + 1 <= capacity;
return reverse[i+1] != 0;
}
// 获取最小索引堆中索引为i的元素
public Item getItem( int i ){
assert contain(i);
return data[i+1];
}
// 将最小索引堆中索引为i的元素修改为newItem
public void change( int i , Item newItem ){
assert contain(i);
i += 1;
data[i] = newItem;
// 有了 reverse 之后,
// 我们可以非常简单的通过reverse直接定位索引i在indexes中的位置
shiftUp( reverse[i] );
shiftDown( reverse[i] );
}
// 交换索引堆中的索引i和j
// 由于有了反向索引reverse数组,
// indexes数组发生改变以后, 相应的就需要维护reverse数组
private void swapIndexes(int i, int j){
int t = indexes[i];
indexes[i] = indexes[j];
indexes[j] = t;
reverse[indexes[i]] = i;
reverse[indexes[j]] = j;
}
//********************
//* 最小索引堆核心辅助函数
//********************
// 索引堆中, 数据之间的比较根据data的大小进行比较, 但实际操作的是索引
private void shiftUp(int k){
while( k > 1 && data[indexes[k/2]].compareTo(data[indexes[k]]) > 0 ){
swapIndexes(k, k/2);
k /= 2;
}
}
// 索引堆中, 数据之间的比较根据data的大小进行比较, 但实际操作的是索引
private void shiftDown(int k){
while( 2*k <= count ){
int j = 2*k;
if( j+1 <= count && data[indexes[j+1]].compareTo(data[indexes[j]]) < 0 )
j ++;
if( data[indexes[k]].compareTo(data[indexes[j]]) <= 0 )
break;
swapIndexes(k, j);
k = j;
}
}
// 测试 IndexMinHeap
public static void main(String[] args) {
int N = 1000000;
IndexMinHeap<Integer> indexMinHeap = new IndexMinHeap<Integer>(N);
for( int i = 0 ; i < N ; i ++ )
indexMinHeap.insert( i , (int)(Math.random()*N) );
}
}
// Kruskal算法求最小生成树
public class KruskalMST<Weight extends Number & Comparable> {
private Vector<Edge<Weight>> mst; // 最小生成树所包含的所有边
private Number mstWeight; // 最小生成树的权值
// 构造函数, 使用Kruskal算法计算graph的最小生成树
public KruskalMST(WeightedGraph graph){
mst = new Vector<Edge<Weight>>();
// 将图中的所有边存放到一个最小堆中
MinHeap<Edge<Weight>> pq = new MinHeap<Edge<Weight>>( graph.E() );
for( int i = 0 ; i < graph.V() ; i ++ )
for( Object item : graph.adj(i) ){
Edge<Weight> e = (Edge<Weight>)item;
if( e.v() <= e.w() )
pq.insert(e);
}
// 创建一个并查集, 来查看已经访问的节点的联通情况
UnionFind uf = new UnionFind(graph.V());
while( !pq.isEmpty() && mst.size() < graph.V() - 1 ){
// 从最小堆中依次从小到大取出所有的边
Edge<Weight> e = pq.extractMin();
// 如果该边的两个端点是联通的, 说明加入这条边将产生环, 扔掉这条边
if( uf.isConnected( e.v() , e.w() ) )
continue;
// 否则, 将这条边添加进最小生成树, 同时标记边的两个端点联通
mst.add( e );
uf.unionElements( e.v() , e.w() );
}
// 计算最小生成树的权值
mstWeight = mst.elementAt(0).wt();
for( int i = 1 ; i < mst.size() ; i ++ )
mstWeight = mstWeight.doubleValue() + mst.elementAt(i).wt().doubleValue();
}
// 返回最小生成树的所有边
Vector<Edge<Weight>> mstEdges(){
return mst;
}
// 返回最小生成树的权值
Number result(){
return mstWeight;
}
// 测试 Kruskal
public static void main(String[] args) {
String filename = "testG1.txt";
int V = 8;
SparseWeightedGraph<Double> g = new SparseWeightedGraph<Double>(V, false);
ReadWeightedGraph readGraph = new ReadWeightedGraph(g, filename);
// Test Kruskal
System.out.println("Test Kruskal:");
KruskalMST<Double> kruskalMST = new KruskalMST<Double>(g);
Vector<Edge<Double>> mst = kruskalMST.mstEdges();
for( int i = 0 ; i < mst.size() ; i ++ )
System.out.println(mst.elementAt(i));
System.out.println("The MST weight is: " + kruskalMST.result());
System.out.println();
}
}
// 使用Prim算法求图的最小生成树
public class LazyPrimMST<Weight extends Number & Comparable> {
private WeightedGraph<Weight> G; // 图的引用
private MinHeap<Edge<Weight>> pq; // 最小堆, 算法辅助数据结构
private boolean[] marked; // 标记数组, 在算法运行过程中标记节点i是否被访问
private Vector<Edge<Weight>> mst; // 最小生成树所包含的所有边
private Number mstWeight; // 最小生成树的权值
// 构造函数, 使用Prim算法求图的最小生成树
public LazyPrimMST(WeightedGraph<Weight> graph){
// 算法初始化
G = graph;
pq = new MinHeap<Edge<Weight>>(G.E());
marked = new boolean[G.V()];
mst = new Vector<Edge<Weight>>();
// Lazy Prim
visit(0);
while( !pq.isEmpty() ){
// 使用最小堆找出已经访问的边中权值最小的边
Edge<Weight> e = pq.extractMin();
// 如果这条边的两端都已经访问过了, 则扔掉这条边
if( marked[e.v()] == marked[e.w()] )
continue;
// 否则, 这条边则应该存在在最小生成树中
mst.add( e );
// 访问和这条边连接的还没有被访问过的节点
if( !marked[e.v()] )
visit( e.v() );
else
visit( e.w() );
}
// 计算最小生成树的权值
mstWeight = mst.elementAt(0).wt();
for( int i = 1 ; i < mst.size() ; i ++ )
mstWeight = mstWeight.doubleValue() + mst.elementAt(i).wt().doubleValue();
}
// 访问节点v
private void visit(int v){
assert !marked[v];
marked[v] = true;
// 将和节点v相连接的所有未访问的边放入最小堆中
for( Edge<Weight> e : G.adj(v) )
if( !marked[e.other(v)] )
pq.insert(e);
}
// 返回最小生成树的所有边
Vector<Edge<Weight>> mstEdges(){
return mst;
}
// 返回最小生成树的权值
Number result(){
return mstWeight;
}
// 测试 Lazy Prim
public static void main(String[] args) {
String filename = "testG1.txt";
int V = 8;
SparseWeightedGraph<Double> g = new SparseWeightedGraph<Double>(V, false);
ReadWeightedGraph readGraph = new ReadWeightedGraph(g, filename);
// Test Lazy Prim MST
System.out.println("Test Lazy Prim MST:");
LazyPrimMST<Double> lazyPrimMST = new LazyPrimMST<Double>(g);
Vector<Edge<Double>> mst = lazyPrimMST.mstEdges();
for( int i = 0 ; i < mst.size() ; i ++ )
System.out.println(mst.elementAt(i));
System.out.println("The MST weight is: " + lazyPrimMST.result());
System.out.println();
}
}
public class Main {
// 比较Lazy Prim, Prim和Kruskal的时间性能
public static void main(String[] args) {
String filename1 = "E:\\workspace\\Play-with-Algorithms\\src\\com\\imooc\\ch8_6\\testG1.txt";
int V1 = 8;
String filename2 = "E:\\workspace\\Play-with-Algorithms\\src\\com\\imooc\\ch8_6\\testG2.txt";
int V2 = 250;
String filename3 = "E:\\workspace\\Play-with-Algorithms\\src\\com\\imooc\\ch8_6\\testG3.txt";
int V3 = 1000;
String filename4 = "E:\\workspace\\Play-with-Algorithms\\src\\com\\imooc\\ch8_6\\testG4.txt";
int V4 = 10000;
//String filename5 = "testG5.txt";
//int V5 = 1000000;
// 文件读取
SparseWeightedGraph<Double> g1 = new SparseWeightedGraph<Double>(V1, false);
ReadWeightedGraph readGraph1 = new ReadWeightedGraph(g1, filename1);
System.out.println( filename1 + " load successfully.");
SparseWeightedGraph<Double> g2 = new SparseWeightedGraph<Double>(V2, false);
ReadWeightedGraph readGraph2 = new ReadWeightedGraph(g2, filename2);
System.out.println( filename2 + " load successfully.");
SparseWeightedGraph<Double> g3 = new SparseWeightedGraph<Double>(V3, false);
ReadWeightedGraph readGraph3 = new ReadWeightedGraph(g3, filename3);
System.out.println( filename3 + " load successfully.");
SparseWeightedGraph<Double> g4 = new SparseWeightedGraph<Double>(V4, false);
ReadWeightedGraph readGraph4 = new ReadWeightedGraph(g4, filename4);
System.out.println( filename4 + " load successfully.");
// SparseWeightedGraph<Double> g5 = new SparseWeightedGraph<Double>(V5, false);
// ReadWeightedGraph readGraph5 = new ReadWeightedGraph(g5, filename5);
// System.out.println( filename5 + " load successfully.");
System.out.println();
long startTime, endTime;
// Test Lazy Prim MST
System.out.println("Test Lazy Prim MST:");
startTime = System.currentTimeMillis();
LazyPrimMST<Double> lazyPrimMST1 = new LazyPrimMST<Double>(g1);
endTime = System.currentTimeMillis();
System.out.println("Test for G1: " + (endTime-startTime) + "ms.");
startTime = System.currentTimeMillis();
LazyPrimMST<Double> lazyPrimMST2 = new LazyPrimMST<Double>(g2);
endTime = System.currentTimeMillis();
System.out.println("Test for G2: " + (endTime-startTime) + "ms.");
startTime = System.currentTimeMillis();
LazyPrimMST<Double> lazyPrimMST3 = new LazyPrimMST<Double>(g3);
endTime = System.currentTimeMillis();
System.out.println("Test for G3: " + (endTime-startTime) + "ms.");
startTime = System.currentTimeMillis();
LazyPrimMST<Double> lazyPrimMST4 = new LazyPrimMST<Double>(g4);
endTime = System.currentTimeMillis();
System.out.println("Test for G4: " + (endTime-startTime) + "ms.");
// startTime = System.currentTimeMillis();
// LazyPrimMST<Double> lazyPrimMST5 = new LazyPrimMST<Double>(g5);
// endTime = System.currentTimeMillis();
// System.out.println("Test for G5: " + (endTime-startTime) + "ms.");
System.out.println();
// Test Prim MST
System.out.println("Test Prim MST:");
startTime = System.currentTimeMillis();
PrimMST<Double> primMST1 = new PrimMST<Double>(g1);
endTime = System.currentTimeMillis();
System.out.println("Test for G1: " + (endTime-startTime) + "ms.");
startTime = System.currentTimeMillis();
PrimMST<Double> primMST2 = new PrimMST<Double>(g2);
endTime = System.currentTimeMillis();
System.out.println("Test for G2: " + (endTime-startTime) + "ms.");
startTime = System.currentTimeMillis();
PrimMST<Double> primMST3 = new PrimMST<Double>(g3);
endTime = System.currentTimeMillis();
System.out.println("Test for G3: " + (endTime-startTime) + "ms.");
startTime = System.currentTimeMillis();
PrimMST<Double> primMST4 = new PrimMST<Double>(g4);
endTime = System.currentTimeMillis();
System.out.println("Test for G4: " + (endTime-startTime) + "ms.");
// startTime = System.currentTimeMillis();
// PrimMST<Double> primMST5 = new PrimMST<Double>(g5);
// endTime = System.currentTimeMillis();
// System.out.println("Test for G5: " + (endTime-startTime) + "ms.");
System.out.println();
// Test Kruskal MST
System.out.println("Test Kruskal MST:");
startTime = System.currentTimeMillis();
KruskalMST<Double> kruskalMST1 = new KruskalMST<Double>(g1);
endTime = System.currentTimeMillis();
System.out.println("Test for G1: " + (endTime-startTime) + "ms.");
startTime = System.currentTimeMillis();
KruskalMST<Double> kruskalMST2 = new KruskalMST<Double>(g2);
endTime = System.currentTimeMillis();
System.out.println("Test for G2: " + (endTime-startTime) + "ms.");
startTime = System.currentTimeMillis();
KruskalMST<Double> kruskalMST3 = new KruskalMST<Double>(g3);
endTime = System.currentTimeMillis();
System.out.println("Test for G3: " + (endTime-startTime) + "ms.");
startTime = System.currentTimeMillis();
KruskalMST<Double> kruskalMST4 = new KruskalMST<Double>(g4);
endTime = System.currentTimeMillis();
System.out.println("Test for G4: " + (endTime-startTime) + "ms.");
// startTime = System.currentTimeMillis();
// KruskalMST<Double> kruskalMST5 = new KruskalMST<Double>(g5);
// endTime = System.currentTimeMillis();
// System.out.println("Test for G5: " + (endTime-startTime) + "ms.");
System.out.println();
}
}
// 在堆的有关操作中,需要比较堆中元素的大小,所以Item需要extends Comparable
public class MinHeap<Item extends Comparable> {
protected Item[] data;
protected int count;
protected int capacity;
// 构造函数, 构造一个空堆, 可容纳capacity个元素
public MinHeap(int capacity){
data = (Item[])new Comparable[capacity+1];
count = 0;
this.capacity = capacity;
}
// 构造函数, 通过一个给定数组创建一个最小堆
// 该构造堆的过程, 时间复杂度为O(n)
public MinHeap(Item arr[]){
int n = arr.length;
data = (Item[])new Comparable[n+1];
capacity = n;
for( int i = 0 ; i < n ; i ++ )
data[i+1] = arr[i];
count = n;
for( int i = count/2 ; i >= 1 ; i -- )
shiftDown(i);
}
// 返回堆中的元素个数
public int size(){
return count;
}
// 返回一个布尔值, 表示堆中是否为空
public boolean isEmpty(){
return count == 0;
}
// 向最小堆中插入一个新的元素 item
public void insert(Item item){
assert count + 1 <= capacity;
data[count+1] = item;
count ++;
shiftUp(count);
}
// 从最小堆中取出堆顶元素, 即堆中所存储的最小数据
public Item extractMin(){
assert count > 0;
Item ret = data[1];
swap( 1 , count );
count --;
shiftDown(1);
return ret;
}
// 获取最小堆中的堆顶元素
public Item getMin(){
assert( count > 0 );
return data[1];
}
// 交换堆中索引为i和j的两个元素
private void swap(int i, int j){
Item t = data[i];
data[i] = data[j];
data[j] = t;
}
//********************
//* 最小堆核心辅助函数
//********************
private void shiftUp(int k){
while( k > 1 && data[k/2].compareTo(data[k]) > 0 ){
swap(k, k/2);
k /= 2;
}
}
private void shiftDown(int k){
while( 2*k <= count ){
int j = 2*k; // 在此轮循环中,data[k]和data[j]交换位置
if( j+1 <= count && data[j+1].compareTo(data[j]) < 0 )
j ++;
// data[j] 是 data[2*k]和data[2*k+1]中的最小值
if( data[k].compareTo(data[j]) <= 0 ) break;
swap(k, j);
k = j;
}
}
// 测试 MinHeap
public static void main(String[] args) {
MinHeap<Integer> minHeap = new MinHeap<Integer>(100);
int N = 100; // 堆中元素个数
int M = 100; // 堆中元素取值范围[0, M)
for( int i = 0 ; i < N ; i ++ )
minHeap.insert( new Integer((int)(Math.random() * M)) );
Integer[] arr = new Integer[N];
// 将minheap中的数据逐渐使用extractMin取出来
// 取出来的顺序应该是按照从小到大的顺序取出来的
for( int i = 0 ; i < N ; i ++ ){
arr[i] = minHeap.extractMin();
System.out.print(arr[i] + " ");
}
System.out.println();
// 确保arr数组是从小到大排列的
for( int i = 1 ; i < N ; i ++ )
assert arr[i-1] <= arr[i];
}
}
// 使用优化的Prim算法求图的最小生成树
public class PrimMST<Weight extends Number & Comparable> {
private WeightedGraph G; // 图的引用
private IndexMinHeap<Weight> ipq; // 最小索引堆, 算法辅助数据结构
private Edge<Weight>[] edgeTo; // 访问的点所对应的边, 算法辅助数据结构
private boolean[] marked; // 标记数组, 在算法运行过程中标记节点i是否被访问
private Vector<Edge<Weight>> mst; // 最小生成树所包含的所有边
private Number mstWeight; // 最小生成树的权值
// 构造函数, 使用Prim算法求图的最小生成树
public PrimMST(WeightedGraph graph){
G = graph;
assert( graph.E() >= 1 );
ipq = new IndexMinHeap<Weight>(graph.V());
// 算法初始化
marked = new boolean[G.V()];
edgeTo = new Edge[G.V()];
for( int i = 0 ; i < G.V() ; i ++ ){
marked[i] = false;
edgeTo[i] = null;
}
mst = new Vector<Edge<Weight>>();
// Prim
visit(0);
while( !ipq.isEmpty() ){
// 使用最小索引堆找出已经访问的边中权值最小的边
// 最小索引堆中存储的是点的索引, 通过点的索引找到相对应的边
int v = ipq.extractMinIndex();
assert( edgeTo[v] != null );
mst.add( edgeTo[v] );
visit( v );
}
// 计算最小生成树的权值
mstWeight = mst.elementAt(0).wt();
for( int i = 1 ; i < mst.size() ; i ++ )
mstWeight = mstWeight.doubleValue() + mst.elementAt(i).wt().doubleValue();
}
// 访问节点v
void visit(int v){
assert !marked[v];
marked[v] = true;
// 将和节点v相连接的未访问的另一端点, 和与之相连接的边, 放入最小堆中
for( Object item : G.adj(v) ){
Edge<Weight> e = (Edge<Weight>)item;
int w = e.other(v);
// 如果边的另一端点未被访问
if( !marked[w] ){
// 如果从没有考虑过这个端点, 直接将这个端点和与之相连接的边加入索引堆
if( edgeTo[w] == null ){
edgeTo[w] = e;
ipq.insert(w, e.wt());
}
// 如果曾经考虑这个端点, 但现在的边比之前考虑的边更短, 则进行替换
else if( e.wt().compareTo(edgeTo[w].wt()) < 0 ){
edgeTo[w] = e;
ipq.change(w, e.wt());
}
}
}
}
// 返回最小生成树的所有边
Vector<Edge<Weight>> mstEdges(){
return mst;
}
// 返回最小生成树的权值
Number result(){
return mstWeight;
}
// 测试 Prim
public static void main(String[] args) {
String filename = "testG1.txt";
int V = 8;
SparseWeightedGraph<Double> g = new SparseWeightedGraph<Double>(V, false);
ReadWeightedGraph readGraph = new ReadWeightedGraph(g, filename);
// Test Prim MST
System.out.println("Test Prim MST:");
PrimMST<Double> primMST = new PrimMST<Double>(g);
Vector<Edge<Double>> mst = primMST.mstEdges();
for( int i = 0 ; i < mst.size() ; i ++ )
System.out.println(mst.elementAt(i));
System.out.println("The MST weight is: " + primMST.result());
System.out.println();
}
}
// 通过文件读取有全图的信息
public class ReadWeightedGraph {
private Scanner scanner;
// 由于文件格式的限制,我们的文件读取类只能读取权值为Double类型的图
public ReadWeightedGraph(WeightedGraph<Double> graph, String filename){
readFile(filename);
try {
int V = scanner.nextInt();
if (V < 0)
throw new IllegalArgumentException("number of vertices in a Graph must be nonnegative");
assert V == graph.V();
int E = scanner.nextInt();
if (E < 0)
throw new IllegalArgumentException("number of edges in a Graph must be nonnegative");
for (int i = 0; i < E; i++) {
int v = scanner.nextInt();
int w = scanner.nextInt();
Double weight = scanner.nextDouble();
assert v >= 0 && v < V;
assert w >= 0 && w < V;
graph.addEdge(new Edge<Double>(v, w, weight));
}
}
catch (InputMismatchException e) {
String token = scanner.next();
throw new InputMismatchException("attempts to read an 'int' value from input stream, but the next token is \"" + token + "\"");
}
catch (NoSuchElementException e) {
throw new NoSuchElementException("attemps to read an 'int' value from input stream, but there are no more tokens available");
}
}
private void readFile(String filename){
assert filename != null;
try {
File file = new File(filename);
if (file.exists()) {
FileInputStream fis = new FileInputStream(file);
scanner = new Scanner(new BufferedInputStream(fis), "UTF-8");
scanner.useLocale(Locale.ENGLISH);
}
else
throw new IllegalArgumentException(filename + " doesn't exist.");
}
catch (IOException ioe) {
throw new IllegalArgumentException("Could not open " + filename, ioe);
}
}
}
// 稀疏图 - 邻接表
public class SparseWeightedGraph<Weight extends Number & Comparable>
implements WeightedGraph {
private int n; // 节点数
private int m; // 边数
private boolean directed; // 是否为有向图
private Vector<Edge<Weight>>[] g; // 图的具体数据
// 构造函数
public SparseWeightedGraph( int n , boolean directed ){
assert n >= 0;
this.n = n;
this.m = 0; // 初始化没有任何边
this.directed = directed;
// g初始化为n个空的vector, 表示每一个g[i]都为空, 即没有任和边
g = (Vector<Edge<Weight>>[])new Vector[n];
for(int i = 0 ; i < n ; i ++)
g[i] = new Vector<Edge<Weight>>();
}
public int V(){ return n;} // 返回节点个数
public int E(){ return m;} // 返回边的个数
// 向图中添加一个边, 权值为weight
public void addEdge(Edge e){
assert e.v() >= 0 && e.v() < n ;
assert e.w() >= 0 && e.w() < n ;
// 注意, 由于在邻接表的情况, 查找是否有重边需要遍历整个链表
// 我们的程序允许重边的出现
g[e.v()].add(new Edge(e));
if( e.v() != e.w() && !directed )
g[e.w()].add(new Edge(e.w(), e.v(), e.wt()));
m ++;
}
// 验证图中是否有从v到w的边
public boolean hasEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
for( int i = 0 ; i < g[v].size() ; i ++ )
if( g[v].elementAt(i).other(v) == w )
return true;
return false;
}
// 显示图的信息
public void show(){
for( int i = 0 ; i < n ; i ++ ){
System.out.print("vertex " + i + ":\t");
for( int j = 0 ; j < g[i].size() ; j ++ ){
Edge e = g[i].elementAt(j);
System.out.print( "( to:" + e.other(i) + ",wt:" + e.wt() + ")\t");
}
System.out.println();
}
}
// 返回图中一个顶点的所有邻边
// 由于java使用引用机制,返回一个Vector不会带来额外开销,
public Iterable<Edge<Weight>> adj(int v) {
assert v >= 0 && v < n;
return g[v];
}
}
// Union-Find
public class UnionFind {
// rank[i]表示以i为根的集合所表示的树的层数
// 在后续的代码中, 我们并不会维护rank的语意, 也就是rank的值在路径压缩的过程中, 有可能不在是树的层数值
// 这也是我们的rank不叫height或者depth的原因, 他只是作为比较的一个标准
// 关于这个问题,可以参考问答区:http://coding.imooc.com/learn/questiondetail/7287.html
private int[] rank;
private int[] parent; // parent[i]表示第i个元素所指向的父节点
private int count; // 数据个数
// 构造函数
public UnionFind(int count){
rank = new int[count];
parent = new int[count];
this.count = count;
// 初始化, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合
for( int i = 0 ; i < count ; i ++ ){
parent[i] = i;
rank[i] = 1;
}
}
// 查找过程, 查找元素p所对应的集合编号
// O(h)复杂度, h为树的高度
int find(int p){
assert( p >= 0 && p < count );
// path compression 1
while( p != parent[p] ){
parent[p] = parent[parent[p]];
p = parent[p];
}
return p;
}
// 查看元素p和元素q是否所属一个集合
// O(h)复杂度, h为树的高度
boolean isConnected( int p , int q ){
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(h)复杂度, h为树的高度
void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if( pRoot == qRoot )
return;
// 根据两个元素所在树的元素个数不同判断合并方向
// 将元素个数少的集合合并到元素个数多的集合上
if( rank[pRoot] < rank[qRoot] ){
parent[pRoot] = qRoot;
}
else if( rank[qRoot] < rank[pRoot]){
parent[qRoot] = pRoot;
}
else{ // rank[pRoot] == rank[qRoot]
parent[pRoot] = qRoot;
rank[qRoot] += 1; // 此时, 我维护rank的值
}
}
}
interface WeightedGraph<Weight extends Number & Comparable> {
public int V();
public int E();
public void addEdge(Edge<Weight> e);
boolean hasEdge( int v , int w );
void show();
public Iterable<Edge<Weight>> adj(int v);
}
8-7 最小生成树算法的思考
Lazy Prim O(ElogE)
Prim O(ElogV)
Kruskal O(ElogE)
如果横切边有相等的边
根据算法的具体实现,每次选择一个边
此时,图存在多个最小生成树
Vyssotsky’s Algorithm
将边逐渐添加到生成树中
一旦形成环,删除环中权值最大的边
第9章 最短路径
9-1 最短路径问题和松弛操作
路径规划
工作任务规划
最短路径树
Shortest Path Tree
单源最短路径
Single Source Shortest Path
松弛操作 Relaxation
松弛操作是最短路径求解的核心
9-2 Dijkstra算法的思想
dijkstra单源最短路径算法
前提:图中不能有负权边
复杂度O(Elog(V))
9-3 实现Dijkstra算法
// 稠密图 - 邻接矩阵
public class DenseWeightedGraph<Weight extends Number & Comparable>
implements WeightedGraph{
private int n; // 节点数
private int m; // 边数
private boolean directed; // 是否为有向图
private Edge<Weight>[][] g; // 图的具体数据
// 构造函数
public DenseWeightedGraph( int n , boolean directed ){
assert n >= 0;
this.n = n;
this.m = 0; // 初始化没有任何边
this.directed = directed;
// g初始化为n*n的布尔矩阵, 每一个g[i][j]均为null, 表示没有任和边
// false为boolean型变量的默认值
g = new Edge[n][n];
for(int i = 0 ; i < n ; i ++)
for(int j = 0 ; j < n ; j ++)
g[i][j] = null;
}
public int V(){ return n;} // 返回节点个数
public int E(){ return m;} // 返回边的个数
// 向图中添加一个边
public void addEdge(Edge e){
assert e.v() >= 0 && e.v() < n ;
assert e.w() >= 0 && e.w() < n ;
if( hasEdge( e.v() , e.w() ) )
return;
g[e.v()][e.w()] = new Edge(e);
if( e.v() != e.w() && !directed )
g[e.w()][e.v()] = new Edge(e.w(), e.v(), e.wt());
m ++;
}
// 验证图中是否有从v到w的边
public boolean hasEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
return g[v][w] != null;
}
// 显示图的信息
public void show(){
for( int i = 0 ; i < n ; i ++ ){
for( int j = 0 ; j < n ; j ++ )
if( g[i][j] != null )
System.out.print(g[i][j].wt()+"\t");
else
System.out.print("NULL\t");
System.out.println();
}
}
// 返回图中一个顶点的所有邻边
// 由于java使用引用机制,返回一个Vector不会带来额外开销,
public Iterable<Edge<Weight>> adj(int v) {
assert v >= 0 && v < n;
Vector<Edge<Weight>> adjV = new Vector<Edge<Weight>>();
for(int i = 0 ; i < n ; i ++ )
if( g[v][i] != null )
adjV.add( g[v][i] );
return adjV;
}
}
// Dijkstra算法求最短路径
public class Dijkstra<Weight extends Number & Comparable> {
private WeightedGraph G; // 图的引用
private int s; // 起始点
private Number[] distTo; // distTo[i]存储从起始点s到i的最短路径长度
private boolean[] marked; // 标记数组, 在算法运行过程中标记节点i是否被访问
private Edge<Weight>[] from; // from[i]记录最短路径中, 到达i点的边是哪一条
// 可以用来恢复整个最短路径
// 构造函数, 使用Dijkstra算法求最短路径
public Dijkstra(WeightedGraph graph, int s){
// 算法初始化
G = graph;
assert s >= 0 && s < G.V();
this.s = s;
distTo = new Number[G.V()];
marked = new boolean[G.V()];
from = new Edge[G.V()];
for( int i = 0 ; i < G.V() ; i ++ ){
distTo[i] = 0.0;
marked[i] = false;
from[i] = null;
}
// 使用索引堆记录当前找到的到达每个顶点的最短距离
IndexMinHeap<Weight> ipq = new IndexMinHeap<Weight>(G.V());
// 对于其实点s进行初始化
distTo[s] = 0.0;
from[s] = new Edge<Weight>(s, s, (Weight)(Number)(0.0));
ipq.insert(s, (Weight)distTo[s] );
marked[s] = true;
while( !ipq.isEmpty() ){
int v = ipq.extractMinIndex();
// distTo[v]就是s到v的最短距离
marked[v] = true;
// 对v的所有相邻节点进行更新
for( Object item : G.adj(v) ){
Edge<Weight> e = (Edge<Weight>)item;
int w = e.other(v);
// 如果从s点到w点的最短路径还没有找到
if( !marked[w] ){
// 如果w点以前没有访问过,
// 或者访问过, 但是通过当前的v点到w点距离更短, 则进行更新
if( from[w] == null || distTo[v].doubleValue() + e.wt().doubleValue() < distTo[w].doubleValue() ){
distTo[w] = distTo[v].doubleValue() + e.wt().doubleValue();
from[w] = e;
if( ipq.contain(w) )
ipq.change(w, (Weight)distTo[w] );
else
ipq.insert(w, (Weight)distTo[w] );
}
}
}
}
}
// 返回从s点到w点的最短路径长度
Number shortestPathTo( int w ){
assert w >= 0 && w < G.V();
assert hasPathTo(w);
return distTo[w];
}
// 判断从s点到w点是否联通
boolean hasPathTo( int w ){
assert w >= 0 && w < G.V() ;
return marked[w];
}
// 寻找从s到w的最短路径, 将整个路径经过的边存放在vec中
Vector<Edge<Weight>> shortestPath( int w){
assert w >= 0 && w < G.V();
assert hasPathTo(w);
// 通过from数组逆向查找到从s到w的路径, 存放到栈中
Stack<Edge<Weight>> s = new Stack<Edge<Weight>>();
Edge<Weight> e = from[w];
while( e.v() != this.s ){
s.push(e);
e = from[e.v()];
}
s.push(e);
// 从栈中依次取出元素, 获得顺序的从s到w的路径
Vector<Edge<Weight>> res = new Vector<Edge<Weight>>();
while( !s.empty() ){
e = s.pop();
res.add( e );
}
return res;
}
// 打印出从s点到w点的路径
void showPath(int w){
assert w >= 0 && w < G.V();
assert hasPathTo(w);
Vector<Edge<Weight>> path = shortestPath(w);
for( int i = 0 ; i < path.size() ; i ++ ){
System.out.print( path.elementAt(i).v() + " -> ");
if( i == path.size()-1 )
System.out.println(path.elementAt(i).w());
}
}
}
// 边
public class Edge<Weight extends Number & Comparable> implements Comparable<Edge<Weight>>{
private int a, b; // 边的两个端点
private Weight weight; // 边的权值
public Edge(int a, int b, Weight weight)
{
this.a = a;
this.b = b;
this.weight = weight;
}
public Edge(Edge<Weight> e)
{
this.a = e.a;
this.b = e.b;
this.weight = e.weight;
}
public int v(){ return a;} // 返回第一个顶点
public int w(){ return b;} // 返回第二个顶点
public Weight wt(){ return weight;} // 返回权值
// 给定一个顶点, 返回另一个顶点
public int other(int x){
assert x == a || x == b;
return x == a ? b : a;
}
// 输出边的信息
public String toString(){
return "" + a + "-" + b + ": " + weight;
}
// 边之间的比较
public int compareTo(Edge<Weight> that)
{
if( weight.compareTo(that.wt()) < 0 )
return -1;
else if ( weight.compareTo(that.wt()) > 0 )
return +1;
else
return 0;
}
}
// 最小索引堆
public class IndexMinHeap<Item extends Comparable> {
protected Item[] data; // 最小索引堆中的数据
protected int[] indexes; // 最小索引堆中的索引, indexes[x] = i 表示索引i在x的位置
protected int[] reverse; // 最小索引堆中的反向索引, reverse[i] = x 表示索引i在x的位置
protected int count;
protected int capacity;
// 构造函数, 构造一个空堆, 可容纳capacity个元素
public IndexMinHeap(int capacity){
data = (Item[])new Comparable[capacity+1];
indexes = new int[capacity+1];
reverse = new int[capacity+1];
for( int i = 0 ; i <= capacity ; i ++ )
reverse[i] = 0;
count = 0;
this.capacity = capacity;
}
// 返回索引堆中的元素个数
public int size(){
return count;
}
// 返回一个布尔值, 表示索引堆中是否为空
public boolean isEmpty(){
return count == 0;
}
// 向最小索引堆中插入一个新的元素, 新元素的索引为i, 元素为item
// 传入的i对用户而言,是从0索引的
public void insert(int i, Item item){
assert count + 1 <= capacity;
assert i + 1 >= 1 && i + 1 <= capacity;
// 再插入一个新元素前,还需要保证索引i所在的位置是没有元素的。
assert !contain(i);
i += 1;
data[i] = item;
indexes[count+1] = i;
reverse[i] = count + 1;
count ++;
shiftUp(count);
}
// 从最小索引堆中取出堆顶元素, 即索引堆中所存储的最小数据
public Item extractMin(){
assert count > 0;
Item ret = data[indexes[1]];
swapIndexes( 1 , count );
reverse[indexes[count]] = 0;
count --;
shiftDown(1);
return ret;
}
// 从最小索引堆中取出堆顶元素的索引
public int extractMinIndex(){
assert count > 0;
int ret = indexes[1] - 1;
swapIndexes( 1 , count );
reverse[indexes[count]] = 0;
count --;
shiftDown(1);
return ret;
}
// 获取最小索引堆中的堆顶元素
public Item getMin(){
assert count > 0;
return data[indexes[1]];
}
// 获取最小索引堆中的堆顶元素的索引
public int getMinIndex(){
assert count > 0;
return indexes[1]-1;
}
// 看索引i所在的位置是否存在元素
boolean contain( int i ){
assert i + 1 >= 1 && i + 1 <= capacity;
return reverse[i+1] != 0;
}
// 获取最小索引堆中索引为i的元素
public Item getItem( int i ){
assert contain(i);
return data[i+1];
}
// 将最小索引堆中索引为i的元素修改为newItem
public void change( int i , Item newItem ){
assert contain(i);
i += 1;
data[i] = newItem;
// 有了 reverse 之后,
// 我们可以非常简单的通过reverse直接定位索引i在indexes中的位置
shiftUp( reverse[i] );
shiftDown( reverse[i] );
}
// 交换索引堆中的索引i和j
// 由于有了反向索引reverse数组,
// indexes数组发生改变以后, 相应的就需要维护reverse数组
private void swapIndexes(int i, int j){
int t = indexes[i];
indexes[i] = indexes[j];
indexes[j] = t;
reverse[indexes[i]] = i;
reverse[indexes[j]] = j;
}
//********************
//* 最小索引堆核心辅助函数
//********************
// 索引堆中, 数据之间的比较根据data的大小进行比较, 但实际操作的是索引
private void shiftUp(int k){
while( k > 1 && data[indexes[k/2]].compareTo(data[indexes[k]]) > 0 ){
swapIndexes(k, k/2);
k /= 2;
}
}
// 索引堆中, 数据之间的比较根据data的大小进行比较, 但实际操作的是索引
private void shiftDown(int k){
while( 2*k <= count ){
int j = 2*k;
if( j+1 <= count && data[indexes[j+1]].compareTo(data[indexes[j]]) < 0 )
j ++;
if( data[indexes[k]].compareTo(data[indexes[j]]) <= 0 )
break;
swapIndexes(k, j);
k = j;
}
}
// 测试 IndexMinHeap
public static void main(String[] args) {
int N = 1000000;
IndexMinHeap<Integer> indexMinHeap = new IndexMinHeap<Integer>(N);
for( int i = 0 ; i < N ; i ++ )
indexMinHeap.insert( i , (int)(Math.random()*N) );
}
}
public class Main {
// 测试我们的Dijkstra最短路径算法
public static void main(String[] args) {
String filename = "E:\\workspace\\Play-with-Algorithms\\src\\com\\imooc\\ch9_3\\testG1.txt";
int V = 5;
SparseWeightedGraph<Integer> g = new SparseWeightedGraph<Integer>(V, true);
// Dijkstra最短路径算法同样适用于有向图
//SparseGraph<int> g = SparseGraph<int>(V, false);
ReadWeightedGraph readGraph = new ReadWeightedGraph(g, filename);
System.out.println("Test Dijkstra:\n");
Dijkstra<Integer> dij = new Dijkstra<Integer>(g,0);
for( int i = 1 ; i < V ; i ++ ){
if(dij.hasPathTo(i)) {
System.out.println("Shortest Path to " + i + " : " + dij.shortestPathTo(i));
dij.showPath(i);
}
else
System.out.println("No Path to " + i );
System.out.println("----------");
}
}
}
// 通过文件读取有全图的信息
public class ReadWeightedGraph {
private Scanner scanner;
// 由于文件格式的限制,我们的文件读取类只能读取权值为Double类型的图
public ReadWeightedGraph(WeightedGraph<Double> graph, String filename){
readFile(filename);
try {
int V = scanner.nextInt();
if (V < 0)
throw new IllegalArgumentException("number of vertices in a Graph must be nonnegative");
assert V == graph.V();
int E = scanner.nextInt();
if (E < 0)
throw new IllegalArgumentException("number of edges in a Graph must be nonnegative");
for (int i = 0; i < E; i++) {
int v = scanner.nextInt();
int w = scanner.nextInt();
Double weight = scanner.nextDouble();
assert v >= 0 && v < V;
assert w >= 0 && w < V;
graph.addEdge(new Edge<Double>(v, w, weight));
}
}
catch (InputMismatchException e) {
String token = scanner.next();
throw new InputMismatchException("attempts to read an 'int' value from input stream, but the next token is \"" + token + "\"");
}
catch (NoSuchElementException e) {
throw new NoSuchElementException("attemps to read an 'int' value from input stream, but there are no more tokens available");
}
}
private void readFile(String filename){
assert filename != null;
try {
File file = new File(filename);
if (file.exists()) {
FileInputStream fis = new FileInputStream(file);
scanner = new Scanner(new BufferedInputStream(fis), "UTF-8");
scanner.useLocale(Locale.ENGLISH);
}
else
throw new IllegalArgumentException(filename + " doesn't exist.");
}
catch (IOException ioe) {
throw new IllegalArgumentException("Could not open " + filename, ioe);
}
}
}
// 稀疏图 - 邻接表
public class SparseWeightedGraph<Weight extends Number & Comparable>
implements WeightedGraph {
private int n; // 节点数
private int m; // 边数
private boolean directed; // 是否为有向图
private Vector<Edge<Weight>>[] g; // 图的具体数据
// 构造函数
public SparseWeightedGraph( int n , boolean directed ){
assert n >= 0;
this.n = n;
this.m = 0; // 初始化没有任何边
this.directed = directed;
// g初始化为n个空的vector, 表示每一个g[i]都为空, 即没有任和边
g = (Vector<Edge<Weight>>[])new Vector[n];
for(int i = 0 ; i < n ; i ++)
g[i] = new Vector<Edge<Weight>>();
}
public int V(){ return n;} // 返回节点个数
public int E(){ return m;} // 返回边的个数
// 向图中添加一个边, 权值为weight
public void addEdge(Edge e){
assert e.v() >= 0 && e.v() < n ;
assert e.w() >= 0 && e.w() < n ;
// 注意, 由于在邻接表的情况, 查找是否有重边需要遍历整个链表
// 我们的程序允许重边的出现
g[e.v()].add(new Edge(e));
if( e.v() != e.w() && !directed )
g[e.w()].add(new Edge(e.w(), e.v(), e.wt()));
m ++;
}
// 验证图中是否有从v到w的边
public boolean hasEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
for( int i = 0 ; i < g[v].size() ; i ++ )
if( g[v].elementAt(i).other(v) == w )
return true;
return false;
}
// 显示图的信息
public void show(){
for( int i = 0 ; i < n ; i ++ ){
System.out.print("vertex " + i + ":\t");
for( int j = 0 ; j < g[i].size() ; j ++ ){
Edge e = g[i].elementAt(j);
System.out.print( "( to:" + e.other(i) + ",wt:" + e.wt() + ")\t");
}
System.out.println();
}
}
// 返回图中一个顶点的所有邻边
// 由于java使用引用机制,返回一个Vector不会带来额外开销,
public Iterable<Edge<Weight>> adj(int v) {
assert v >= 0 && v < n;
return g[v];
}
}
interface WeightedGraph<Weight extends Number & Comparable> {
public int V();
public int E();
public void addEdge(Edge<Weight> e);
boolean hasEdge( int v , int w );
void show();
public Iterable<Edge<Weight>> adj(int v);
}
9-4 负权边和Bellman-Ford算法
拥有负权环的图
没有最短路径
前提:图中不能有负权环
Bellman-Ford可以判读图中是否有负权环
复杂度O(EV)
如果一个图没有负权环
从一个到另一个点的最短路径,最多经过所有的V个顶线,有V-1条边
否则,存在顶点经过了两次,既存在负权环
对一个点的一次松弛操作,就是找到经过这个点的另外一条路径,多一条边,权值更小。
如果一个图没有负权环,从一个点到另外一点的最短路径,最多经过所有的V个顶线,有V-1条边
对所有的点进行V-1次松弛操作
9-5 实现Bellman-Ford算法
// 使用BellmanFord算法求最短路径
public class BellmanFord<Weight extends Number & Comparable> {
private WeightedGraph G; // 图的引用
private int s; // 起始点
private Number[] distTo; // distTo[i]存储从起始点s到i的最短路径长度
Edge<Weight>[] from; // from[i]记录最短路径中, 到达i点的边是哪一条
// 可以用来恢复整个最短路径
boolean hasNegativeCycle; // 标记图中是否有负权环
// 构造函数, 使用BellmanFord算法求最短路径
public BellmanFord(WeightedGraph graph, int s){
G = graph;
this.s = s;
distTo = new Number[G.V()];
from = new Edge[G.V()];
// 初始化所有的节点s都不可达, 由from数组来表示
for( int i = 0 ; i < G.V() ; i ++ )
from[i] = null;
// 设置distTo[s] = 0, 并且让from[s]不为NULL, 表示初始s节点可达且距离为0
distTo[s] = 0.0;
from[s] = new Edge<Weight>(s, s, (Weight)(Number)(0.0)); // 这里我们from[s]的内容是new出来的, 注意要在析构函数里delete掉
// Bellman-Ford的过程
// 进行V-1次循环, 每一次循环求出从起点到其余所有点, 最多使用pass步可到达的最短距离
for( int pass = 1 ; pass < G.V() ; pass ++ ){
// 每次循环中对所有的边进行一遍松弛操作
// 遍历所有边的方式是先遍历所有的顶点, 然后遍历和所有顶点相邻的所有边
for( int i = 0 ; i < G.V() ; i ++ ){
// 使用我们实现的邻边迭代器遍历和所有顶点相邻的所有边
for( Object item : G.adj(i) ){
Edge<Weight> e = (Edge<Weight>)item;
// 对于每一个边首先判断e->v()可达
// 之后看如果e->w()以前没有到达过, 显然我们可以更新distTo[e->w()]
// 或者e->w()以前虽然到达过, 但是通过这个e我们可以获得一个更短的距离, 即可以进行一次松弛操作, 我们也可以更新distTo[e->w()]
if( from[e.v()] != null && (from[e.w()] == null || distTo[e.v()].doubleValue() + e.wt().doubleValue() < distTo[e.w()].doubleValue()) ){
distTo[e.w()] = distTo[e.v()].doubleValue() + e.wt().doubleValue();
from[e.w()] = e;
}
}
}
}
hasNegativeCycle = detectNegativeCycle();
}
// 判断图中是否有负权环
boolean detectNegativeCycle(){
for( int i = 0 ; i < G.V() ; i ++ ){
for( Object item : G.adj(i) ){
Edge<Weight> e = (Edge<Weight>)item;
if( from[e.v()] != null && distTo[e.v()].doubleValue() + e.wt().doubleValue() < distTo[e.w()].doubleValue() )
return true;
}
}
return false;
}
// 返回图中是否有负权环
boolean negativeCycle(){
return hasNegativeCycle;
}
// 返回从s点到w点的最短路径长度
Number shortestPathTo( int w ){
assert w >= 0 && w < G.V();
assert !hasNegativeCycle;
assert hasPathTo(w);
return distTo[w];
}
// 判断从s点到w点是否联通
boolean hasPathTo( int w ){
assert( w >= 0 && w < G.V() );
return from[w] != null;
}
// 寻找从s到w的最短路径, 将整个路径经过的边存放在vec中
Vector<Edge<Weight>> shortestPath(int w){
assert w >= 0 && w < G.V() ;
assert !hasNegativeCycle ;
assert hasPathTo(w) ;
// 通过from数组逆向查找到从s到w的路径, 存放到栈中
Stack<Edge<Weight>> s = new Stack<Edge<Weight>>();
Edge<Weight> e = from[w];
while( e.v() != this.s ){
s.push(e);
e = from[e.v()];
}
s.push(e);
// 从栈中依次取出元素, 获得顺序的从s到w的路径
Vector<Edge<Weight>> res = new Vector<Edge<Weight>>();
while( !s.empty() ){
e = s.pop();
res.add(e);
}
return res;
}
// 打印出从s点到w点的路径
void showPath(int w){
assert( w >= 0 && w < G.V() );
assert( !hasNegativeCycle );
assert( hasPathTo(w) );
Vector<Edge<Weight>> res = shortestPath(w);
for( int i = 0 ; i < res.size() ; i ++ ){
System.out.print(res.elementAt(i).v() + " -> ");
if( i == res.size()-1 )
System.out.println(res.elementAt(i).w());
}
}
}
// 稠密图 - 邻接矩阵
public class DenseWeightedGraph<Weight extends Number & Comparable>
implements WeightedGraph{
private int n; // 节点数
private int m; // 边数
private boolean directed; // 是否为有向图
private Edge<Weight>[][] g; // 图的具体数据
// 构造函数
public DenseWeightedGraph( int n , boolean directed ){
assert n >= 0;
this.n = n;
this.m = 0; // 初始化没有任何边
this.directed = directed;
// g初始化为n*n的布尔矩阵, 每一个g[i][j]均为null, 表示没有任和边
// false为boolean型变量的默认值
g = new Edge[n][n];
for(int i = 0 ; i < n ; i ++)
for(int j = 0 ; j < n ; j ++)
g[i][j] = null;
}
public int V(){ return n;} // 返回节点个数
public int E(){ return m;} // 返回边的个数
// 向图中添加一个边
public void addEdge(Edge e){
assert e.v() >= 0 && e.v() < n ;
assert e.w() >= 0 && e.w() < n ;
if( hasEdge( e.v() , e.w() ) )
return;
g[e.v()][e.w()] = new Edge(e);
if( e.v() != e.w() && !directed )
g[e.w()][e.v()] = new Edge(e.w(), e.v(), e.wt());
m ++;
}
// 验证图中是否有从v到w的边
public boolean hasEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
return g[v][w] != null;
}
// 显示图的信息
public void show(){
for( int i = 0 ; i < n ; i ++ ){
for( int j = 0 ; j < n ; j ++ )
if( g[i][j] != null )
System.out.print(g[i][j].wt()+"\t");
else
System.out.print("NULL\t");
System.out.println();
}
}
// 返回图中一个顶点的所有邻边
// 由于java使用引用机制,返回一个Vector不会带来额外开销,
public Iterable<Edge<Weight>> adj(int v) {
assert v >= 0 && v < n;
Vector<Edge<Weight>> adjV = new Vector<Edge<Weight>>();
for(int i = 0 ; i < n ; i ++ )
if( g[v][i] != null )
adjV.add( g[v][i] );
return adjV;
}
}
// 边
public class Edge<Weight extends Number & Comparable> implements Comparable<Edge<Weight>>{
private int a, b; // 边的两个端点
private Weight weight; // 边的权值
public Edge(int a, int b, Weight weight)
{
this.a = a;
this.b = b;
this.weight = weight;
}
public Edge(Edge<Weight> e)
{
this.a = e.a;
this.b = e.b;
this.weight = e.weight;
}
public int v(){ return a;} // 返回第一个顶点
public int w(){ return b;} // 返回第二个顶点
public Weight wt(){ return weight;} // 返回权值
// 给定一个顶点, 返回另一个顶点
public int other(int x){
assert x == a || x == b;
return x == a ? b : a;
}
// 输出边的信息
public String toString(){
return "" + a + "-" + b + ": " + weight;
}
// 边之间的比较
public int compareTo(Edge<Weight> that)
{
if( weight.compareTo(that.wt()) < 0 )
return -1;
else if ( weight.compareTo(that.wt()) > 0 )
return +1;
else
return 0;
}
}
public class Main {
// 测试我们的Bellman-Ford最短路径算法
public static void main(String[] args) {
String filename = "E:\\workspace\\Play-with-Algorithms\\src\\com\\imooc\\ch9_5\\testG2.txt";
//String filename = "testG_negative_circle.txt";
int V = 5;
SparseWeightedGraph<Integer> g = new SparseWeightedGraph<Integer>(V, true);
ReadWeightedGraph readGraph = new ReadWeightedGraph(g, filename);
System.out.println("Test Bellman-Ford:\n");
BellmanFord<Integer> bellmanFord = new BellmanFord<Integer>(g,0);
if( bellmanFord.negativeCycle() )
System.out.println("The graph contain negative cycle!");
else
for( int i = 1 ; i < V ; i ++ ){
if(bellmanFord.hasPathTo(i)) {
System.out.println("Shortest Path to " + i + " : " + bellmanFord.shortestPathTo(i));
bellmanFord.showPath(i);
}
else
System.out.println("No Path to " + i );
System.out.println("----------");
}
}
}
// 通过文件读取有全图的信息
public class ReadWeightedGraph {
private Scanner scanner;
// 由于文件格式的限制,我们的文件读取类只能读取权值为Double类型的图
public ReadWeightedGraph(WeightedGraph<Double> graph, String filename){
readFile(filename);
try {
int V = scanner.nextInt();
if (V < 0)
throw new IllegalArgumentException("number of vertices in a Graph must be nonnegative");
assert V == graph.V();
int E = scanner.nextInt();
if (E < 0)
throw new IllegalArgumentException("number of edges in a Graph must be nonnegative");
for (int i = 0; i < E; i++) {
int v = scanner.nextInt();
int w = scanner.nextInt();
Double weight = scanner.nextDouble();
assert v >= 0 && v < V;
assert w >= 0 && w < V;
graph.addEdge(new Edge<Double>(v, w, weight));
}
}
catch (InputMismatchException e) {
String token = scanner.next();
throw new InputMismatchException("attempts to read an 'int' value from input stream, but the next token is \"" + token + "\"");
}
catch (NoSuchElementException e) {
throw new NoSuchElementException("attemps to read an 'int' value from input stream, but there are no more tokens available");
}
}
private void readFile(String filename){
assert filename != null;
try {
File file = new File(filename);
if (file.exists()) {
FileInputStream fis = new FileInputStream(file);
scanner = new Scanner(new BufferedInputStream(fis), "UTF-8");
scanner.useLocale(Locale.ENGLISH);
}
else
throw new IllegalArgumentException(filename + " doesn't exist.");
}
catch (IOException ioe) {
throw new IllegalArgumentException("Could not open " + filename, ioe);
}
}
}
// 稀疏图 - 邻接表
public class SparseWeightedGraph<Weight extends Number & Comparable>
implements WeightedGraph {
private int n; // 节点数
private int m; // 边数
private boolean directed; // 是否为有向图
private Vector<Edge<Weight>>[] g; // 图的具体数据
// 构造函数
public SparseWeightedGraph( int n , boolean directed ){
assert n >= 0;
this.n = n;
this.m = 0; // 初始化没有任何边
this.directed = directed;
// g初始化为n个空的vector, 表示每一个g[i]都为空, 即没有任和边
g = (Vector<Edge<Weight>>[])new Vector[n];
for(int i = 0 ; i < n ; i ++)
g[i] = new Vector<Edge<Weight>>();
}
public int V(){ return n;} // 返回节点个数
public int E(){ return m;} // 返回边的个数
// 向图中添加一个边, 权值为weight
public void addEdge(Edge e){
assert e.v() >= 0 && e.v() < n ;
assert e.w() >= 0 && e.w() < n ;
// 注意, 由于在邻接表的情况, 查找是否有重边需要遍历整个链表
// 我们的程序允许重边的出现
g[e.v()].add(new Edge(e));
if( e.v() != e.w() && !directed )
g[e.w()].add(new Edge(e.w(), e.v(), e.wt()));
m ++;
}
// 验证图中是否有从v到w的边
public boolean hasEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
for( int i = 0 ; i < g[v].size() ; i ++ )
if( g[v].elementAt(i).other(v) == w )
return true;
return false;
}
// 显示图的信息
public void show(){
for( int i = 0 ; i < n ; i ++ ){
System.out.print("vertex " + i + ":\t");
for( int j = 0 ; j < g[i].size() ; j ++ ){
Edge e = g[i].elementAt(j);
System.out.print( "( to:" + e.other(i) + ",wt:" + e.wt() + ")\t");
}
System.out.println();
}
}
// 返回图中一个顶点的所有邻边
// 由于java使用引用机制,返回一个Vector不会带来额外开销,
public Iterable<Edge<Weight>> adj(int v) {
assert v >= 0 && v < n;
return g[v];
}
}
interface WeightedGraph<Weight extends Number & Comparable> {
public int V();
public int E();
public void addEdge(Edge<Weight> e);
boolean hasEdge( int v , int w );
void show();
public Iterable<Edge<Weight>> adj(int v);
}
9-6 更多和最短路径相关的思考
利用队列数据结构
queue-based bellman-ford算法
dijkstra 无负权边 有向无向图均可 O(ElogV)
Bellman-Ford 无负权环 有向图 O(VE)
利用拓扑排序 有向无环图DAG 有向图 O(V+E)
Floyed算法,处理无负权环的图
O(V^3)
最长路径问题不能有正权环。
无权环的最长路径问题是指数级难度的。
对于有权图,不能使用Dijkstra求最长路径问题。
可以使用Bellman-Ford算法。
第10章 结束语
10-1 总结,算法思想,大家加油
线性 -> 树形结构 -> 图形结构
O(n^2) 选择排序 插入排序
O(nlogn) 归并排序 快速排序
partition -> 随机化 -> 大量重复元素
求逆数的个数 k-selection
堆(Heap) 堆排序 优先队列 索引堆
二叉查找树(Binary Search Tree) 解决查找问题
并查集(Union Find) 基于rank优化 -> 路径压缩 Kruskal
图论问题
图的表示:邻接表和邻接矩阵
有向图和无向图
有权图和无权图
图的遍历:DFS,BFS
联通分量 Flood Fill 寻路 走迷宫 迷宫生成
无权图的最短路径 环的判断
最小生成树问题(Minimum Spanning Tree) Prim Kruskal
最短路径问题(Shortest Path) Dijkstra Belman-Ford
更多算法问题
数据结构相关
双向队列 斐波那契堆 红黑树 区间树 KD树…
具体领域相关
数学:数论;计算几何 图论:网络流…
算法设计相关
分治 归并排序;快速排序;树结构
贪心 选择排序;堆;Kruskal;Prim;Dijkstra
递归回溯 树的遍历;图的遍历
动态规划 Prim;Dijkstra