算法编程题-图的最短路算法总结

Llama Factory

Llama Factory

模型微调
LLama-Factory

LLaMA Factory 是一个简单易用且高效的大型语言模型(Large Language Model)训练与微调平台。通过 LLaMA Factory,可以在无需编写任何代码的前提下,在本地完成上百种预训练模型的微调


在有向图或者无向图中求解最短路径算法是很多算法编程题的原型。本文将对这类算法进行总结,涉及深度优先搜索算法、Floyd算法、Dijkstra算法等常见的算法。本文以 LeetCode 743 网络延迟时间为例来说明这个问题。

原题描述

有n个网络节点,命名为从1到n,给定一个列表times,每一项有三个元素a, b, t, 表示从节点a到节点b有条路径,传输信号的时间是t,现在从节点k发送一个信号,信号在网络中传输,求网络中所有节点都收到这个信号的最短时间,如果存在无法收到信号的节点,则最短时间返回-1。

思路算法

深度优先搜索

思路简述

很容易想到的一种办法是深度优先搜索,搜索完所有可能的路径,然后在搜索的过程中,记录每一个节点收到信号的时间,统计最短时间即可。但是搜索所有路径需要耗费的时间是巨大的,这里需要考虑剪枝。可以想到,如果在搜索到一个节点的时候,发现当前时间大于等于历史上搜索到达该节点的时间,那么这个节点就没有必要继续搜索下去了。
对于无向图,该算法同样适用,搜索过程的代码也是一样的。对于有负权的图,也应该是适用的,且搜索过程的代码也不变。

代码

func networkDelayTime(times [][]int, n int, k int) int {
	// 先把图整理成邻接表的形式
	graph := make([][][]int, n)
	for _, time := range times {
		a := time[0] - 1
		b := time[1] - 1
		t := time[2]
		graph[a] = append(graph[a], []int{b, t})
	}

	costTime := make([]int, n)
	for i := 0; i < n; i++ {
		costTime[i] = -1 // 表征每一个节点收到信号的时间
	}
	costTime[k-1] = 0

	// 开始深度优先搜索
	var dfs func(curNode, curTime int)
	dfs = func(curNode, curTime int) {
		if costTime[curNode] == -1 || costTime[curNode] > curTime {
			costTime[curNode] = curTime
		}
		for _, adj := range graph[curNode] {
			if costTime[adj[0]] == -1 || costTime[adj[0]] > curTime + adj[1] {
				dfs(adj[0], curTime + adj[1])
			}
		}
	}
	dfs(k-1, 0)
	ans := -1
	for _, time := range costTime {
		if time == -1 {
			return -1
		}
		if ans == -1 || time > ans {
			ans = time
		}
	}
	return ans
}

复杂度分析

  • 时间复杂度:不好估计,应该不是 O ( m ) O(m) O(m) , 其中m为边数,因为在搜索的过程中,一个节点可能被访问多次,同样每一条边也会被访问多次。
  • 空间复杂度: O ( m ) O(m) O(m), 其中m为边数,因为是用邻接表来保存的边的信息,每条边只被保存一次。这里也说明,对于图的两种保存方式,邻接矩阵和邻接表,应该根据图的稀疏程度来决定选择那一种保存方式。
    在这里插入图片描述

Floyd算法

思路简述

Floyd算法一种多源求取最短路径的算法。一般时间复杂度为 O ( n 3 ) O(n^3) O(n3)。具体的过程就是通过遍历每一个节点,将每一个节点作为一个中转点i,然后遍历所有的入边点j和所有的出边点p,如果ji+ip < jp, 则可以更新点j到点p的距离。
对于无向图,本方法依然使用,只要将每一条无向边看做是两条相反方向的有向边即可。图中允许存在负权重,但不能出现一条环路上全是负权值,因为这样无法找到最短路径。

代码

func networkDelayTime(times [][]int, n int, k int) int {
	// 先把图整理成连接矩阵的形式
	matrix := make([][]int, n)
	for i := 0; i < n; i++ {
		matrix[i] = make([]int, n)
		for j := 0; j < n; j++ {
			matrix[i][j] = -1
		}
		matrix[i][i] = 0
	}
	for _, time := range times {
		a := time[0] - 1
		b := time[1] - 1
		t := time[2]
		matrix[a][b] = t
	}
	// Floyd算法过程
	for i := 0; i < n; i++ {   // 中转点
		for j := 0; j < n; j++ {   // 入度
			for p := 0; p < n; p++ {   // 出度
				if matrix[j][i] != -1 && matrix[i][p] != -1 {
					if matrix[j][p] == -1 || matrix[j][p] > matrix[j][i] + matrix[i][p] {
						matrix[j][p] = matrix[j][i] + matrix[i][p]
					}
				}
			}
		}
	}
	ans := -1
	for i := 0; i < n; i++ {
		if matrix[k - 1][i] == -1 {
			return -1
		}
		ans = max(ans, matrix[k - 1][i])
	}
	return ans
}

