《演算法筆記》10. 並查集、圖相關演算法、看完這篇不能再說不會了。

Inky發表於2020-08-06

1 並查集、圖相關演算法

轉載註明出處,原始碼地址: https://github.com/Dairongpeng/algorithm-note ,歡迎star

1.1 並查集

1.1.1 並查集基本結構和操作

1、有若干個樣本a、b、c、d...型別假設是V

2、在並查集中一開始認為每個樣本都在單獨的集合裡

3、使用者可以在任何時候呼叫如下兩個方法:

boolean isSameSet(V x, V y):查詢樣本x和樣本y是否屬於一個集合

void union(V x, V y):把x和y各自所在集合的所有樣本合併成一個集合

4、isSameSet和union方法的代價越低越好,最好O(1)

思路:isSameSet方法,我們設計為每個元素有一個指向自己的指標,成為代表點。判斷兩個元素是否在一個集合中,分別呼叫這兩個元素的向上指標,兩個元素最上方的指標如果記憶體地址相同,那麼兩個元素在一個集合中,反之不在

思路:union方法,例如將a所在的集合和e所在的集合合併成一個大的集合union(a,e)。a的代表點指標是a,e的代表點指標是e,我們拿較小的集合掛在大的集合下面,比如e小,那麼e放在a的下面。連結的方式為小集合e頭結點本來指向自己的代表節點,現在要指向a節點

並查集的優化點主要有兩個,一個是合併的時候小的集合掛在大的集合下面,第二個優化是找某節點最上方的代表節點,把沿途節點全部拍平,下次再找該沿途節點,都變為O(1)。兩種優化的目的都是為了更少的遍歷節點。

由於我們加入了優化,如果N個節點,我們呼叫findFather越頻繁,我們的時間複雜度越低,因為第一次呼叫我們加入了優化。如果findFather呼叫接近N次或者遠遠超過N次,我們並查集的時間複雜度就是O(1)。該複雜度只需要記住結論,證明無須掌握。該證明從1964年一直研究到1989年,整整25年才得出證明!演算法導論23章,英文版接近50頁的證明。

package class10;

import java.util.HashMap;
import java.util.List;
import java.util.Stack;

public class Code01_UnionFind {

        // 並查集結構中的節點型別
	public static class Node<V> {
		V value;

		public Node(V v) {
			value = v;
		}
	}

	public static class UnionSet<V> {
	        // 記錄樣本到樣本代表點的關係
		public HashMap<V, Node<V>> nodes;
		// 記錄某節點到父親節點的關係。
		// 比如b指向a,c指向a,d指向a,a指向自身
		// map中儲存的a->a b->a c->a d->a
		public HashMap<Node<V>, Node<V>> parents;
		// 只有當前點,他是代表點,會在sizeMap中記錄該代表點的連通個數
		public HashMap<Node<V>, Integer> sizeMap;

                // 初始化構造一批樣本
		public UnionSet(List<V> values) {
		        // 每個樣本的V指向自身的代表節點
		        // 每個樣本當前都是獨立的,parent是自身
		        // 每個樣本都是代表節點放入sizeMap
			for (V cur : values) {
				Node<V> node = new Node<>(cur);
				nodes.put(cur, node);
				parents.put(node, node);
				sizeMap.put(node, 1);
			}
		}

		// 從點cur開始,一直往上找,找到不能再往上的代表點,返回
		// 通過把路徑上所有節點指向最上方的代表節點,目的是把findFather優化成O(1)的
		public Node<V> findFather(Node<V> cur) {
		        // 在找father的過程中,沿途所有節點加入當前容器,便於後面扁平化處理
			Stack<Node<V>> path = new Stack<>();
			// 當前節點的父親不是指向自己,進行迴圈
			while (cur != parents.get(cur)) {
				path.push(cur);
				cur = parents.get(cur);
			}
			// 迴圈結束,cur是最上的代表節點
			// 把沿途所有節點拍平,都指向當前最上方的代表節點
			while (!path.isEmpty()) {
				parents.put(path.pop(), cur);
			}
			return cur;
		}

                // isSameSet方法
		public boolean isSameSet(V a, V b) {
		        // 先檢查a和b有沒有登記
			if (!nodes.containsKey(a) || !nodes.containsKey(b)) {
				return false;
			}
			// 比較a的最上的代表點和b最上的代表點
			return findFather(nodes.get(a)) == findFather(nodes.get(b));
		}

