图及相关算法

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

基本实现

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

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)
posted @ 2023-10-19 23:00  Appletree24  阅读(17)  评论(0编辑  收藏  举报