第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