@@ -201,6 +201,8 @@ for fr, to, w in times:
201201
202202> 以下所有的模板都是基于邻接矩阵建图。
203203
204+ 强烈建议大家学习完专题篇的搜索之后再来学习下面经典算法。大家可以拿几道普通的搜索题目测试下,如果能够做出来再往下学习。推荐题目:[ 最大化一张图中的路径价值] ( https://leetcode-cn.com/problems/maximum-path-quality-of-a-graph/ " 最大化一张图中的路径价值 ")
205+
204206### 最短距离,最短路径
205207
206208#### Dijkstra 算法
@@ -622,11 +624,11 @@ var Floyd-Warshall = function(graph, n){
622624
623625```
624626
625- 如果这道题你可以解决了,我再推荐一道题给你 [ 1617. 统计子树中城市之间最大距离] ( https://leetcode-cn.com/problems/count-subtrees-with-max-distance-between-cities/ ) ,国际版有一个题解代码挺清晰,挺好理解的,只不过没有使用状态压缩性能不是很好罢了,地址:https://leetcode.com/problems/count-subtrees-with-max-distance-between-cities/discuss/1136596/Python-Floyd-Warshall-and-check-all-subtrees
627+ 如果这道题你可以解决了,我再推荐一道题给你 [ 1617. 统计子树中城市之间最大距离] ( https://leetcode-cn.com/problems/count-subtrees-with-max-distance-between-cities/ " 1617. 统计子树中城市之间最大距离 " ) ,国际版有一个题解代码挺清晰,挺好理解的,只不过没有使用状态压缩性能不是很好罢了,地址:https://leetcode.com/problems/count-subtrees-with-max-distance-between-cities/discuss/1136596/Python-Floyd-Warshall-and-check-all-subtrees
626628
627629图上的动态规划算法大家还可以拿这个题目来练习一下。
628630
629- - [ 787. K 站中转内最便宜的航班] ( https://leetcode-cn.com/problems/cheapest-flights-within-k-stops/ )
631+ - [ 787. K 站中转内最便宜的航班] ( https://leetcode-cn.com/problems/cheapest-flights-within-k-stops/ " 787. K 站中转内最便宜的航班 " )
630632
631633#### 贝尔曼-福特算法
632634
@@ -694,11 +696,11 @@ const BellmanFord = (edges, startPoint)=>{
694696
695697推荐阅读:
696698
697- - [ bellman-ford-algorithm] ( https://www.programiz.com/dsa/bellman-ford-algorithm )
699+ - [ bellman-ford-algorithm] ( https://www.programiz.com/dsa/bellman-ford-algorithm " bellman-ford-algorithm " )
698700
699701题目推荐:
700702
701- - [ Best Currency Path] ( https://binarysearch.com/problems/Best-Currency-Path )
703+ - [ Best Currency Path] ( https://binarysearch.com/problems/Best-Currency-Path " Best Currency Path " )
702704
703705### 拓扑排序
704706
@@ -749,83 +751,96 @@ graph = {0: [1, 2], 1: [3], 2: [3], 3: [4, 5], 4: [], 5: []}
749751topologicalSort(graph)
750752```
751753
752- <!-- ### 最小生成树
754+ ### 最小生成树
753755
754- 什么是最小生成树,这两个算法又是如何计算最小生成树的呢?
756+ 首先我们来看下什么是生成树。
755757
756- 首先我们来看下什么是生成树。 生成树是一个图的一部分,生成树包含图的**所有顶点**,且不包含环, 这也是为什么叫做生成树,而不是生成图的原因。你可以将生成树看成是根节点不确定的多叉树 。
758+ 首先生成树是原图的一个子图,它本质是一棵树, 这也是为什么叫做生成树,而不是生成图的原因。其次生成树应该包括图中所有的顶点。 如下图由于没有包含所有顶点,换句话说所有顶点没有在同一个联通域,因此不是一个生成树 。
757759
758- 最小生成树是在生成树的基础上加了**最小**关键字,是最小权重生成树的简称。其指的是对于带权图来说,生成树的权重是其所有边的权重和,那么**最小生成树就是权重和最小的生成树**,由此可看出,不管是生成树还是最小生成树都可能不唯一。
760+ ![ ] ( https://tva1.sinaimg.cn/large/008i3skNly1gw90jdhugxj30jg0c6mxj.jpg )
759761
760- 这在实际生活中有很强的价值。比如我要修建一个地铁,并覆盖 n 个站,如果建造才能使得花费最小?由于每个站之间的路线不同,因此造价也不一样,因此这就是一个最小生成树的实际使用场景,类似的例子还有很多。
762+ > 黄色顶点没有包括在内
761763
762- 
764+ 你可以将生成树看成是根节点不确定的多叉树,由于是一棵树,那么一定不包含环。如下图就不是生成树。
763765
764- (图来自维基百科)
766+ ![ ] ( https://tva1.sinaimg.cn/large/008i3skNly1gw90i7uk9aj30pw0cmq3l.jpg )
765767
766- Kruskal 和 Prim 是两个经典的求最小生成树的算法,本节我们就来了解一下它们。
767-
768- #### Kruskal
768+ 因此不难得出,最小生成树的边的个数是 n - 1,其中 n 为顶点个数。
769769
770- Kruskal 算法也被形象地称为**加边法**,每前进一次都选择权重最小的边,加入到结果集。为了防止环的产生(增加环是无意义的,只要权重是正数,一定会使结果更差),我们需要检查下当前选择的边是否和已经选择的边联通了。如果联通了,是没有必要选取的,因为这会使得环产生。因此算法上,我们可使用并查集辅助完成。下面算法中的 find_parent 部分,实际上就是并查集的核心代码,只是我们没有将其封装并使用罢了 。
770+ 接下来我们看下什么是最小生成树 。
771771
772- Kruskal 具体算法:
773-
774- 1. 对边进行排序
775- 2. 将 n 个顶点初始化为 n 个联通域
776- 3. 按照权值从小到大选择边加入到结果集,如果当前选择的边是否和已经选择的边联通了,则放弃选择,否则进行选择,加入到结果集。
777- 4. 重复 3 直到我们找到了一个联通域大小为 n 的子图
772+ 最小生成树是在生成树的基础上加了** 最小** 关键字,是最小权重生成树的简称。从这句话也可以看出,最小生成树处理正是有权图。生成树的权重是其所有边的权重和,那么** 最小生成树就是权重和最小的生成树** ,由此可看出,不管是生成树还是最小生成树都可能不唯一。
778773
779- 代码模板:
774+ 最小生成树在实际生活中有很强的价值。比如我要修建一个地铁,并覆盖 n 个站,这 n 个站要互相都可以到达(同一个联通域),如果建造才能使得花费最小?由于每个站之间的路线不同,因此造价也不一样,因此这就是一个最小生成树的实际使用场景,类似的例子还有很多。
780775
781- ```py
782- from typing import List, Tuple
776+ ![ ] ( https://tva1.sinaimg.cn/large/008eGmZEly1gmst4yvz7sj308c06qjrl.jpg )
783777
778+ (图来自维基百科)
784779
785- def kruskal(num_nodes: int, edges: List[Tuple[int, int, int]]) -> int:
786- """
787- >>> kruskal(4, 3, [(0, 1, 3), (1, 2, 5), (2, 3, 1)])
788- [(2, 3, 1), (0, 1, 3), (1, 2, 5)]
780+ 不难看出,计算最小生成树就是从边集合中挑选 n - 1 个边,使得其满足生成树,并且权值和最小。
789781
790- >>> kruskal(4, 5, [(0, 1, 3), (1, 2, 5), (2, 3, 1), (0, 2, 1), (0, 3, 2)])
791- [(2, 3, 1), (0, 2, 1), (0, 1, 3)]
782+ Kruskal 和 Prim 是两个经典的求最小生成树的算法,这两个算法又是如何计算最小生成树的呢?本节我们就来了解一下它们。
792783
793- >>> kruskal(4, 6, [(0, 1, 3), (1, 2, 5), (2, 3, 1), (0, 2, 1), (0, 3, 2),
794- ... (2, 1, 1)])
795- [(2, 3, 1), (0, 2, 1), (2, 1, 1)]
796- """
797- edges = sorted(edges, key=lambda edge: edge[2])
784+ #### Kruskal
798785
799- parent = list(range(num_nodes))
786+ Kruskal 相对比较容易理解,推荐掌握。
800787
801- def find_parent(i):
802- if i != parent[i]:
803- parent[i] = find_parent(parent[i])
804- return parent[i]
788+ Kruskal 算法也被形象地称为** 加边法** ,每前进一次都选择权重最小的边,加入到结果集。为了防止环的产生(增加环是无意义的,只要权重是正数,一定会使结果更差),我们需要检查下当前选择的边是否和已经选择的边联通了。如果联通了,是没有必要选取的,因为这会使得环产生。因此算法上,我们可使用并查集辅助完成。关于并查集,我们会在之后的进阶篇进行讲解。
805789
806- minimum_spanning_tree_cost = 0
807- minimum_spanning_tree = []
790+ > 下面代码中的 find_parent 部分,实际上就是并查集的核心代码,只是我们没有将其封装并使用罢了。
808791
809- for edge in edges:
810- parent_a = find_parent(edge[0])
811- parent_b = find_parent(edge[1])
812- if parent_a != parent_b:
813- minimum_spanning_tree_cost += edge[2]
814- minimum_spanning_tree.append(edge)
815- parent[parent_a] = parent_b
792+ Kruskal 具体算法:
816793
817- return minimum_spanning_tree
794+ 1 . 对边按照权值从小到大进行排序。
795+ 2 . 将 n 个顶点初始化为 n 个联通域
796+ 3 . 按照权值从小到大选择边加入到结果集,每次** 贪心地** 选择最小边。如果当前选择的边是否和已经选择的边联通了(如果强行加就有环了),则放弃选择,否则进行选择,加入到结果集。
797+ 4 . 重复 3 直到我们找到了一个联通域大小为 n 的子图
818798
799+ 代码模板:
819800
820- if __name__ == "__main__": # pragma: no cover
821- num_nodes, num_edges = list(map(int, input().strip().split()))
822- edges = []
801+ 其中 edge 是一个数组,数组每一项都形如: (cost, fr, to),含义是 从 fr 到 to 有一条权值为 cost的边。
823802
824- for _ in range(num_edges):
825- node1, node2, cost = [int(x) for x in input().strip().split()]
826- edges.append((node1, node2, cost))
803+ ``` py
804+ class DisjointSetUnion :
805+ def __init__ (self , n ):
806+ self .n = n
807+ self .rank = [1 ] * n
808+ self .f = list (range (n))
809+
810+ def find (self , x : int ) -> int :
811+ if self .f[x] == x:
812+ return x
813+ self .f[x] = self .find(self .f[x])
814+ return self .f[x]
815+
816+ def unionSet (self , x : int , y : int ) -> bool :
817+ fx, fy = self .find(x), self .find(y)
818+ if fx == fy:
819+ return False
820+
821+ if self .rank[fx] < self .rank[fy]:
822+ fx, fy = fy, fx
823+
824+ self .rank[fx] += self .rank[fy]
825+ self .f[fy] = fx
826+ return True
827827
828- kruskal(num_nodes, edges)
828+ class Solution :
829+ def Kruskal (self , edges ) -> int :
830+ n = len (points)
831+ dsu = DisjointSetUnion(n)
832+
833+ edges.sort()
834+
835+ ret, num = 0 , 1
836+ for length, x, y in edges:
837+ if dsu.unionSet(x, y):
838+ ret += length
839+ num += 1
840+ if num == n:
841+ break
842+
843+ return ret
829844```
830845
831846#### Prim
@@ -838,136 +853,43 @@ Prim 具体算法:
8388532 . 在集合 E 中 (集合 E 为原始图的边集)选取最小的边 <u, v> 其中 u 为 MV 中已有的元素,而 v 为 MV 中不存在的元素(像不像上面说的** 不断生长的真实世界的树** ),将 v 加入到 MV,将 <u, v> 加到 ME。
8398543 . 重复 2 直到我们找到了一个联通域大小为 n 的子图
840855
841- 算法模板 :
856+ 代码模板 :
842857
843- > 为了体现完整性,代码中关于堆的部分采用了手动实现的方式 。
858+ 其中 dist 是二维数组,dist [ i ] [ j ] = x 表示顶点 i 到顶点 j 有一条权值为 x 的边 。
844859
845860``` py
846- import sys
847- from collections import defaultdict
848-
849-
850- def PrimsAlgorithm(l): # noqa: E741
851-
852- nodePosition = []
853-
854- def get_position(vertex):
855- return nodePosition[vertex]
856-
857- def set_position(vertex, pos):
858- nodePosition[vertex] = pos
859-
860- def top_to_bottom(heap, start, size, positions):
861- if start > size // 2 - 1:
862- return
863- else:
864- if 2 * start + 2 >= size:
865- m = 2 * start + 1
866- else:
867- if heap[2 * start + 1] < heap[2 * start + 2]:
868- m = 2 * start + 1
869- else:
870- m = 2 * start + 2
871- if heap[m] < heap[start]:
872- temp, temp1 = heap[m], positions[m]
873- heap[m], positions[m] = heap[start], positions[start]
874- heap[start], positions[start] = temp, temp1
875-
876- temp = get_position(positions[m])
877- set_position(positions[m], get_position(positions[start]))
878- set_position(positions[start], temp)
879-
880- top_to_bottom(heap, m, size, positions)
881-
882- # Update function if value of any node in min-heap decreases
883- def bottom_to_top(val, index, heap, position):
884- temp = position[index]
885-
886- while index != 0:
887- if index % 2 == 0:
888- parent = int((index - 2) / 2)
889- else:
890- parent = int((index - 1) / 2)
861+ class Solution :
862+ def Prim (self , dist ) -> int :
863+ n = len (dist)
864+ d = [float (" inf" )] * n # 表示各个顶点与加入最小生成树的顶点之间的最小距离.
865+ vis = [False ] * n # 表示是否已经加入到了最小生成树里面
866+ d[0 ] = 0
867+ ans = 0
868+ for _ in range (n):
869+ # 寻找目前这轮的最小d
870+ M = float (" inf" )
871+ for i in range (n):
872+ if not vis[i] and d[i] < M:
873+ node = i
874+ M = d[i]
875+ vis[node] = True
876+ ans += M
877+ for i in range (n):
878+ if not vis[i]:
879+ d[i] = min (d[i], dist[i][node])
880+ return ans
891881
892- if val < heap[parent]:
893- heap[index] = heap[parent]
894- position[index] = position[parent]
895- set_position(position[parent], index)
896- else:
897- heap[index] = val
898- position[index] = temp
899- set_position(temp, index)
900- break
901- index = parent
902- else:
903- heap[0] = val
904- position[0] = temp
905- set_position(temp, 0)
906-
907- def heapify(heap, positions):
908- start = len(heap) // 2 - 1
909- for i in range(start, -1, -1):
910- top_to_bottom(heap, i, len(heap), positions)
911-
912- def deleteMinimum(heap, positions):
913- temp = positions[0]
914- heap[0] = sys.maxsize
915- top_to_bottom(heap, 0, len(heap), positions)
916- return temp
917-
918- visited = [0 for i in range(len(l))]
919- Nbr_TV = [-1 for i in range(len(l))] # Neighboring Tree Vertex of selected vertex
920- # Minimum Distance of explored vertex with neighboring vertex of partial tree
921- # formed in graph
922- Distance_TV = [] # Heap of Distance of vertices from their neighboring vertex
923- Positions = []
924-
925- for x in range(len(l)):
926- p = sys.maxsize
927- Distance_TV.append(p)
928- Positions.append(x)
929- nodePosition.append(x)
930-
931- TreeEdges = []
932- visited[0] = 1
933- Distance_TV[0] = sys.maxsize
934- for x in l[0]:
935- Nbr_TV[x[0]] = 0
936- Distance_TV[x[0]] = x[1]
937- heapify(Distance_TV, Positions)
938-
939- for i in range(1, len(l)):
940- vertex = deleteMinimum(Distance_TV, Positions)
941- if visited[vertex] == 0:
942- TreeEdges.append((Nbr_TV[vertex], vertex))
943- visited[vertex] = 1
944- for v in l[vertex]:
945- if visited[v[0]] == 0 and v[1] < Distance_TV[get_position(v[0])]:
946- Distance_TV[get_position(v[0])] = v[1]
947- bottom_to_top(v[1], get_position(v[0]), Distance_TV, Positions)
948- Nbr_TV[v[0]] = vertex
949- return TreeEdges
950-
951-
952- if __name__ == "__main__": # pragma: no cover
953- # < --------- Prims Algorithm --------- >
954- n = int(input("Enter number of vertices: ").strip())
955- e = int(input("Enter number of edges: ").strip())
956- adjlist = defaultdict(list)
957- for x in range(e):
958- l = [int(x) for x in input().strip().split()] # noqa: E741
959- adjlist[l[0]].append([l[1], l[2]])
960- adjlist[l[1]].append([l[0], l[2]])
961- print(PrimsAlgorithm(adjlist))
962882```
963883
964884#### 两种算法比较
965885
966- 为了后面描述方便,我们令 V 为图中的顶点数, E 为图中的边数。那么 KruKal 的算法复杂度是 $O(ElogE)$,Prim 的算法时间复杂度为 $E + VlogV$。
886+ 为了后面描述方便,我们令 V 为图中的顶点数, E 为图中的边数。那么 KruKal 的算法复杂度是 $O(ElogE)$,Prim 的算法时间复杂度为 $E + VlogV$。因此 Prim 适合适用于稠密图,而 KruKal 则适合稀疏图。
887+
888+ 大家也可以参考一下 [ 维基百科 - 最小生成树] ( https://zh.wikipedia.org/wiki/%E6%9C%80%E5%B0%8F%E7%94%9F%E6%88%90%E6%A0%91 " 维基百科 - 最小生成树 ") 的资料作为补充。
967889
968- KruKal 是基于图的联通性贪心算法。而 Prim 则是基于堆的贪心算法。
890+ 另外这里有一份视频学习资料,其中的动画做的不错,大家可以作为参考,地址: https://www.bilibili.com/video/BV1Eb41177d1/
969891
970- 大家也可以参考一下 [维基百科 - 最小生成树 ](https://zh.wikipedia.org/wiki/%E6%9C%80%E5%B0%8F%E7%94%9F%E6%88%90%E6%A0%91) 的资料作为补充。 -->
892+ 大家可以使用 LeetCode 的 [ 1584. 连接所有点的最小费用 ] ( https://leetcode-cn.com/problems/min-cost-to-connect-all-points/ " 1584. 连接所有点的最小费用 ") 来练习该算法。
971893
972894### 其他算法
973895
@@ -1104,14 +1026,14 @@ for i in range(len(a)):
11041026 print (a[i])
11051027```
11061028
1107- 典型题目[ 1263. 推箱子] ( https://leetcode-cn.com/problems/minimum-moves-to-move-a-box-to-their-target-location/ )
1029+ 典型题目[ 1263. 推箱子] ( https://leetcode-cn.com/problems/minimum-moves-to-move-a-box-to-their-target-location/ " 1263. 推箱子 " )
11081030
11091031#### 二分图
11101032
11111033二分图我在这两道题中讲过了,大家看一下之后把这两道题做一下就行了。其实这两道题和一道题没啥区别。
11121034
1113- - [ 0886. 可能的二分法] ( https://leetcode-solution-leetcode-pp.gitbook.io/leetcode-solution/medium/886.possible-bipartition )
1114- - [ 0785. 判断二分图] ( https://leetcode-solution-leetcode-pp.gitbook.io/leetcode-solution/medium/785.is-graph-bipartite )
1035+ - [ 0886. 可能的二分法] ( https://leetcode-solution-leetcode-pp.gitbook.io/leetcode-solution/medium/886.possible-bipartition " 0886. 可能的二分法 " )
1036+ - [ 0785. 判断二分图] ( https://leetcode-solution-leetcode-pp.gitbook.io/leetcode-solution/medium/785.is-graph-bipartite " 0785. 判断二分图 " )
11151037
11161038推荐顺序为: 先看 886 再看 785。
11171039
0 commit comments