                // union方法
		public void union(V a, V b) {
		        // 先檢查a和b有沒有都登記過
			if (!nodes.containsKey(a) || !nodes.containsKey(b)) {
				return;
			}
			
			// 找到a的最上面的代表點
			Node<V> aHead = findFather(nodes.get(a));
			// 找到b的最上面的代表點
			Node<V> bHead = findFather(nodes.get(b));
			
			// 只有兩個最上代表點記憶體地址不相同,需要union
			if (aHead != bHead) {
			
			        // 由於aHead和bHead都是代表點,那麼在sizeMap裡可以拿到大小
				int aSetSize = sizeMap.get(aHead);
				int bSetSize = sizeMap.get(bHead);
				
				// 哪個小,哪個掛在下面
				Node<V> big = aSetSize >= bSetSize ? aHead : bHead;
				Node<V> small = big == aHead ? bHead : aHead;
				// 把小集合直接掛到大集合的最上面的代表節點下面
				parents.put(small, big);
				// 大集合的代表節點的size要吸收掉小集合的size
				sizeMap.put(big, aSetSize + bSetSize);
				// 把小的記錄刪除
				sizeMap.remove(small);
			}
		}
	}

}

並查集用來處理連通性的問題特別方便

1.1.2 例題

學生例項有三個屬性,身份證資訊,B站ID,Github的Id。我們認為,任何兩個學生例項,只要身份證一樣,或者B站ID一樣,或者Github的Id一樣,我們都算一個人。給定一大批學生例項,輸出實質有幾個人?

思路:把例項的三個屬性建立三張對映表,每個例項去對比,某個例項屬性在表中能查的到,需要聯通該例項到之前儲存該例項屬性的頭結點下

package class10;

import java.util.HashMap;
import java.util.List;
import java.util.Stack;

public class Code07_MergeUsers {

	public static class Node<V> {
		V value;

		public Node(V v) {
			value = v;
		}
	}

	public static class UnionSet<V> {
		public HashMap<V, Node<V>> nodes;
		public HashMap<Node<V>, Node<V>> parents;
		public HashMap<Node<V>, Integer> sizeMap;

		public UnionSet(List<V> values) {
			for (V cur : values) {
				Node<V> node = new Node<>(cur);
				nodes.put(cur, node);
				parents.put(node, node);
				sizeMap.put(node, 1);
			}
		}

		// 從點cur開始,一直往上找,找到不能再往上的代表點,返回
		public Node<V> findFather(Node<V> cur) {
			Stack<Node<V>> path = new Stack<>();
			while (cur != parents.get(cur)) {
				path.push(cur);
				cur = parents.get(cur);
			}
			// cur頭節點
			while (!path.isEmpty()) {
				parents.put(path.pop(), cur);
			}
			return cur;
		}

		public boolean isSameSet(V a, V b) {
			if (!nodes.containsKey(a) || !nodes.containsKey(b)) {
				return false;
			}
			return findFather(nodes.get(a)) == findFather(nodes.get(b));
		}

		public void union(V a, V b) {
			if (!nodes.containsKey(a) || !nodes.containsKey(b)) {
				return;
			}
			Node<V> aHead = findFather(nodes.get(a));
			Node<V> bHead = findFather(nodes.get(b));
			if (aHead != bHead) {
				int aSetSize = sizeMap.get(aHead);
				int bSetSize = sizeMap.get(bHead);
				Node<V> big = aSetSize >= bSetSize ? aHead : bHead;
				Node<V> small = big == aHead ? bHead : aHead;
				parents.put(small, big);
				sizeMap.put(big, aSetSize + bSetSize);
				sizeMap.remove(small);
			}
		}
		
		
		public int getSetNum() {
			return sizeMap.size();
		}
		
	}

	public static class User {
		public String a;
		public String b;
		public String c;

		public User(String a, String b, String c) {
			this.a = a;
			this.b = b;
			this.c = c;
		}

	}

	// (1,10,13) (2,10,37) (400,500,37)
	// 如果兩個user,a欄位一樣、或者b欄位一樣、或者c欄位一樣,就認為是一個人
	// 請合併users,返回合併之後的使用者數量
	public static int mergeUsers(List<User> users) {
		UnionSet<User> unionFind = new UnionSet<>(users);
		HashMap<String, User> mapA = new HashMap<>();
		HashMap<String, User> mapB = new HashMap<>();
		HashMap<String, User> mapC = new HashMap<>();
		for(User user : users) {
			if(mapA.containsKey(user.a)) {
				unionFind.union(user, mapA.get(user.a));
			}else {
				mapA.put(user.a, user);
			}
			if(mapB.containsKey(user.b)) {
				unionFind.union(user, mapB.get(user.b));
			}else {
				mapB.put(user.b, user);
			}
			if(mapC.containsKey(user.c)) {
				unionFind.union(user, mapC.get(user.c));
			}else {
				mapC.put(user.c, user);
			}
		}
		// 向並查集詢問,合併之後,還有多少個集合?
		return unionFind.getSetNum();
	}

}

