图及相关算法
图
准备找实习了,把忘了的东西从头捡一捡
基本实现
大一时候有个特别蠢的问题,一直老想为什么不内置图的实现,现在想想真是蠢到家了……
Go语言实现无向无环图
import "fmt"
//Implment by adjacency matrix
type graphadjMat struct {
vertices []int
adjMat [][]int
}
func newGraphAdjMat(vertices []int, edges [][]int) *graphadjMat {
n := len(vertices)
adjMat := make([][]int, n)
for i := range adjMat {
adjMat[i] = make([]int, n)
}
g := &graphadjMat{
vertices: vertices,
adjMat: adjMat,
}
for i := range edges {
g.addEdge(edges[i][0], edges[i][1])
}
return g
}
func (g *graphadjMat) addVertex(val int) {
n := g.size()
g.vertices = append(g.vertices, val)
newRow := make([]int, n)
g.adjMat = append(g.adjMat, newRow)
for i := range g.adjMat {
//Looking at the matirx horizontally,the action is euqivalent to inserting 0 at the end of the row
g.adjMat[i] = append(g.adjMat[i], 0)
}
}
func (g *graphadjMat) removeVertex(index int) {
if index >= g.size() {
fmt.Errorf("%s", "Index out of Bounds")
return
}
g.vertices = append(g.vertices[:index], g.vertices[index+1:]...)
g.adjMat = append(g.adjMat[:index], g.adjMat[index+1:]...)
for i := range g.adjMat {
g.adjMat[i] = append(g.adjMat[i][:index], g.adjMat[i][index+1:]...)
}
}
// acyclic graph [ˌeɪˈsaɪklɪk ɡræf]
func (g *graphadjMat) addEdge(i, j int) {
if i < 0 || j < 0 || i >= g.size() || j >= g.size() || i == j {
fmt.Errorf("%s", "Index out of bounds")
}
g.adjMat[i][j] = 1
g.adjMat[j][i] = 1
}
func (g *graphadjMat) removeEdge(i, j int) {
if i < 0 || j < 0 || i >= g.size() || j >= g.size() || i == j {
fmt.Errorf("%s", "Index out of bounds")
}
g.adjMat[i][j] = 0
g.adjMat[j][i] = 0
}
func (g *graphadjMat) size() int {
return len(g.vertices)
}
func sum[T int | int16 | int32 | int64](nums []T) T {
var sum T
for _, v := range nums {
sum += v
}
return sum
}
func max[T int | int16 | int32 | int64](a, b T) T {
if a > b {
return a
} else {
return b
}
}
func minWithT[T int | int16 | int32 | int64](numList ...T) T {
var minValue T = numList[0]
for _, v := range numList {
if minValue > v {
minValue = v
}
}
return minValue
}
type vertex struct {
val int
}
type graphadjMat struct {
vertices []int
adjMat [][]int
}
type graphAdjList struct {
adjList map[vertex][]vertex
}
//Implement by adjacency list
func newGraphAdjList(edges [][]vertex) *graphAdjList {
g := &graphAdjList{
adjList: make(map[vertex][]vertex),
}
for _, edge := range edges {
g.addVertex(edge[0])
g.addVertex(edge[1])
g.addEdge(edge[0], edge[1])
}
return g
}
func (g *graphAdjList) size() int {
return len(g.adjList)
}
func (g *graphAdjList) addVertex(ver vertex) {
_, ok := g.adjList[ver]
if ok {
fmt.Errorf("%s", "The vertex has been found")
return
}
g.adjList[ver] = make([]vertex, 0)
}
func (g *graphAdjList) deleteVertex(ver vertex) {
_, ok := g.adjList[ver]
if !ok {
panic("error,the vertex cant be found")
}
delete(g.adjList, ver)
for i, list := range g.adjList {
g.adjList[i] = deleteSliceElems(list, ver)
}
}
func (g *graphAdjList) addEdge(ver1, ver2 vertex) {
_, ok1 := g.adjList[ver1]
_, ok2 := g.adjList[ver2]
if !ok1 || !ok2 || ver1 == ver2 {
panic("Error")
}
g.adjList[ver1] = append(g.adjList[ver1], ver2)
g.adjList[ver2] = append(g.adjList[ver2], ver1)
}
func (g *graphAdjList) removeEdge(ver1, ver2 vertex) {
_, ok1 := g.adjList[ver1]
_, ok2 := g.adjList[ver2]
if !ok1 || !ok2 || ver1 == ver2 {
panic("Error")
}
g.adjList[ver1] = deleteSliceElems(g.adjList[ver1], ver1)
g.adjList[ver2] = deleteSliceElems(g.adjList[ver2], ver2)
}
func deleteSliceElems(slice []vertex, ver vertex) []vertex {
for i, v := range slice {
if v == ver {
slice = append(slice[:i], slice[i+1:]...)
break
}
}
return slice
}
Union-Find algorithm(并查集算法)
Overview
作为最小生成树算法的前置内容,以及解决部分问题的有效解法,并查集算法有必要学习并记录下来
API we need to implement(需要实现的API)
type UF struct{
func union(p,q int)
func connected(p,q int) bool
func count() int
}
union
函数表示将p与q两节点连通connected
函数表示判断p与q节点是否联通count
函数将返回图中的连通分量数量- 请注意,上述内容不符合Go语言语法要求,仅是说明举例用途
结合一下离散数学的内容,连通是一种等价关系。满足自反性、对称性、传递性
How to implement(实现思路)
设定每个节点有一个指针指向其父节点,如果父节点是自身的节点就是根节点。所以一开始所有节点应该都算是根节点。
type UF struct{
count int
parents []int
}
func newUF(n int) *UF{
uf:=&UF{count:n,parents:make([]int,n)}
for i:=0;i<n:i++{
//Every node's parents is itself when initilize
uf.parents[i]=i
}
return uf
}
如果A节点需要与B节点联通,只需要将A节点的根节点连接到B节点的根节点即可。
func (uf *UF) unionWithPro(p, q int) {
rootP := uf.findWithPro(p)
rootQ := uf.findWithPro(q)
if rootP == rootQ {
return
}
uf.parents[rootP] = rootQ
uf.count--
}
但这样会出现问题,在极端情况下,可能会经过连接形成一条链表。也就是说此时高度为N,二叉树不平衡,这样会导致如下问题:
如果我们需要判断两节点是否联通,需要判断二者的根节点是否为同一节点,所以就需要一个API可以找到某节点的根节点:
func (uf *UF) findWithPro(x int) int {
for uf.parents[x] != x {
x = uf.parents[x]
}
return x
}
不难分析,这个函数会从某个节点向上遍历直至树根,时间复杂度为高度O(logN),但可惜正如上面所言,当树极度不平衡时,会退化为O(N),当数据量极大时,会造成很大的性能损失。所以需要优化。
优化
问题的根源在于union
函数只是粗暴的把一个根节点接到另外一个,并没有考虑两种里哪一种可以更好的维护平衡。如果我们每次都将高度较小的树接到高度较大的树下面,就可以避免这一问题了。遂修改代码如下:
type UF struct {
//the amount of connected components in the graph
count int
//a node's parents node
parents []int
//every root node's tree's height
size []int
}
func newUF(n int) *UF {
uf := &UF{count: n, parents: make([]int, n), size: make([]int, n)}
for i := 0; i < n; i++ {
uf.parents[i] = i
uf.size[i] = 1
}
return uf
}
func (uf *UF) union(p, q int) {
rootP := uf.find(p)
rootQ := uf.find(q)
if rootP == rootQ {
return
}
sizeP := uf.size[rootP]
sizeQ := uf.size[rootQ]
if sizeP > sizeQ {
uf.parents[rootQ] = rootP
uf.size[rootP] += sizeQ
} else {
uf.parents[rootP] = rootQ
uf.size[rootQ] += sizeP
}
uf.count--
}
为了判断树对应的高度,我们还需要修改UF数据结构的定义,加入size数组,存储每个根节点对应树的高度。
至此,时间复杂度下降为O(logN)。
路径压缩
此时按照上面的代码,我们也许可以得到一颗这样的树。但其实我们还可以进一步优化,因为我们实际上只关心某两个节点的根节点是不是同一个,以便来判断连通与否。所以我们可以尝试压缩路径到同一层,这样就可以将时间复杂度降为O(1)
可以通过修改find函数的逻辑实现这一操作,有迭代法和递归法两种,此处只记录效果更好的递归法:
func (uf *UF) find(x int) int {
if uf.parents[x] != x {
//we move the current node to previous layer if it doesn't fit the condition
uf.parents[x] = uf.find(uf.parents[x])
}
return uf.parents[x]
}
如果采用迭代写法,会压缩成如下的效果
如果采用递归写法,会压缩成如下:
借助路径压缩,size数组就可以去掉,保留与否均可。
Kruskal
Overview
克鲁斯卡尔是一种贪心算法,用来求图中的最小生成树。每次都选择最小的边加入(前提是保证不会形成回路),以便得到全局权重和最小。所以我们需要以下手段:
- 能够选择权值最小的边(排序解决)
- 能够判断加入后是否形成回路(并查集解决)
实现
1584. 连接所有点的最小费用 - 力扣(LeetCode)
以上题为例,将并查集也用上。
首先解决第一点,将图中权值排序,遍历每个点,计算到其他点的距离,即为此边的权值。最后排序即可。
第二点,是否形成回路,每次我们都从排序好了的边集合中选取,选取前判断此边的两点是否连通,如果已连通就跳过,未连通便加入,并在并查集中连接两点。
有一个结论是,最终形成的最小生成树一定有n(节点数量)-1条边,当我们判断已经加入这么多条边时,就可以返回最后的结果
type UF struct {
count int
parents []int
size []int
}
func newUF(n int) *UF {
uf := &UF{count: n, parents: make([]int, n), size: make([]int, n)}
for i := 0; i < n; i++ {
uf.parents[i] = i
uf.size[i] = 1
}
return uf
}
func (uf *UF) counts() int {
return uf.count
}
func (uf *UF) find(x int) int {
if uf.parents[x] != x {
uf.parents[x] = uf.find(uf.parents[x])
}
return uf.parents[x]
}
func (uf *UF) union(p, q int) {
rootP := uf.find(p)
rootQ := uf.find(q)
if rootP == rootQ {
return
}
if uf.size[rootP] > uf.size[rootQ] {
uf.parents[rootQ] = rootP
uf.size[rootP] += uf.size[rootQ]
} else {
uf.parents[rootP] = rootQ
uf.size[rootQ] += uf.size[rootP]
}
uf.count--
}
func (uf *UF) connected(p, q int) bool {
rootP := uf.find(p)
rootQ := uf.find(q)
return rootP == rootQ
}
func minCostConnectPoints(points [][]int) int {
var ans = 0
type edge struct{ v, w, dis int }
edges := []edge{}
for i := 0; i < len(points); i++ {
for j := i + 1; j < len(points); j++ {
edges = append(edges, edge{i, j, int(cal_distance(points[i][0], points[i][1],
points[j][0], points[j][1]))})
}
}
sort.Slice(edges, func(i, j int) bool { return edges[i].dis < edges[j].dis })
graphUnion := newUF(len(points))
edgeCount := len(points) - 1
for _, v := range edges {
if graphUnion.connected(v.v, v.w) {
continue
} else {
ans += v.dis
edgeCount--
if edgeCount == 0 {
break
}
graphUnion.union(v.v, v.w)
}
}
return ans
}
func cal_distance(x1, y1, x2, y2 int) float64 {
x := math.Abs(float64(x1 - x2))
y := math.Abs(float64(y1 - y2))
return x + y
}
Dijkstra
Overview
Dijkstra算法可以求解有向加权图中的单源最值路径,如何判断可以求解的最值为最大值还是最小值,需要通过以下方式:
- 首先看需求,不满足需求都是白搭(First priority)
- 如果图中每次添加边时,你的总权重都会减小,那么就可以求解最大值,反之,可以求解最小值
Dijkstra算法写起来和BFS大同小异,只是要在其中加入选择当前可到达节点中花费最小的操作
铺垫
闲的没事,换Python写一写
本文选择用邻接表形式实现图
首先我们在求解单源最短路径时,既然是路径,那就需要知道当前节点都能去到哪些节点,也就是获得某一节点的邻接节点(adjacency node)这就是第一个API
def ajd(s:int) -> List[int]:
另外我们还需要选择花费最小的道路,所以还需要知道两点间的路径权值是多少,就是第二个API:
def weight(graph: List[List[int]], fromNode: int, desNode: int) -> int:
for wList in graph[fromNode]:
if wList[0] == desNode:
return wList[1]
上面的weight函数实际逻辑需要根据图的存储结构进行适当变化
在写Dijkstra前,可以通过BFS模板来辅助理解:
def bfs_model(start:TreeNode) -> None:
queue = []
visited = set()
queue.append(start)
visited.add(start)
layerCount = 0
while len(queue)!=0:
curLayer = len(queue)
for i in range(curLayer):
curNode = queue.pop(0)
print(f"{curNode}在第{layerCount}层")
for adjNode in curNode.adj():
if adjNode not in visited:
queue.append(adjNode)
visited.add(adjNode)
step+=1
其中的while循环保证每次遍历至下一层(每次queue中保存一层的内容)有关层数或是长度的操作在while循环内操作即可
for循环则是横向遍历,一层层扩散,遍历当前层的所有节点,有关每个节点的细致操作在for循环内进行即可
针对BFS算法,有时可能需要知道每个节点所在的层数,但Dijkstra算法仅需知道两点间最短路径的长度,关心的是走到目前为止的路径和,所以for循环其实可以去掉。
或者也可以存储一个自定义的数据结构,其中包含当前节点,以及节点对应的层数。在每次入队新节点的时候,肯定是当前节点的儿子辈,传入参数是当前结构中的层数加一就可以了。
class State:
def __init__(self, node: TreeNode, depth: int):
self.depth = depth
self.node = node
def levelTraverse(root: TreeNode) -> None:
if not root:
return 0
q = []
q.append(State(root, 1))
while q:
cur = q.pop(0)
cur_node = cur.node
cur_depth = cur.depth
print(f"节点 {cur_node} 在第 {cur_depth} 层")
if cur_node.left:
q.append(State(cur_node.left, cur_depth + 1))
if cur_node.right:
q.append(State(cur_node.right, cur_depth + 1))
实现
def dijkstra_slide(start: int, graph: List[List[int]]) -> List[int]:
nodeCount = len(graph)
distTo = [0]*nodeCount
for i in range(nodeCount):
# if we need max value, use -inf, otherwise the opposite
distTo[i] = float('inf')
distTo[start] = 0
pri_queue = PriorityQueue(lambda a, b: a.distance - b.distance)
pri_queue.put(State(start, 0))
# or while not pri_queue.empty() is okay
while len(pri_queue) != 0:
curNode = pri_queue.get()
curDistance = curNode.distance
curId = curNode.id
if curDistance > distTo[curId]:
continue
for adjId in adj(curId):
if distTo[adjId] > distTo[curId]+weight(graph,curNode,adjId):
distTo[adjId] = distTo[curId]+weight(graph,curNode,adjId)
pri_queue.put(State(adjId,distTo[curId]+weight(graph,curNode,adjId)))
return distTo
不再需要visited数组避免无限循环的原因是,对于整幅图,其最小路径是确定的,队列不会一直减小。另外,使用优先队列的原因是,我们需要维护一个从小到大排列的队列,方便每次选取花费最小的路径,自然而然地就想到使用优先队列实现。
上述dijkstra函数会返回列表,对应指定start节点到其他所有节点的最小距离,如果只需要某两点间的最小距离,这么做效率显然不高,需要通过特判提前结束才行。
def dijkstra_slide(start: int, end: int,graph: List[List[int]]) -> int:
nodeCount = len(graph)
distTo = [0]*nodeCount
for i in range(nodeCount):
# if we need max value, use -inf, otherwise the opposite
distTo[i] = float('inf')
distTo[start] = 0
pri_queue = PriorityQueue(lambda a, b: a.distance - b.distance)
pri_queue.put(State(start, 0))
# or while not pri_queue.empty() is okay
while len(pri_queue) != 0:
curNode = pri_queue.get()
curDistance = curNode.distance
curId = curNode.id
#focus here, other code maybe a bit wrongs, nvm
if curId==end:
return curDistance
if curDistance > distTo[curId]:
continue
for adjId in adj(curId):
if distTo[adjId] > distTo[curId]+weight(graph,curNode,adjId):
distTo[adjId] = distTo[curId]+weight(graph,curNode,adjId)
pri_queue.put(State(adjId,distTo[curId]+weight(graph,curNode,adjId)))
return distTo
题目
743. 网络延迟时间 - 力扣(LeetCode)
符合算法要求,加权有向图,没有负权重边。并且每次添加边时,总权重和都会增加,所以可以求解最小值,同时也符合需求。
from typing import List
from queue import PriorityQueue
class State:
def __init__(self, id, distFromStart) -> None:
self.id = id
self.distFromStart = distFromStart
def __lt__(self,other):
return self.distFromStart < other.distFromStart
def weight(graph:List[List[int]],fromNode: int, desNode: int) -> int:
for wList in graph[fromNode]:
if wList[0] == desNode:
return wList[1]
def adj(graph:List[List[tuple[int]]],s: int) -> List[int]:
adjList = []
for item in graph[s]:
adjList.append(item[0])
return adjList
def dijkstra(start: int, graph: List[List[tuple[int]]]) -> List[int]:
vCount = len(graph)
distTo = [float('inf')] * vCount
distTo[start] = 0
pq = PriorityQueue()
pq.put(State(start, 0))
while not pq.empty():
curState = pq.get()
curId = curState.id
curDist = curState.distFromStart
if curDist > distTo[curId]:
continue
for (nextNodeId, weight) in graph[curId]:
distToNextNode = curDist + weight
if distTo[nextNodeId] > distToNextNode:
distTo[nextNodeId] = distToNextNode
pq.put(State(nextNodeId, distToNextNode))
return distTo
class Solution:
def networkDelayTime(self, times: List[List[int]], n: int, k: int) -> int:
graph = [[] for _ in range(n+1)]
for edge in times:
graph[edge[0]].append((edge[1],edge[2]))
distTo = dijkstra(k,graph)
ans = 0
for dist in distTo[1:]:
if dist == float('inf'):
return -1
ans = max(ans,dist)
return ans
1514. 概率最大的路径 - 力扣(LeetCode)
这题看着不符合要求,实际上也可以做,因为我们可以换个角度看。
对于不满足的有向图,其实无向图就是有向图,只不过是双向。
此题要求求解最大值,同样也可以解决,因为当增加一条新边时,概率都是小于1的数,并且独立事件可以通过乘积计算概率,所以总的权重和会变小,这符合可以求解最大值的要求。
from typing import List
from queue import PriorityQueue
import heapq
walkPos = [[1, 0], [-1, 0], [0, 1], [0, -1]]
class State:
def __init__(self, id:int, probability:int) -> None:
self.id = id
self.probability = probability
def __lt__(self, other):
return self.probability > other.probability
def dijkstra(start: int, end:int,graph: List[List[List[float]]]) -> List[int]:
#graph's length is the amount of nodes
res = [-1] * len(graph)
res[start] = 1
pq = []
heapq.heappush(pq,State(start,1))
while pq:
cur_node = heapq.heappop(pq)
cur_pro = cur_node.probability
cur_id = cur_node.id
if cur_id == end:
return cur_pro
if cur_pro < res[cur_id]:
continue
for neighbours in graph[cur_id]:
nextId = neighbours[0]
nextPro = neighbours[1]
if nextPro*res[cur_id] > res[nextId]:
res[nextId] = nextPro*res[cur_id]
heapq.heappush(pq,State(nextId,nextPro*res[cur_id]))
return 0.0
class Solution:
def maxProbability(self, n: int, edges: List[List[int]], succProb: List[float], start_node: int, end_node: int) -> float:
graph = [[] for _ in range(n)]
for i in range(len(edges)):
fromNode = edges[i][0]
toNode = edges[i][1]
probability = succProb[i]
graph[fromNode].append([toNode,probability])
graph[toNode].append([fromNode,probability])
return dijkstra(start_node,end_node,graph)