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迴圈,比較簡單暴力