钱塘江上潮信来 今日方知我是我.|

Appletree24

园龄:2年10个月粉丝:25关注:0

2023-10-19 23:00阅读: 23评论: 0推荐: 0

图及相关算法

准备找实习了,把忘了的东西从头捡一捡

基本实现

大一时候有个特别蠢的问题,一直老想为什么不内置图的实现,现在想想真是蠢到家了……

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)。

路径压缩

image.png
此时按照上面的代码,我们也许可以得到一颗这样的树。但其实我们还可以进一步优化,因为我们实际上只关心某两个节点的根节点是不是同一个,以便来判断连通与否。所以我们可以尝试压缩路径到同一层,这样就可以将时间复杂度降为O(1)
image.png
可以通过修改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]
}

如果采用迭代写法,会压缩成如下的效果
image.png
如果采用递归写法,会压缩成如下:
image.png
借助路径压缩,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 中国大陆许可协议进行许可。

posted @   Appletree24  阅读(23)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起
  1. 1 愛錠 (Edit ver.) LiSA
愛錠 (Edit ver.) - LiSA
00:00 / 00:00
An audio error has occurred.

話してしまえば 思い出

隠してしまえば 幸せ

時間がすべてを奪ってく

What do you think?do you think?

愛してしまえば 地獄で

離れてしまえば 孤独だ

もう戻れない

ああ、想う 想うほど 絡まる愛錠

この手を繋ぐ鎖のように

ただ目の前の明日を信じられるのならば

それだけでいい

今そっと手を伸ばした

もたれてしまえば 2倍で

壊してしまえば それぞれ

時間このまま過ぎ去って

What do you think?do you think?

願ってしまえば 欲しくて

叶ってしまえば 足りない

知っている

もう戻れはしない

あの日には

今夜すべてを置き去りに

独りで逃げ出したって

きっと私はいつまでも

後悔に縛られたまま

ずっときょうを恨みながら

罪責と悪夢に魘されるのでしょう

解けないわ

鉄と鉄が擦れ合い

指と指の温もりも

今ここに貴方を感じられるカギ

ああ、想う 想うほど 絡まる愛錠

この手を繋ぐ鎖のように

ただ目の前の明日を信じられるのならば

それだけでいいよ 今は

ああ、巡る 巡るほど 絡まる愛錠

切れない絆 確かめながら

どんな見えない明日も貴方が傍にいるのなら

それだけでいい

強く確かな愛情