1.2 圖相關演算法

1.2.1 圖的概念

1、由點的集合和邊的集合構成

2、雖然存在有向圖和無向圖的概念,但實際上都可以用有向圖來表達,無向圖可以理解為兩個聯通點互相指向

3、邊上可能帶有權值

1.2.2 圖的表示方法

對於下面一張無向圖,可以改為有向圖:

graph LR;
A-->C
C-->A
C-->B
B-->C
B-->D
D-->B
D-->A
A-->D

1.2.2.1 鄰接表表示法

記錄某個節點,直接到達的鄰居節點:

A: C,D

B: C,D

C: A,B

D: B,A

如果是帶有權重的邊,可以封裝我們的結構,例如A到C的權重是3,那麼我們可以表示為A: C(3),D

1.2.2.2 鄰接矩陣表示法

我們把不存在路徑的用正無窮表示,這裡用'-'表示,例如A到C的邊權重是3,可把上圖表示為:

  A  B  C  D
A 0  0  3  -
B -  0  0  0
C 3  0  0  -
D 0  0  -  0

圖演算法並不難,難點在於圖有很多種表示方式,表達一張圖的篇幅比較大,coding容易出錯。我們的套路就是熟悉一種結構,遇到不同的表達方式,嘗試轉化成為我們熟悉的結構,進行操作

點結構的描述:

package class10;

import java.util.ArrayList;

// 點結構的描述  A  0
public class Node {
        // 點的編號,標識
	public int value;
	// 入度,表示有多少個點連向該點
	public int in;
	// 出度,表示從該點出發連向別的節點多少
	public int out;
	// 直接鄰居:表示由自己出發,直接指向哪些節點。nexts.size==out
	public ArrayList<Node> nexts;
	// 直接下級邊:表示由自己出發的邊有多少
	public ArrayList<Edge> edges;

	public Node(int value) {
		this.value = value;
		in = 0;
		out = 0;
		nexts = new ArrayList<>();
		edges = new ArrayList<>();
	}
}

邊結構的描述:

package class10;

// 由於任何圖都可以理解為有向圖,我們定義有向的邊結構
public class Edge {
        // 邊的權重資訊
	public int weight;
	// 出發的節點
	public Node from;
	// 指向的節點
	public Node to;

	public Edge(int weight, Node from, Node to) {
		this.weight = weight;
		this.from = from;
		this.to = to;
	}

}

圖結構的描述:

package class10;

import java.util.HashMap;
import java.util.HashSet;

// 圖結構
public class Graph {
        // 點的集合,編號為1的點是什麼,用map
	public HashMap<Integer, Node> nodes;
	// 邊的集合
	public HashSet<Edge> edges;
	
	public Graph() {
		nodes = new HashMap<>();
		edges = new HashSet<>();
	}
}

任意圖結構的描述,向我們上述的圖結構轉化:

例如,我們有一種圖的描述是,變的權重,從from節點指向to節點

package class10;

public class GraphGenerator {

	// matrix 所有的邊
	// N*3 的矩陣
	// [weight, from節點上面的值,to節點上面的值]
	public static Graph createGraph(Integer[][] matrix) {
	        // 定義我們的圖結構
		Graph graph = new Graph();
		// 遍歷給定的圖結構進行轉換
		for (int i = 0; i < matrix.length; i++) { 
			// matrix[0][0], matrix[0][1]  matrix[0][2]
			Integer weight = matrix[i][0];
			Integer from = matrix[i][1];
			Integer to = matrix[i][2];
			
			// 我們的圖結構不包含當前from節點,新建該節點
			if (!graph.nodes.containsKey(from)) {
				graph.nodes.put(from, new Node(from));
			}
			// 沒有to節點,建立該節點
			if (!graph.nodes.containsKey(to)) {
				graph.nodes.put(to, new Node(to));
			}
			// 拿出我們圖結構的from節點
			Node fromNode = graph.nodes.get(from);
			// 拿出我們圖結構的to節點
			Node toNode = graph.nodes.get(to);
			// 建立我們的邊結構。權重,from指向to
			Edge newEdge = new Edge(weight, fromNode, toNode);
			// 把to節點加入到from節點的直接鄰居中
			fromNode.nexts.add(toNode);
			// from的出度加1
			fromNode.out++;
			// to的入度加1
			toNode.in++;
			// 該邊需要放到from的直接邊的集合中
			fromNode.edges.add(newEdge);
			// 把該邊加入到我們圖結構的邊集中
			graph.edges.add(newEdge);
		}
		return graph;
	}

}