复杂度分析

  • 时间复杂度: O ( n 3 ) O(n^3) O(n3)
  • 空间复杂度: O ( n 2 ) O(n^2) O(n2)
    在这里插入图片描述

朴素dijkstra算法

思路简述

单源最短路径可以直接用dijkstra算法求出源点到图中每一个节点的距离。dijkstra(迪克斯特拉)算法的基本过程为:从源点开始不断寻找离源点距离最近且没有被使用过的中转点,找到之后,尝试用这个点作为目标点和源点之间的中转点,更新图中所有节点的到源点的最新最短距离。
该算法对于无向图同样适用,但是不适用带有负权值边的图。

代码

func networkDelayTime(times [][]int, n int, k int) int {
	// 先把图整理成连接矩阵的形式
	matrix := make([][]int, n)
	for i := 0; i < n; i++ {
		matrix[i] = make([]int, n)
		for j := 0; j < n; j++ {
			matrix[i][j] = -1
		}
		matrix[i][i] = 0
	}
	for _, time := range times {
		a := time[0] - 1
		b := time[1] - 1
		t := time[2]
		matrix[a][b] = t
	}
	dis := make([]int, n)
	for i := 0; i < n; i++ {
		dis[i] = -1
	}
	nativeDijkstra(matrix, dis, k - 1)   // 朴素迪克斯特拉算法
	ans := -1
	for i := 0; i < n; i++ {
		if dis[i] == -1 {
			return -1
		}
		ans = max(ans, dis[i])
	}
	return ans
}


// nativeDijkstra 朴素迪克斯特拉算法
func nativeDijkstra(matrix [][]int, dis []int, s int) {
	n := len(matrix)
	updated := make([]bool, n)   // 标识是否被更新过
	dis[s] = 0
	for i := 0; i < n; i++ {   // 迭代n次
		t := -1
		for j := 0; j < n; j++ {
			// 找到之前没用过的中转点
			if !updated[j] && dis[j] != -1 && (t == -1 || dis[j] < dis[t]) {
				t = j
			}
		}
		if t == -1 {
			break
		}
		// 根据找到的中转点t更新所有点到源点的距离
		updated[t] = true
		for j := 0; j < n; j++ {
			if matrix[t][j] != -1 && (dis[j] == -1 || dis[j] > dis[t] + matrix[t][j]) {
				dis[j] = dis[t] + matrix[t][j]
			}
		}
	}
}

复杂度分析

  • 时间复杂度: O ( n 2 ) O(n^2) O(n2)
  • 空间复杂度: O ( n 2 ) O(n^2) O(n2)
    在这里插入图片描述

堆优化dijkstra算法

思路简述

可以注意到,在朴素的dijkstra算法中,寻找未使用过的且离源点最近的中转点时用的是遍历的方法,这里就可以采用优先级队列进行优化。再结合邻接表,在稀疏图中的性能提升相当明显。

代码

func networkDelayTimeV3(times [][]int, n int, k int) int {
	// 先把图整理成邻接表的形式
	graph := make([][][]int, n)
	for _, time := range times {
		a := time[0] - 1
		b := time[1] - 1
		t := time[2]
		graph[a] = append(graph[a], []int{b, t})
	}
	dis := make([]int, n)
	for i := 0; i < n; i++ {
		dis[i] = -1
	}
	heapDijkstra(graph, dis, k - 1)
	ans := -1
	for i := 0; i < n; i++ {
		if dis[i] == -1 {
			return -1
		}
		ans = max(ans, dis[i])
	}
	return ans
}


func heapDijkstra(graph [][][]int, dis []int, s int) {
	h := Heap{}
	n := len(graph)
	updated := make([]bool, n)
	dis[s] = 0
	heap.Push(&h, &Path{s, 0})
	for len(h) > 0 {
		p := heap.Pop(&h).(*Path)
		if !updated[p.PointNO] {  // 没有被用过
			for _, item := range graph[p.PointNO] {
				b := item[0]
				t := item[1]
				if dis[b] == -1 || dis[b] > dis[p.PointNO] + t {
					dis[b] = dis[p.PointNO] + t
					heap.Push(&h, &Path{b, dis[b]})
				}
			}
			updated[p.PointNO] = true
		}
	}
}


type Path struct {
	PointNO int   // 经典编号
	Dis     int   // 该点到源点的距离
}


type Heap []*Path

func (h Heap) Len() int {
	return len(h)
}

func (h Heap) Less(i, j int) bool {
	return h[i].Dis < h[j].Dis
}

func (h Heap) Swap(i, j int) {
	h[i], h[j] = h[j], h[i]
}

