并查集、图相关算法

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循环,比较简单暴力

posted @ 2020-08-06 10:49  x1aoda1  阅读(644)  评论(0编辑  收藏  举报