1.2.3 圖的遍歷

例如該圖:

graph LR;
A-->B
A-->C
A-->D
B-->C
B-->E
C-->A
C-->B
C-->D
C-->E

1.2.3.1 寬度優先遍歷

1、利用佇列實現

2、從源節點開始依次按照寬度進佇列,然後彈出

3、每彈出一個點,把該節點所有沒有進過佇列的鄰接點放入佇列

4、直到佇列變空

寬度優先的思路:實質先遍歷自己,再遍歷自己的下一跳節點(同一層節點的順序無需關心),再到下跳節點......

我們從A點開始遍歷:

1、A進佇列--> Q[A];A進入Set--> S[A]

2、A出隊:Q[],列印A;A直接鄰居為BCD,都不在Set中,進入佇列Q[D,C,B], 進入S[A,B,C,D]

3、B出隊:Q[D,C], B有CE三個鄰居,C已經在Set中, 放入E, S[A,B,C,D,E],佇列放E, Q[E,D,C]

4、 C出隊,周而復始

package class10;

import java.util.HashSet;
import java.util.LinkedList;
import java.util.Queue;

public class Code02_BFS {

	// 從node出發,進行寬度優先遍歷
	public static void bfs(Node node) {
		if (node == null) {
			return;
		}
		Queue<Node> queue = new LinkedList<>();
		// 圖需要用set結構,因為圖相比於二叉樹有可能存在環
		// 即有可能存在某個點多次進入佇列的情況
		HashSet<Node> set = new HashSet<>();
		queue.add(node);
		set.add(node);
		while (!queue.isEmpty()) {
			Node cur = queue.poll();
			System.out.println(cur.value);
			for (Node next : cur.nexts) {
			    // 直接鄰居,沒有進入過Set的進入Set和佇列
			    // 用set限制佇列的元素,防止有環佇列一直會加入元素
				if (!set.contains(next)) {
					set.add(next);
					queue.add(next);
				}
			}
		}
	}

}

1.2.3.2 深度優先遍歷

1、利用棧實現

2、從源節點開始把節點按照深度放入棧,然後彈出

3、每彈出一個點,把該節點下一個沒有進過棧的鄰接點放入棧

4、直到棧變空

深度優先思路:表示從某個節點一直往下深入,直到沒有路了,返回。我們的棧實質記錄的是我們深度優先遍歷的路徑

我們從A點開始遍歷:

1、A進棧,Stack[A] 列印A。彈出A,當前彈出的節點A去列舉它的後代BCD,B沒加入過棧中。壓入A再壓入B,Stack[B,A]。列印B

2、彈出B,B的直接後代鄰居為CE,C在棧中而E不在棧中。重新壓B,壓E,Stack[E,B,A]。列印E

3、彈出E,E有鄰居D,D不在棧中。壓回E,再壓D,此時Stack[D,E,B,A]。列印D

4、 彈出D,D的直接鄰居是A,A已經在棧中了。說明A-B-E-D這條路徑走到了盡頭。彈出D之後,當前迴圈結束。繼續while棧不為空,重複操作

package class10;

import java.util.HashSet;
import java.util.Stack;

public class Code02_DFS {

	public static void dfs(Node node) {
		if (node == null) {
			return;
		}
		Stack<Node> stack = new Stack<>();
		// Set的作用和寬度優先遍歷類似,保證重複的點不要進棧
		HashSet<Node> set = new HashSet<>();
		stack.add(node);
		set.add(node);
		// 列印實時機是在進棧的時候
		// 同理該步可以換成其他處理邏輯,表示深度遍歷處理某件事情
		System.out.println(node.value);
		while (!stack.isEmpty()) {
			Node cur = stack.pop();
			// 列舉當前彈出節點的後代
			for (Node next : cur.nexts) {
		                // 只要某個後代沒進入過棧,進棧
				if (!set.contains(next)) {
				        // 把該節點的父親節點重新壓回棧中
					stack.push(cur);
					// 再把自己壓入棧中
					stack.push(next);
					set.add(next);
				    // 列印當前節點的值
				    System.out.println(next.value);
				        // 直接break,此時棧頂是當前next節點,達到深度優先的目的
					break;
				}
			}
		}
	}

}

1.2.4 圖的拓撲排序

1、在圖中找到所有入度為0的點輸出

2、把所有入度為0的點在圖中刪掉,且消除這些點的影響邊。繼續找入度為0的點輸出,刪除,消邊,周而復始

3、圖的所有點都被刪除後,依次輸出的順序就是圖的拓撲排序

要求:有向圖且其中沒有環

應用:事件安排,編譯順序