func (h *Heap) Push(v interface{}) {
	*h = append(*h, v.(*Path))
}

func (h *Heap) Pop() interface{} {
	ret := (*h)[len(*h) - 1]
	*h = (*h)[: len(*h) - 1]
	return ret
}

复杂度分析

  • 时间复杂度: O ( m l o g n + m a x ( m , n ) ) O(mlogn + max(m, n)) O(mlogn+max(m,n)), 在迪克斯特拉算法中,注意到每条边对应的点时间对最多进入堆一次,所以迪克斯特拉算法的时间复杂度的一部分就是 m l o g n mlogn mlogn
  • 空间复杂度: O ( m a x ( m , n ) O(max(m, n) O(max(m,n)
    在这里插入图片描述

Bellman-Ford算法

思路简述

Bellman-Ford算法也是一种单源最短路径算法,其通过迭代n轮,在每一轮迭代中,再去遍历所有边a->b,并且尝试将a->b作为源点s->b路径上的最后一条边能不能减少b到源点的距离。这种算法的优点在于实现简单,且适用于无向图和负权边图,缺点就是计算复杂度相当高。

代码

func networkDelayTime(times [][]int, n int, k int) int {
	dis := make([]int, n)
	for i := 0; i < n; i++ {
		dis[i] = -1
	}
	dis[k - 1] = 0
	bf(times, dis, k - 1)  // 贝尔曼-福德算法
	ans := -1
	for i := 0; i < n; i++ {
		if dis[i] == -1 {
			return -1
		}
		ans = max(ans, dis[i])
	}
	return ans
}

func bf(edges [][]int, dis []int, s int) {
	n := len(edges)
	for i := 0; i < n; i++ {   // 迭代n轮
		prev := append([]int(nil), dis...)
		for _, edge := range edges {   // 遍历所有的边,作为s->b路径上的最后一条边
			a := edge[0] - 1
			b := edge[1] - 1
			c := edge[2]
			if prev[a] != -1 && (dis[b] == -1 || dis[b] > prev[a] + c) {
				dis[b] = prev[a] + c
			}
		}
	}
}

复杂度分析

  • 时间复杂度: O ( m ∗ n ) O(m * n) O(mn)
  • 空间复杂度: O ( n ) O(n) O(n)
    在这里插入图片描述

SPFA算法

思路简述

SPFA算法的全称是Shorted Path Faster Algorithmn,是贝尔曼-福德算法的队列优化版本。核心思路就是认为只有之前更新成功过的节点才能作为中转节点,所以用一个双端队列来保存更新成功过的节点,然后根据这个节点的出边来做节点距离的更新。

代码

func networkDelayTime(times [][]int, n int, k int) int {
	// 先把图整理成邻接表的形式
	graph := make([][][]int, n)
	for _, time := range times {
		a := time[0] - 1
		b := time[1] - 1
		t := time[2]
		graph[a] = append(graph[a], []int{b, t})
	}
	dis := make([]int, n)
	for i := 0; i < n; i++ {
		dis[i] = -1
	}
	spfa(graph, dis, k - 1)
	ans := -1
	for i := 0; i < n; i++ {
		if dis[i] == -1 {
			return -1
		}
		ans = max(ans, dis[i])
	}
	return ans
}


func spfa(graph [][][]int, dis []int, s int) {
	n := len(dis)
	deque := list.New()
	dis[s] = 0
	deque.PushBack(s)
	visited := make([]bool, n)   // 记录对应点是否在队列中
	visited[s] = true   
	for deque.Len() != 0 {
		p := deque.Remove(deque.Front()).(int)
		visited[s] = false
		for _, edge := range graph[p] {
			a := edge[0]
			c := edge[1]
			if dis[a] == -1 || dis[a] > dis[p] + c {
				dis[a] = dis[p] + c
				if visited[a] {
					continue
				}
				deque.PushBack(a)   // 更新成功的点才加入队列中作为中转点
				visited[a] = false
			}
		}
	}
}

复杂度分析

  • 时间复杂度: O ( m n ) O(mn) O(mn), 这里有意思的是,SPFA算法的名称是由段凡丁提出来的,并且在论文中还断言该算法的时间复杂度为 O ( k m ) O(km) O(km),其中k为很小的常数,但是后面被人证明严格复杂度还是 O ( m ∗ n ) O(m * n) O(mn)。可见,有些算法的时间复杂度并不好分析。
  • 空间复杂度: O ( m ) O(m) O(m)
    在这里插入图片描述

参考

您可能感兴趣的与本文相关的镜像

Llama Factory

Llama Factory

模型微调
LLama-Factory

LLaMA Factory 是一个简单易用且高效的大型语言模型(Large Language Model)训练与微调平台。通过 LLaMA Factory,可以在无需编写任何代码的前提下,在本地完成上百种预训练模型的微调

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值