zoukankan      html  css  js  c++  java
  • 《算法笔记》10. 并查集、图相关算法、看完这篇不能再说不会了。

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

  • 相关阅读:
    【centos6.5 安装 node.js + npm】
    【钉钉PC】PC端钉钉清除缓存
    【laravel5.4】中jquery的post Ajax提交
    python 设计模式之中介者模式
    python 设计模式之备忘录模式
    python 设计模式之观察者模式
    python 设计模式之策略模式
    23种设计模式有哪些,不带定义,不带例子
    python 设计模式之模板方法模式
    python 设计模式之访问者模式
  • 原文地址:https://www.cnblogs.com/darope/p/13444839.html
Copyright © 2011-2022 走看看