在我們的專案中,專案之間互相依賴,就是拓撲排序的一個應用,從最底層依賴的包往上層編譯,最終把總的專案編譯通過。所以專案中迴圈依賴是編譯不通過的

例如下列的有向無環圖:

graph LR;
A-->B
B-->C
A-->C
C-->E
E-->F
C-->T
F-->T

圖中的字母代表事情,做事情的先後順序就是按照有向圖的描述,請安排事情的先後順序(拓撲排序)。

拓撲排序為:A B C E F T

package class10;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

public class Code03_TopologySort {

	// 有向無環圖,返回拓撲排序的順序list
	public static List<Node> sortedTopology(Graph graph) {
		// key:某一個node
		// value:該節點剩餘的入度
		HashMap<Node, Integer> inMap = new HashMap<>();
		// 剩餘入度為0的點,才能進這個佇列
		Queue<Node> zeroInQueue = new LinkedList<>();
		
		// 拿到該圖中所有的點集
		for (Node node : graph.nodes.values()) {
		        // 初始化每個點,每個點的入度是原始節點的入度資訊
		        // 加入inMap
			inMap.put(node, node.in);
			// 由於是有向無環圖,則必定有入度為0的起始點。放入到zeroInQueue
			if (node.in == 0) {
				zeroInQueue.add(node);
			}
		}
		
		// 拓撲排序的結果,依次加入result
		List<Node> result = new ArrayList<>();
		
		while (!zeroInQueue.isEmpty()) {
		        // 該有向無環圖初始入度為0的點,直接彈出放入結果集中
			Node cur = zeroInQueue.poll();
			result.add(cur);
			// 該節點的下一層鄰居節點,入度減一且加入到入度的map中
			for (Node next : cur.nexts) {
				inMap.put(next, inMap.get(next) - 1);
				// 如果下一層存在入度變為0的節點,加入到0入度的佇列中
				if (inMap.get(next) == 0) {
					zeroInQueue.add(next);
				}
			}
		}
		return result;
	}
}

1.2.5 圖的最小生成樹演算法

最小生成樹解釋,就是在不破壞原有圖點與點的連通性基礎上,讓連通的邊的整體權值最小。返回最小權值或者邊的集合

1.2.5.1 Kruskal(克魯斯卡爾)演算法

連通性藉助並查集實現

1、總是從權值最小的邊開始考慮,依次考察權值依次變大的邊

2、當前的邊要麼進入最小生成樹的集合,要麼丟棄

3、如果當前的邊進入最小生成樹的集合中不會形成環,就要當前邊

4、如果當前的邊進入最小生成樹的集合中會形成環,就不要當前邊

5、考察完所有邊之後,最小生成樹的集合也就得到了

package class10;

import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.PriorityQueue;
import java.util.Set;
import java.util.Stack;

//undirected graph only
public class Code04_Kruskal {

	// Union-Find Set 我們的並查集結構
	public static class UnionFind {
		// key 某一個節點, value key節點往上的節點
		private HashMap<Node, Node> fatherMap;
		// key 某一個集合的代表節點, value key所在集合的節點個數
		private HashMap<Node, Integer> sizeMap;

		public UnionFind() {
			fatherMap = new HashMap<Node, Node>();
			sizeMap = new HashMap<Node, Integer>();
		}
		
		public void makeSets(Collection<Node> nodes) {
			fatherMap.clear();
			sizeMap.clear();
			for (Node node : nodes) {
				fatherMap.put(node, node);
				sizeMap.put(node, 1);
			}
		}

		private Node findFather(Node n) {
			Stack<Node> path = new Stack<>();
			while(n != fatherMap.get(n)) {
				path.add(n);
				n = fatherMap.get(n);
			}
			while(!path.isEmpty()) {
				fatherMap.put(path.pop(), n);
			}
			return n;
		}

		public boolean isSameSet(Node a, Node b) {
			return findFather(a) == findFather(b);
		}

		public void union(Node a, Node b) {
			if (a == null || b == null) {
				return;
			}
			Node aDai = findFather(a);
			Node bDai = findFather(b);
			if (aDai != bDai) {
				int aSetSize = sizeMap.get(aDai);
				int bSetSize = sizeMap.get(bDai);
				if (aSetSize <= bSetSize) {
					fatherMap.put(aDai, bDai);
					sizeMap.put(bDai, aSetSize + bSetSize);
					sizeMap.remove(aDai);
				} else {
					fatherMap.put(bDai, aDai);
					sizeMap.put(aDai, aSetSize + bSetSize);
					sizeMap.remove(bDai);
				}
			}
		}
	}
	

	public static class EdgeComparator implements Comparator<Edge> {

		@Override
		public int compare(Edge o1, Edge o2) {
			return o1.weight - o2.weight;
		}

	}

