图及相关算法
图
准备找实习了,把忘了的东西从头捡一捡
基本实现
大一时候有个特别蠢的问题,一直老想为什么不内置图的实现,现在想想真是蠢到家了……
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)
本文作者:Appletree24
本文链接:https://www.cnblogs.com/appletree24/p/17775934.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步