        // K演算法
	public static Set<Edge> kruskalMST(Graph graph) {
	        // 先拿到並查集結構
		UnionFind unionFind = new UnionFind();
		// 該圖的所有點加入到並查集結構
		unionFind.makeSets(graph.nodes.values());
		// 邊按照權值從小到大排序,加入到堆
		PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(new EdgeComparator());
		
		for (Edge edge : graph.edges) { // M 條邊
			priorityQueue.add(edge);  // O(logM)
		}
		
		Set<Edge> result = new HashSet<>();
		// 堆不為空,彈出小根堆的堆頂
		while (!priorityQueue.isEmpty()) { 
	    	        // 假設M條邊,O(logM)
			Edge edge = priorityQueue.poll(); 
			
			// 如果該邊的左右兩側不在同一個集合中
			if (!unionFind.isSameSet(edge.from, edge.to)) { // O(1)
			        // 要這條邊
				result.add(edge);
				// 聯合from和to
				unionFind.union(edge.from, edge.to);
			}
		}
		return result;
	}
}

K演算法求無向圖的最小生成樹,求權值是沒問題的,如果糾結最小生成樹的連通結構,實質是少了一側,即A指向B, B指向A只會保留其一。可以手動補齊

1.2.5.2 Prim演算法

P演算法無需並查集結構,普通set即可滿足

1、任意指定一個出發點,比如A, A的直接邊被解鎖

2、在A解鎖的邊裡選擇一個最小的邊,該邊兩側有沒有新節點,如果有選擇該邊。沒有就捨棄該邊

3、在被選擇的新節點中再解鎖該節點的直接邊

4、周而復始,直到所有點被解鎖

package class10;

import java.util.Comparator;
import java.util.HashSet;
import java.util.PriorityQueue;
import java.util.Set;

// undirected graph only
public class Code05_Prim {

	public static class EdgeComparator implements Comparator<Edge> {

		@Override
		public int compare(Edge o1, Edge o2) {
			return o1.weight - o2.weight;
		}

	}

	public static Set<Edge> primMST(Graph graph) {
		// 解鎖的邊進入小根堆
		PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(new EdgeComparator());

		// 哪些點被解鎖出來了
		HashSet<Node> nodeSet = new HashSet<>();
		// 已經考慮過的邊,不要重複考慮
		Set<Edge> result = new HashSet<>();
		// 依次挑選的的邊在result裡
		Set<Edge> result = new HashSet<>(); 
		// 隨便挑了一個點,進入迴圈處理完後直接break
		for (Node node : graph.nodes.values()) { 
			// node 是開始點
			if (!nodeSet.contains(node)) {
			    // 開始節點保留
				nodeSet.add(node);
				// 開始節點的所有鄰居節點全部放到小根堆
				// 即由一個點,解鎖所有相連的邊
				for (Edge edge : node.edges) {
				    if (!edgeSet.contains(edge)) {
				        edgeSet.add(edge);
				        priorityQueue.add(edge);
				    }
				}
				
				while (!priorityQueue.isEmpty()) {
				        // 彈出解鎖的邊中,最小的邊
					Edge edge = priorityQueue.poll(); 
					 // 可能的一個新的點,from已經被考慮了,只需要看to
					Node toNode = edge.to;
					// 不含有的時候,就是新的點
					if (!nodeSet.contains(toNode)) { 
						nodeSet.add(toNode);
						result.add(edge);
						for (Edge nextEdge : toNode.edges) {
						// 沒加過的,放入小根堆
					        if (!edgeSet.contains(edge)) {
				                edgeSet.add(edge);
				                priorityQueue.add(edge);
				            }
						}
					}
				}
			}
			// 直接break意味著我們不用考慮森林的情況
			// 如果不加break我們可以相容多個無向圖的森林的生成樹
			// break;
		}
		return result;
	}

	// 請保證graph是連通圖
	// graph[i][j]表示點i到點j的距離,如果是系統最大值代表無路
	// 返回值是最小連通圖的路徑之和
	public static int prim(int[][] graph) {
		int size = graph.length;
		int[] distances = new int[size];
		boolean[] visit = new boolean[size];
		visit[0] = true;
		for (int i = 0; i < size; i++) {
			distances[i] = graph[0][i];
		}
		int sum = 0;
		for (int i = 1; i < size; i++) {
			int minPath = Integer.MAX_VALUE;
			int minIndex = -1;
			for (int j = 0; j < size; j++) {
				if (!visit[j] && distances[j] < minPath) {
					minPath = distances[j];
					minIndex = j;
				}
			}
			if (minIndex == -1) {
				return sum;
			}
			visit[minIndex] = true;
			sum += minPath;
			for (int j = 0; j < size; j++) {
				if (!visit[j] && distances[j] > graph[minIndex][j]) {
					distances[j] = graph[minIndex][j];
				}
			}
		}
		return sum;
	}

	public static void main(String[] args) {
		System.out.println("hello world!");
	}

}

1.2.6 圖的最短路徑演算法

1.2.6.1 Dijkstra(迪杰特斯拉)演算法

Dijkstra演算法必須要求邊的權值不為負,且必須指定出發點。則可以求出發點到所有節點的最短距離是多少。如果到達不了,為正無窮

1、Dijkstra演算法必須指定一個源點

2、生成一個源點到各個點的最小距離表,一開始只有一條記錄,即原點到自己的最小距離為0,源點到其他所有點的最小距離都為正無窮大

3、從距離表中拿出沒拿過記錄裡的最小記錄,通過這個點出發的邊,更新源點到各個點的最小距離表,不斷重複這一步

4、源點到所有的點記錄如果都被拿過一遍,過程停止,最小距離表得到了

package class10;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map.Entry;

// 沒改進之前的版本
public class Code06_Dijkstra {

        // 返回的map表就是從from到表中key的各個的最小距離
        // 某個點不在map中記錄,則from到該點位正無窮
	public static HashMap<Node, Integer> dijkstra1(Node from) {
		// 從from出發到所有點的最小距離表
		HashMap<Node, Integer> distanceMap = new HashMap<>();
		// from到from距離為0
		distanceMap.put(from, 0);
		// 已經求過距離的節點,存在selectedNodes中,以後再也不碰
		HashSet<Node> selectedNodes = new HashSet<>();
		// from 0 得到沒選擇過的點的最小距離
		Node minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);
		
		// 得到minNode之後
		while (minNode != null) {
		        // 把minNode對應的距離取出,此時minNode就是橋連點
			int distance = distanceMap.get(minNode);
			
			// 把minNode上所有的鄰邊拿出來
			// 這裡就是要拿到例如A到C和A到橋連點B再到C哪個距離小的距離
			for (Edge edge : minNode.edges) {
			        // 某條邊對應的下一跳節點toNode
				Node toNode = edge.to;
				
				// 如果關於from的distencMap中沒有去toNode的記錄,表示正無窮,直接新增該條
				if (!distanceMap.containsKey(toNode)) {
				        // from到minNode的距離加上個minNode到當前to節點的邊距離
					distanceMap.put(toNode, distance + edge.weight);
					
				// 如果有,看該距離是否更小,更小就更新
				} else {
					distanceMap.put(edge.to, 
							Math.min(distanceMap.get(toNode), distance + edge.weight));
				}
			}
			// 鎖上minNode,表示from通過minNode到其他節點的最小值已經找到
			// minNode將不再使用
			selectedNodes.add(minNode);
			// 再在沒有選擇的節點中挑選MinNode當成from的橋接點
			minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);
		}
		// 最終distanceMap全部更新,返回
		return distanceMap;
	}

        // 得到沒選擇過的點的最小距離
	public static Node getMinDistanceAndUnselectedNode(
			HashMap<Node, Integer> distanceMap, 
			HashSet<Node> touchedNodes) {
		Node minNode = null;
		int minDistance = Integer.MAX_VALUE;
		for (Entry<Node, Integer> entry : distanceMap.entrySet()) {
			Node node = entry.getKey();
			int distance = entry.getValue();
			// 沒有被選擇過,且距離最小
			if (!touchedNodes.contains(node) && distance < minDistance) {
				minNode = node;
				minDistance = distance;
			}
		}
		return minNode;
	}
	
	/**
	* 我們可以藉助小根堆來替代之前的distanceMap。達到優化演算法的目的
	* 原因是之前我們要遍歷hash表選出最小距離,現在直接是堆頂元素
	* 但是我們找到通過橋節點更小的距離後,需要臨時更該堆結構中元素資料
	* 所以系統提供的堆我們需要改寫
	**/

	public static class NodeRecord {
		public Node node;
		public int distance;

		public NodeRecord(Node node, int distance) {
			this.node = node;
			this.distance = distance;
		}
	}

        // 自定義小根堆結構
        // 需要提供add元素的方法,和update元素的方法
        // 需要提供ignore方法,表示我們已經找到from到某節點的最短路徑
        // 再出現from到該節點的其他路徑距離,我們直接忽略
	public static class NodeHeap {
		private Node[] nodes; // 實際的堆結構
		// key 某一個node, value 上面堆中的位置
		// 如果節點曾經進過堆,現在不在堆上,則node對應-1
		// 用來找需要ignore的節點
		private HashMap<Node, Integer> heapIndexMap;
		// key 某一個節點, value 從源節點出發到該節點的目前最小距離
		private HashMap<Node, Integer> distanceMap;
		private int size; // 堆上有多少個點

		public NodeHeap(int size) {
			nodes = new Node[size];
			heapIndexMap = new HashMap<>();
			distanceMap = new HashMap<>();
			size = 0;
		}

                // 該堆是否空
		public boolean isEmpty() {
			return size == 0;
		}

		// 有一個點叫node,現在發現了一個從源節點出發到達node的距離為distance
		// 判斷要不要更新,如果需要的話,就更新
		public void addOrUpdateOrIgnore(Node node, int distance) {
		        // 如果該節點在堆上,就看是否需要更新
			if (inHeap(node)) {
				distanceMap.put(node, Math.min(distanceMap.get(node), distance));
				// 該節點進堆,判斷是否需要調整
				insertHeapify(node, heapIndexMap.get(node));
			}
			// 如果沒有進入過堆。新建,進堆
			if (!isEntered(node)) {
				nodes[size] = node;
				heapIndexMap.put(node, size);
				distanceMap.put(node, distance);
				insertHeapify(node, size++);
			}
			// 如果不在堆上,且進來過堆上,什麼也不做,ignore
		}

                // 彈出from到堆頂節點的元素,獲取到該元素的最小距離,再調整堆結構
		public NodeRecord pop() {
			NodeRecord nodeRecord = new NodeRecord(nodes[0], distanceMap.get(nodes[0]));
			// 把最後一個元素放在堆頂,進行heapify
			swap(0, size - 1);
			heapIndexMap.put(nodes[size - 1], -1);
			distanceMap.remove(nodes[size - 1]);
			// free C++同學還要把原本堆頂節點析構,對java同學不必
			nodes[size - 1] = null;
			heapify(0, --size);
			return nodeRecord;
		}

		private void insertHeapify(Node node, int index) {
			while (distanceMap.get(nodes[index]) 
					< distanceMap.get(nodes[(index - 1) / 2])) {
				swap(index, (index - 1) / 2);
				index = (index - 1) / 2;
			}
		}

		private void heapify(int index, int size) {
			int left = index * 2 + 1;
			while (left < size) {
				int smallest = left + 1 < size && distanceMap.get(nodes[left + 1]) < distanceMap.get(nodes[left])
						? left + 1
						: left;
				smallest = distanceMap.get(nodes[smallest]) 
						< distanceMap.get(nodes[index]) ? smallest : index;
				if (smallest == index) {
					break;
				}
				swap(smallest, index);
				index = smallest;
				left = index * 2 + 1;
			}
		}

                // 判斷node是否進來過堆
		private boolean isEntered(Node node) {
			return heapIndexMap.containsKey(node);
		}

                // 判斷某個節點是否在堆上
		private boolean inHeap(Node node) {
			return isEntered(node) && heapIndexMap.get(node) != -1;
		}

		private void swap(int index1, int index2) {
			heapIndexMap.put(nodes[index1], index2);
			heapIndexMap.put(nodes[index2], index1);
			Node tmp = nodes[index1];
			nodes[index1] = nodes[index2];
			nodes[index2] = tmp;
		}
	}

	// 使用自定義小根堆,改進後的dijkstra演算法
	// 從from出發,所有from能到達的節點,生成到達每個節點的最小路徑記錄並返回
	public static HashMap<Node, Integer> dijkstra2(Node from, int size) {
	        // 申請堆
		NodeHeap nodeHeap = new NodeHeap(size);
		// 在堆上新增from節點到from節點距離為0
		nodeHeap.addOrUpdateOrIgnore(from, 0);
		// 最終的結果集
		HashMap<Node, Integer> result = new HashMap<>();
		while (!nodeHeap.isEmpty()) {
		        // 每次在小根堆彈出堆頂元素
			NodeRecord record = nodeHeap.pop();
			// 拿出的節點
			Node cur = record.node;
			// from到該節點的距離
			int distance = record.distance;
			// 以此為橋接點,找是否有更小的距離到該節點的其他to節點
			// addOrUpdateOrIgnore該方法保證如果from到to的節點沒有,就add
			// 如果有,看是否需要Ignore,如果不需要Ignore且更小,就Update
			for (Edge edge : cur.edges) {
				nodeHeap.addOrUpdateOrIgnore(edge.to, edge.weight + distance);
			}
			result.put(cur, distance);
		}
		return result;
	}

}

1.2.6.2 floyd演算法

圖節點的最短路徑,處理權值可能為負的情況。三層for迴圈,比較簡單暴力

相關文章