理论基础
二叉树和回溯算法章节中已经讲过深搜和广搜,二叉树的遍历就是深搜和广搜在二叉树结构上的应用, 而回溯算法本身就是深搜,只不过利用其回溯的过程。在图论中,深搜和广搜就是在图上的遍历,图的存储方式一般是 邻接表和邻接矩阵。
dfs 与 bfs 区别
深度优先搜索(dfs)和广度优先搜索(bfs)区别:
- dfs是可一个方向去搜,不到黄河不回头,直到遇到绝境了,搜不下去了,再换方向(换方向的过程就涉及到了回溯)。
- bfs是先把本节点所连接的所有节点遍历一遍,走到下一个节点的时候,再把连接节点的所有节点遍历一遍,搜索方向更像是广度,四面八方的搜索过程。
dfs模板
正是因为dfs搜索可一个方向,并需要回溯,所以用递归的方式来实现是最方便的。回溯操作就在递归函数的下面,递归和回溯是相辅相成的。
1
2
3
4
5
6
7
8
9
10
11
12
|
void dfs(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本节点所连接的其他节点) {
处理节点;
dfs(图,选择的节点); // 递归
回溯,撤销处理结果
}
}
|
深搜三部曲
- 确认递归函数,参数:在写递归函数的过程中,发现需要什么参数,再去补充;一般深搜需要 二维数组数组结构保存所有路径,需要一维数组保存单一路径
- 确认终止条件:终止条件不仅是结束本层递归,同时也是收获结果的时候;很多dfs写法,没有写终止条件,其实终止条件写在了下面dfs递归的逻辑里了,也就是不符合条件,直接不会向下递归
- 处理目前搜索节点出发的路径:一般这里就是一个for循环的操作,去遍历 目前搜索节点 所能到的所有节点
广搜的使用场景
广搜的搜索方式就适合于解决两个点之间的最短路径问题
因为广搜是从起点出发,以起始点为中心一圈一圈进行搜索,一旦遇到终点,记录之前走过的节点就是一条最短路。
当然,也有一些问题是广搜 和 深搜都可以解决的,例如岛屿问题,这类问题的特征就是不涉及具体的遍历方式,只要能把相邻且相同属性的节点标记上就行。
代码框架
很多网上的资料都是直接说BFS用队列来实现,其实仅仅需要一个容器能保存遍历过的元素就可以,用队列,还是用栈,甚至用数组,都是可以的。
用队列的话,就是保证方向统一,例如统一顺时针或者逆时针,因为队列是先进先出,所以加入元素和弹出元素的顺序是没有改变的
如果用栈的话,就是第一圈是方向1,第二圈是反方向,第三圈又回到方向1。,因为栈是先进后出,加入元素和弹出元素的顺序改变了
那么广搜需要注意搜索的顺序吗? 不需要!所以用队列,还是用栈都是可以的,但大家都习惯用队列了,所以下面也用队列来给出广搜代码模板
1
2
3
4
5
6
7
8
9
10
11
|
func bfs(参数) {
// 定义队列
// 起始节点入队
for len(queue) != 0 {
// 取队首元素
for 选择:本节点所连接的其他节点 {
// 处理节点
// 新节点入队
}
}
}
|
并查集
并查集用来解决连通性问题:判断两个元素是否在同一个集合里
- 将两个元素添加到一个集合中
- 判断两个元素是否在一个集合
原理
将A,B两个元素放在同一集合(连通),只需要用一个一维数组来表示,即father[A] = B,father[B] = B,这样就表示AB连通了;根据A可以找到B,根据B可以找到B,AB最终都会找到B,即AB同根,表示两个元素在一个集合里
同理,将A,B,C三个元素放在同一个集合(连通),即:father[A] = B,father[B] = C,father[C] = C,这样就表示ABC连通了;根据A可以找到B,根据B可以找到C,根据C可以找到C,ABC最终都会找到C,即ABC同根,表示三个元素在一个集合里,寻根过程:
1
2
3
4
|
func find(u int) int {
if father[u] == u return u // 如果根就是自己,直接返回
else return find(father[u]) // 如果根不是自己,就根据数组下标一层一层向下找根
}
|
father数组初始化的时候要 father[i] = i,默认自己指向自己
判断两个元素是否在同一个集合里,若通过find函数找到两个元素属于同一个根的话,那么这两个元素就是同一个集合,代码如下:
1
2
3
4
5
|
func isSame(a int, b int) bool {
a = find(a)
b = find(b)
return a == b
}
|
路径压缩
如果让所有节点都直接挂载根下,这样在寻根的时候就只需要一步,想要达到这样的效果,就需要 路径压缩,将非根节点的所有节点直接指向根节点。
实现:在递归的过程中,让 father[u] 接住递归函数 find(father[u]) 的返回结果
因为 find 函数向上寻找根节点时,father[u] 表示 u 的父节点,那么让 father[u] 直接获取find函数返回的根节点,这样就让根节点直接作为了节点 u 的父节点
1
2
3
4
5
|
// 并查集里寻根的过程
func find(u int) int {
if father[u] == u return u
else return father[u] = find(father[u]) // 路径压缩
}
|
并查集模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
var father []int
func validPath(n int, edges [][]int, source int, destination int) bool {
father = make([]int, n)
initial(n)
for i := range edges {
join(edges[i][0], edges[i][1])
}
return isSame(source, destination)
}
// 并查集初始化
func initial(n int) {
for i := 0; i < n; i++ {
father[i] = i
}
}
// 并查集里寻根
func find(u int) int {
if father[u] == u {
return u
}
father[u] = find(father[u])
return father[u]
}
// 判断a和b是否找到同一个根
func isSame(a int, b int) bool {
a = find(a)
b = find(b)
return a == b
}
// 边a->b加入并差集
func join(a int, b int) {
a = find(a)
b = find(b)
if a == b {
return // 同根说明已经在一个集合里
}
father[a] = b
}
|
注意:join函数里要有find函数进行寻根的过程,这样就保证元素在有向图里是强连通的
模拟过程
1、join(1, 8)
2、join(3, 8)
3指向1不指向8是因为在join
函数里分别对3和8寻根之后,3的根为3,8的根为1,再进行关联就为3指向1
3、join(1, 7)
4、join(8, 5)
这里8的根是3,那么3应该指向5,因为是在join
函数里分别对a和b进行了寻根之后再进行的关联
但是这里1不指向8了,3直接指向了8,这是因为路经压缩,这减少了下次查询的路径长度
5、join(2, 9)
6、join(6, 9)
因为9的根为 2,所以用6指向2
13578是同一个集合,269是同一个集合
复杂度分析
路径压缩版并查集
- 空间复杂度: O(n) ,申请一个father数组
- 路径压缩后的并查集时间复杂度在O(logn)与O(1)之间,且随着查询或者合并操作的增加,时间复杂度会越来越趋于O(1)
在第一次查询的时候,相当于是n叉树上从叶子节点到根节点的查询过程,时间复杂度是logn,但路径压缩后,后面的查询操作都是O(1),而 join 函数和 isSame函数里涉及的查询操作也是一样的过程
所有可能的路径
797. 所有可能的路径 - 力扣(LeetCode)
给定一个二维数组含有n个元素,表示有向无环图,每个元素表示该元素下标节点能到达的节点(一条有向边),返回节点0到节点n-1的所有路径
- 确认递归函数,参数:一个图是用来遍历,一个是目前遍历的节点;单一路径和路径集合可以放在全局变量
- 确认终止条件:当目前遍历的节点为最后一个节点的时候,就找到了一条从出发点到终止点的路径
- 处理目前搜索节点出发的路径:得到当前遍历节点的指向的下一个节点,将选中的节点加入到单一路径中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
var path []int // 节点0到节点n-1的所有路径
var res [][]int // 节点0到节点n-1的一条路径
func allPathsSourceTarget(graph [][]int) [][]int {
path = make([]int, 0) // 力扣中全局变量需要重新初始化,否则会携带之前测试case的答案
res = make([][]int, 0)
path = append(path, 0) // 从节点0出发
dfs(graph, 0)
return res
}
func dfs(graph [][]int, cur int) {
// 终止条件:当前遍历节点是节点n-1
if cur == len(graph)-1 {
temp := make([]int, len(path))
copy(temp, path)
res = append(res, temp) // 将找到的一条路径加入结果集
return
}
for i := 0; i < len(graph[cur]); i++ { // 遍历当前节点连接的所有节点
path = append(path, graph[cur][i]) // 将遍历到的节点加入路径
dfs(graph, graph[cur][i]) // 进入下一层递归
path = path[:len(path)-1] // 回溯 在路径中撤销本节点
}
}
|
岛屿数量
200. 岛屿数量 - 力扣(LeetCode)
给定一个二维数组,表示一块区域;元素由0和1组成,1代表陆地,0代表海洋;若陆地被海洋包裹,则说明是岛屿;假设该区域四条边外是海洋;被海洋包裹的多块水平或垂直相连陆地看作一个岛屿;返回该区域中岛屿的数量
本题思路:遍历一遍所有节点;每遇到一个陆地节点且没有被标记过,计数器就加一,然后把该陆地节点所能遍历到的陆地都标记上;在遇到标记过的陆地节点和海洋节点的时候直接跳过;这样计数器就是最终岛屿的数量
把陆地节点所能遍历到的陆地都标记上可以使用 DFS,BFS或者并查集
对于本题来说,DFS和BFS都是对一个节点进行四周遍历,区别是DFS用递归栈来保存遍历过的状态,BFS用队列来保存遍历过的状态
DFS(推荐下面第二种优化后的写法)
- 确认递归函数,参数:一个是图用来遍历,一个是目前遍历的节点的横纵坐标(坐标用来访问四周节点和设置标记节点变量);保存节点标记的变量可以放在全局变量
- 确认终止条件:索引越界或遇到海洋
- 处理目前搜索节点出发的路径:得到当前遍历节点的指向的下一个节点
注意:本题是网格,dfs时要四个方向都搜
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
var visited [][]bool
func numIslands(grid [][]byte) int {
res := 0
visited = make([][]bool, len(grid))
for i := range visited {
visited[i] = make([]bool, len(grid[i]))
}
for i := range grid {
for j := range grid[i] {
if grid[i][j] == '1' && !visited[i][j] { // 遇到陆地节点且没有被标记过
res += 1
dfs(grid, i, j)
}
}
}
return res
}
func dfs(grid [][]byte, x int, y int) {
if grid[x][y] == '0' || visited[x][y] { // 遇到海洋节点或标记过的节点
return
}
visited[x][y] = true // 标记节点
if x != len(grid)-1 { // x没到最后一层
dfs(grid, x+1, y) // 递归 向下探索
}
if y != len(grid[x])-1 { // y没到最后一列
dfs(grid, x, y+1) // 递归 向右探索
}
if x != 0 { // x不是第一层
dfs(grid, x-1, y) // 递归 向上探索
}
if y != 0 { // y不是第一列
dfs(grid, x, y-1) // 递归 向左探索
}
}
|
DFS优化:将遍历过的陆地直接置为0
注意:在终止条件中,先判断索引,再判断是否遇到海洋,防止越界;同理由于判断列数要用到行数,所有也要放在行数判断之后
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
func numIslands(grid [][]byte) int {
res := 0
for i := range grid {
for j := range grid[i] {
if grid[i][j] == '1' { // 遇到陆地节点且没有被标记过
res += 1
dfs(grid, i, j)
}
}
}
return res
}
func dfs(grid [][]byte, x int, y int) {
// 下标越界或遇到海洋节点
if x < 0 || y < 0 || x == len(grid) || y == len(grid[x]) || grid[x][y] == '0' {
return
}
grid[x][y] = '0' // 标记节点
dfs(grid, x+1, y) // 递归 向下探索
dfs(grid, x, y+1) // 递归 向右探索
dfs(grid, x-1, y) // 递归 向上探索
dfs(grid, x, y-1) // 递归 向左探索
}
|
BFS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
func numIslands(grid [][]byte) int {
res := 0
for i := range grid {
for j := range grid[i] {
if grid[i][j] == '1' { // 遇到陆地节点且没有被标记过
res += 1
bfs(grid, i, j)
}
}
}
return res
}
func bfs(grid [][]byte, x int, y int) {
queue := make([][]int, 0) // 定义队列
queue = append(queue, []int{x, y}) // 起始节点入队
move := [4][2]int{{1, 0}, {0, 1}, {0, -1}, {-1, 0}} // 下右左上
for len(queue) != 0 { // 遍历队列
node := queue[0] // 取队首元素
queue = queue[1:] // 更新队列
for i := 0; i < 4; i++ { // 遍历当前节点四周
x = node[0] + move[i][0]
y = node[1] + move[i][1]
// 下标越界或遇到海洋节点
if x < 0 || y < 0 || x == len(grid) || y == len(grid[x]) || grid[x][y] == '0' {
continue
}
grid[x][y] = '0' // 标记节点
queue = append(queue, []int{x, y}) // 入队
}
}
}
|
岛屿的最大面积
695. 岛屿的最大面积 - 力扣(LeetCode)
给定一个二维数组,表示一块区域;元素由0和1组成,1代表陆地,0代表海洋;若陆地被海洋包裹,则说明是岛屿;假设该区域四条边外是海洋;被海洋包裹的多块水平或垂直相连陆地看作一个岛屿;返回该区域中岛屿的最大面积
本题思路:遍历一遍所有节点;每遇到一个陆地节点且没有被标记过,说明是新岛屿,然后把该陆地节点所能遍历到的陆地都标记上;在遇到标记过的陆地节点和海洋节点的时候直接跳过;在标记时统计该岛屿的面积,遍历该岛屿结束后,与已保存的最大岛屿面积作比较取最大值,
把陆地节点所能遍历到的陆地都标记上可以使用 DFS,BFS或者并查集
对于本题来说,DFS和BFS都是对一个节点进行四周遍历,区别是DFS用递归栈来保存遍历过的状态,BFS用队列来保存遍历过的状态
DFS
- 递归参数:一个是图用来遍历,一个是目前遍历的节点的横纵坐标(坐标用来访问四周节点),记录每个岛屿面积的变量可设为全局
- 确认终止条件:索引越界或遇到海洋
- 处理目前搜索节点出发的路径:得到当前遍历节点的指向的下一个节点
注意:每次遍历完岛屿并记录结束后,要清空全局变量,使其在下次岛屿遍历中记录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
var area int
func maxAreaOfIsland(grid [][]int) int {
res := 0
for i := range grid {
for j := range grid[i] {
if grid[i][j] == 1 {
dfs(grid, i, j)
res = max(res, area)
area = 0 // 重置面积
}
}
}
return res
}
func dfs(grid [][]int, x int, y int) {
if x < 0 || y < 0 || x == len(grid) || y == len(grid[x]) || grid[x][y] == 0 {
return
}
grid[x][y] = 0 // 标记
area += 1 // 统计面积
dfs(grid, x+1, y) // 下
dfs(grid, x, y+1) // 右
dfs(grid, x-1, y) // 上
dfs(grid, x, y-1) // 左
}
|
飞地的数量
1020. 飞地的数量 - 力扣(LeetCode)
给定一个二维数组,表示一块区域;元素由0和1组成,1代表陆地,0代表海洋;若陆地被海洋包裹,则说明是岛屿;假设该区域四条边外是海洋;被海洋包裹的多块水平或垂直相连陆地看作一个岛屿;返回该区域中所有不能到达矩阵边界的岛屿的面积总和
本题思路一:遍历一遍所有节点;每遇到一个陆地节点且没有被标记过,说明是新岛屿,然后把该陆地节点所能遍历到的陆地都标记上;在遇到标记过的陆地节点和海洋节点的时候直接跳过;在标记时统计该岛屿的面积,同时检查该节点是否到达边界,一旦该岛屿可以到达边界则标记该岛屿;遍历该岛屿结束后,若该岛屿不能到达边界,则将该岛屿面积记入结果集
思路二:本题要求找到不靠边的陆地面积,那么我们只要从周边找到陆地然后 通过 dfs或者bfs 将周边靠陆地且相邻的陆地都变成海洋,然后再去重新遍历地图的时候,统计此时还剩下的陆地就可以了。
把陆地节点所能遍历到的陆地都标记上可以使用 DFS,BFS或者并查集
对于本题来说,DFS和BFS都是对一个节点进行四周遍历,区别是DFS用递归栈来保存遍历过的状态,BFS用队列来保存遍历过的状态
思路一DFS
- 递归参数:一个是图用来遍历,一个是目前遍历的节点的横纵坐标(坐标用来访问四周节点),记录每个岛屿面积的变量和标记该岛屿能否到达矩阵边界的变量可设为全局
- 确认终止条件:索引越界或遇到海洋
- 处理目前搜索节点出发的路径:得到当前遍历节点的指向的下一个节点
注意:每次遍历完岛屿并记录结束后,要清空全局变量,使其在下次岛屿遍历中记录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
var area int
var arrived bool
func numEnclaves(grid [][]int) int {
res := 0
for i := range grid {
for j := range grid[i] {
if grid[i][j] == 1 {
dfs(grid, i, j)
if !arrived {
res += area // 记录不能到达边界的面积
}
area = 0 // 重置
arrived = false // 重置
}
}
}
return res
}
func dfs(grid [][]int, x int, y int) {
if x < 0 || y < 0 || x == len(grid) || y == len(grid[x]) {
arrived = true // 上一块陆地是边界
return
}
if grid[x][y] == 0 {
return
}
grid[x][y] = 0 // 标记
area += 1 // 统计面积
dfs(grid, x+1, y) // 下
dfs(grid, x, y+1) // 右
dfs(grid, x-1, y) // 上
dfs(grid, x, y-1) // 左
}
|
思路二DFS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
func numEnclaves(grid [][]int) int {
m, n := len(grid), len(grid[0])
for i := 0; i < m; i++ {
if grid[i][0] == 1 { // 第一列
dfs(grid, i, 0)
}
if grid[i][n-1] == 1 { // 最后一列
dfs(grid, i, n-1)
}
}
for i := 0; i < n; i++ {
if grid[0][i] == 1 { // 第一排
dfs(grid, 0, i)
}
if grid[m-1][i] == 1 { // 最后一排
dfs(grid, m-1, i)
}
}
res := 0
for i := range grid {
for j := range grid[i] {
if grid[i][j] == 1 {
res++
}
}
}
return res
}
func dfs(grid [][]int, x, y int) {
if x < 0 || y < 0 || x == len(grid) || y == len(grid[0]) || grid[x][y] == 0 {
return
}
grid[x][y] = 0
dfs(grid, x+1, y)
dfs(grid, x, y+1)
dfs(grid, x-1, y)
dfs(grid, x, y-1)
}
|
被围绕的区域
130. 被围绕的区域 - 力扣(LeetCode)
给定一个二维数组,表示一块区域;元素由x和o组成,o代表陆地,x代表海洋;若陆地被海洋包裹,则说明是岛屿;假设该区域四条边外是海洋;被海洋包裹的多块水平或垂直相连陆地看作一个岛屿;将该区域中所有不能到达矩阵边界的岛屿的都置为海洋
遍历地图周边一圈,将与周边相邻的’O’都改成’A',然后再遍历一遍地图,遇到的 ‘O’ 一定都是地图中间的’O',全部改成’X',遇到’A’全部改回’O'
把陆地节点所能遍历到的陆地都标记上可以使用 DFS,BFS或者并查集
对于本题来说,DFS和BFS都是对一个节点进行四周遍历,区别是DFS用递归栈来保存遍历过的状态,BFS用队列来保存遍历过的状态
- 递归参数:一个是图用来遍历,一个是目前遍历的节点的横纵坐标(坐标用来访问四周节点),记录标记过的陆地的变量可设为全局
- 确认终止条件:索引越界或遇到海洋
- 处理目前搜索节点出发的路径:得到当前遍历节点的指向的下一个节点
注意:
- 标记可到边界的陆地可直接将该陆地置为其他值,后面遍历区域时再将其恢复;用此方法时注意最后遍历时要用
if elseif
;
- DFS中遇到标记过的变量也要返回,不然会陷入死循环,一直标记标记过的变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
func solve(board [][]byte) {
for row := 0; row < len(board); row++ {
if board[row][0] == 'O' { // 第一列
dfs(board, row, 0)
}
if board[row][len(board[0])-1] == 'O' { // 最后一列
dfs(board, row, len(board[0])-1)
}
}
for col := 1; col < len(board[0])-1; col++ {
if board[0][col] == 'O' { // 第一排去首尾剩下中间的
dfs(board, 0, col)
}
if board[len(board)-1][col] == 'O' { // 最后一排去首尾剩下中间的
dfs(board, len(board)-1, col)
}
}
for i := range board {
for j := range board[i] {
if board[i][j] == 'A' {
board[i][j] = 'O' // 恢复可以到达边界的陆地
} else if board[i][j] == 'O' {
board[i][j] = 'X' // 不能到达边界的陆地
}
}
}
}
func dfs(board [][]byte, x int, y int) {
if x < 0 || y < 0 || x == len(board) || y == len(board[x]) || board[x][y] == 'X' || board[x][y] == 'A' {
return
}
board[x][y] = 'A' // 标记该陆地
dfs(board, x+1, y) // 下
dfs(board, x, y+1) // 右
dfs(board, x-1, y) // 上
dfs(board, x, y-1) // 左
}
|
太平洋大西洋水流问题
417. 太平洋大西洋水流问题 - 力扣(LeetCode)
给定一个二维数组,表示一块岛屿,该岛屿左方和上方被太平洋包围,右方和下方被大西洋包围;每个元素表示每块陆地的高度,水可以从高流向相等或小于的陆地;水到岛屿边缘就可以直接入海;返回可以同时流入大西洋和太平洋的陆地坐标
反过来想,从太平洋边上的节点 逆流而上,将遍历过的节点都标记上;从大西洋的边上节点 逆流而上,将遍历过的节点也标记上; 最后遍历岛屿,两方都标记过的节点就是既可以流到太平洋也可以流到大西洋的陆地
用一个三维数组来标记节点,前两个维度标记节点横纵坐标,第三个维度长度为2标记节点能否到达太平洋好大西洋,可作为全局变量;dfs时第三维作为入参标记当前从哪个洋逆流而上
dfs遍历时,需要当前节点和下一个节点的状态比较,可以先比较再决定是否递归遍历,这就需要四个偏移量来计算向四周移动后的坐标,遍历四个偏移量来表示向四周遍历
DFS
注意:
- 先遍历第一列太平洋和最后一列大西洋后,再次遍历排时仍要从当前排第一个元素出发,因为副对角俩元素可以同时到达两个洋
- 由于是在循环中遍历四个方向所以是跳过本次循环,不是
return
返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
var visited [][][]bool
var ds = [4][2]int{{1, 0}, {0, 1}, {-1, 0}, {0, -1}} // 四周偏移量
func pacificAtlantic(heights [][]int) [][]int {
res := make([][]int, 0)
visited = make([][][]bool, len(heights))
for i := range visited {
visited[i] = make([][]bool, len(heights[i]))
}
for i := range visited {
for j := range visited[i] {
visited[i][j] = make([]bool, 2)
}
}
for row := 0; row < len(heights); row++ {
dfs(heights, row, 0, 0) // 标记第一列太平洋
dfs(heights, row, len(heights[0])-1, 1) // 标记最后一列大西洋
}
for col := 0; col < len(heights[0]); col++ {
dfs(heights, 0, col, 0) // 标记第一排太平洋
dfs(heights, len(heights)-1, col, 1) // 标记最后一排大西洋
}
for i := range heights {
for j := range heights[i] {
if visited[i][j][0] && visited[i][j][1] {
res = append(res, []int{i, j})
}
}
}
return res
}
func dfs(heights [][]int, x int, y int, sign int) {
visited[x][y][sign] = true
for _, move := range ds {
nextX := x + move[0]
nextY := y + move[1]
if nextX < 0 || nextY < 0 || nextX == len(heights) || nextY == len(heights[nextX]) {
continue // 索引越界
}
if heights[x][y] > heights[nextX][nextY] || visited[nextX][nextY][sign] {
continue // 高度不合适或标记过
}
dfs(heights, nextX, nextY, sign)
}
}
|
最大人工岛
827. 最大人工岛 - 力扣(LeetCode)
给定一个二维数组,表示一块区域;元素由0和1组成,1代表陆地,0代表海洋;若陆地被海洋包裹,则说明是岛屿;假设该区域四条边外是海洋;被海洋包裹的多块水平或垂直相连陆地看作一个岛屿;现在可以改变一个海洋使其变为陆地,返回改变后的最大岛屿面积
思路:
- 遍历地图一次,得出各个岛屿的面积,并做编号记录,可以使用map记录,key为岛屿编号,value为岛屿面积
- 再遍历地图一次,遇到0就统计其四周相邻岛屿的面积(用到上一步记录好的岛屿面积),将面积加在一起后,再加一得到一个总面积,遍历所有 0 之后,就可以得出一个最大人工岛
优化:第一次遍历时可以直接修改原数组的元素值作为标记,在遍历完一个岛屿后统计其面积,并将标记值加一
注意:
- 第二次遍历统计0四周岛屿面积时需要再用一个map记录该岛屿是否被统计过,因为有的岛屿可能占用0四周的多个陆地,若不记录,则同一岛屿可能被同一0统计多次
- 最后返回结果需要判断是否改变过0,若没有,说明该区域全是陆地,直接返回区域中陆地数
- for循环遍历四周时需要用新变量来表示四周坐标,不能直接修改原坐标,因为递归返回上一层时需要用到本层的坐标
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
var ds = [4][2]int{{1, 0}, {0, 1}, {-1, 0}, {0, -1}}
var mark int
var area int
func largestIsland(grid [][]int) int {
res := 0
mark = 2
getSize := make(map[int]int)
// 统计各个岛屿面积
for i := range grid {
for j := range grid[i] {
if grid[i][j] == 1 {
dfs(grid, i, j) // 遍历该岛屿
getSize[mark] = area // 保存岛屿面积
area = 0 // 置零
mark++ // 标记值更新
}
}
}
// 遍历0得到最大面积
for i := range grid {
for j := range grid[i] {
if grid[i][j] == 0 {
// 得到四周岛屿总面积
total := 0
accounted := make(map[int]bool)
for _, move := range ds {
x := i + move[0]
y := j + move[1]
if x < 0 || y < 0 || x == len(grid) || y == len(grid[x]) || grid[x][y] == 0 {
continue // 索引越界或遇到海洋
} else if accounted[grid[x][y]] {
continue // 该岛屿被统计过
} else {
accounted[grid[x][y]] = true // 标记该岛屿
total += getSize[grid[x][y]] // 统计该岛屿面积
}
}
res = max(res, 1+total) // 更新最大面积
}
}
}
if res == 0 {
return len(grid) * len(grid)
}
return res
}
func dfs(grid [][]int, x int, y int) {
if x < 0 || y < 0 || x == len(grid) || y == len(grid[x]) || grid[x][y] != 1 {
return // 索引越界或遇到海洋或标记过
}
grid[x][y] = mark // 标记陆地
area++ // 岛屿面积更新
for _, move := range ds {
nextX := x + move[0]
nextY := y + move[1]
dfs(grid, nextX, nextY)
}
}
|
单词接龙
127. 单词接龙 - 力扣(LeetCode)
给定两个单词 beginWord
、 endWord
和一个字典 wordList
;每次从字典中选一个单词,该单词和上一个单词只有一个字母不同,返回 从 beginWord
到 endWord
的 最短转换序列 中的 单词数目 ;序列中第一个单词是 beginWord,序列中最后一个单词是 endWord;beginWord不在字典中,endWord在字典中;如果不存在这样的转换序列,返回 0
以示例1为例,从这个图中可以看出 hit 到 cog 的路线,不止一条,有三条,一条是最短的长度为5,两条长度为6
本题要解决两个问题:
- 怎样表示图中的链接:若俩单词只有一个字符不同,则有链接
- 起点和终点的最短路径长度:无向图求最短路,广搜最为合适;广搜只要搜到了终点,则一定是最短的路径,因为广搜就是以起点中心向四周扩散的搜索;本题如果用深搜会比较麻烦,要在到达终点的不同路径中选则一条最短路, 而广搜只要达到终点,一定是最短路
优化:字典可以转成map结构,查找更快一些
注意:
- 无向图需要标记节点是否走过,否则会死循环;本题即标记该单词是否用过
- 比较俩单词是否只差一个单词时,对单词中每个字母用26个字母逐个替换并检索字典中是否存在,若存在则路径长度加一,若为最终单词则返回,否则更新路径长度后入队
- go中字符串和字符互相转换:
[]rune(s)
、string([]rune)
,只处理英文用byte
,遍历26字母时go中用的是rune
- 遍历下一个字符前需要将上一个字符的状态复原
- 对每个单词记录其路径长度,同时作为是否访问过的标志,由于是BFS,所以找到一个链接后,还会继续找本单词的其他链接,所以不能用一个变量来记录路径长度,应该单独记录每个单词的路径长度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
func ladderLength(beginWord string, endWord string, wordList []string) int {
// 转换字典为map型
wordMap := make(map[string]int)
for i, v := range wordList {
wordMap[v] = i
}
// BFS
length := make(map[string]int)
length[beginWord] = 1
queue := make([]string, 0) // 定义队列
queue = append(queue, beginWord) // 入队
for len(queue) != 0 {
word := queue[0] // 取队头元素
path := length[word] // 获取该单词的路径长度
queue = queue[1:] // 队首出队
for i := range word {
wordSlice := []rune(word) // 将单词转为字符数组
for k := 'a'; k <= 'z'; k++ {
wordSlice[i] = k // 替换字符
if _, ok := wordMap[string(wordSlice)]; ok { // 在字典中
if _, ok := length[string(wordSlice)]; !ok { // 未被使用过
length[string(wordSlice)] = path + 1 // 记录单词对应的路径长度
if string(wordSlice) == endWord {
return length[string(wordSlice)] // 到最终单词
}
queue = append(queue, string(wordSlice)) // 入队
}
}
}
}
}
return 0
}
|
钥匙和房间
841. 钥匙和房间 - 力扣(LeetCode)
给定一个二维数组,第一维表示房间,第二维表示房间中的钥匙;0号房间一定是打开的,进入房间必须要钥匙,可以通过进入房间获得其他房间的钥匙;若能够进入所有房间则返回true
,否则返回false
用一个数组记录所有房间的可否进入状态,同时也作为是否入过队的标志;BFS遍历给定二维数组,获得新钥匙就更新房间的可进入状态;最后遍历所有房间的可进入状态,若有房间无法进入则返回false
,否则返回true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
func canVisitAllRooms(rooms [][]int) bool {
accessible := make([]bool, len(rooms))
accessible[0] = true // 0号房间可进入
queue := make([][]int, 0) // 定义队列
queue = append(queue, rooms[0]) // 0号房入队
for len(queue) != 0 {
room := queue[0] // 取队首
queue = queue[1:] // 出队
for _, key := range room { // 遍历本房间中钥匙
if !accessible[key] { // 此房间未入过队
accessible[key] = true // 更新可进入状态
queue = append(queue, rooms[key]) // 新房入队
}
}
}
for i := range accessible {
if !accessible[i] {
return false // 有房间无法进入
}
}
return true
}
|
BFS优化:队列用一维数组保存房间下标
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
func canVisitAllRooms(rooms [][]int) bool {
accessible := make([]bool, len(rooms))
accessible[0] = true // 0号房间可进入
queue := make([]int, 0) // 定义队列
queue = append(queue, 0) // 0号房入队
for len(queue) != 0 {
idx := queue[0] // 取队首
queue = queue[1:] // 出队
for _, key := range rooms[idx] { // 遍历本房间中钥匙
if !accessible[key] { // 此房间未入过队
accessible[key] = true // 更新可进入状态
queue = append(queue, key) // 新房入队
}
}
}
for i := range accessible {
if !accessible[i] {
return false // 有房间无法进入
}
}
return true
}
|
岛屿的周长
463. 岛屿的周长 - 力扣(LeetCode)
给定一个二维数组,表示一块区域;元素由0和1组成,1代表陆地,0代表海洋;若陆地被海洋包裹,则说明是岛屿;假设该区域四条边外是海洋;被海洋包裹的多块水平或垂直相连陆地看作一个岛屿;该区域中恰好有一个岛屿,求该岛屿的周长
遍历区域,每遇到一个陆地,岛屿周长就加四;查看该陆地的四周,每有一个岛屿,则周长减一
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
func islandPerimeter(grid [][]int) int {
res := 0
for i := range grid {
for j := range grid[i] {
if grid[i][j] == 1 { // 遇到陆地
res += 4
if j != len(grid[0])-1 && grid[i][j+1] == 1 {res -= 1} // 不是最后一列且右有陆地
if i != len(grid)-1 && grid[i+1][j] == 1 {res -= 1} // 不是最后一排且下有陆地
if i != 0 && grid[i-1][j] == 1 {res -= 1} // 不是第一排且上有陆地
if j != 0 && grid[i][j-1] == 1 {res -= 1} // 不是第一列且左有陆地
}
}
}
return res
}
|
寻找图中是否存在路径
1971. 寻找图中是否存在路径 - 力扣(LeetCode)
给定整数 n
表示节点总数、二维数组 edges
表示边、source
和 destination
表示起点和终点,如果从 source
到 destination
存在 有效路径 ,则返回 true
,否则返回 false
本题判断起点和终点是否连通,也就是判断是否在一个集合里,可以用并查集来判断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
var father []int
func validPath(n int, edges [][]int, source int, destination int) bool {
father = make([]int, n)
initial(n)
for i := range edges {
join(edges[i][0], edges[i][1])
}
return isSame(source, destination)
}
// 并查集初始化
func initial(n int) {
for i := 0; i < n; i++ {
father[i] = i
}
}
// 并查集里寻根
func find(u int) int {
if father[u] == u {
return u
}
father[u] = find(father[u])
return father[u]
}
// 判断a和b是否找到同一个根
func isSame(a int, b int) bool {
a = find(a)
b = find(b)
return a == b
}
// 边a->b加入并差集
func join(a int, b int) {
a = find(a)
b = find(b)
if a == b {
return // 同根说明已经在一个集合里
}
father[a] = b
}
|
冗余连接
684. 冗余连接 - 力扣(LeetCode)
给定一个二维数组表示图中的边;要求去掉一条边,使图变为树(只有一个根节点);返回该边,若有多个答案,返回二维数组中的最后一条边
遍历给定边,在将边加入并查集的过程中,若发现边的俩节点同根,说明该边加入后一定会形成环,返回该边即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
var father []int
var n int
func findRedundantConnection(edges [][]int) []int {
n = len(edges)
father = make([]int, n+1)
initial()
for i := 0; i < n; i++ {
if isSame(edges[i][0], edges[i][1]) {
return edges[i]
}
join(edges[i][0], edges[i][1])
}
return []int{}
}
func initial() {
for i := 0; i <= n; i++ {
father[i] = i
}
}
func isSame(a int, b int) bool {
a = find(a)
b = find(b)
return a == b
}
func find(u int) int {
if father[u] == u {
return u
}
father[u] = find(father[u])
return father[u]
}
func join(a int, b int) {
a = find(a)
b = find(b)
if a == b {
return
}
father[a] = b
}
|
冗余连接 II
685. 冗余连接 II - 力扣(LeetCode)
给定一个二维数组表示图中的有向边;要求去掉一条边(只有一个根节点),使图变为树;返回该边,若有多个答案,返回二维数组中的最后一条边
由于是有向边,所以需要考虑到边的方向,可以将删除边的情况分为两种,一种是有入度为2的节点,另一种是没有入度为2的节点
- 若有入度为2的节点,则需要在两条边中选择一条删除,且删除二维数组中后面的那一条
- 若无入度为2的节点,则说明仅存在一个有向环,这时和上一题一样只需要找出加入一条边,一旦该边加入即成环,返回该边即可
思路:
- 第一遍遍历各个边,统计每个节点的入度,节点值作为索引,度数作为值
- 第二遍遍历各个边,一旦发现该边指向的节点入度为2,则将该边记录;若有入度为2的节点,一定会统计出两条边
- 若第二遍遍历中能统计到两条边,则先看删掉第二条边(数组中靠后)后能否构成树,若可以则返回该边,否则返回第一条边
- 初始化并查集;将各个边加入并查集;在加入前判断,若要加入的边是要删除的边则不加入,若要加入的边加入后会成环则说明不能成树返回
false
- 若第二遍遍历中没有统计到两条边,说明只存在一个有向环,则找到构成环的边返回即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
|
var father []int
var n int
func findRedundantDirectedConnection(edges [][]int) []int {
inDegree := make([]int, len(edges)+1) // 记录各节点的入度
for i := range edges {
inDegree[edges[i][1]] += 1
}
twoDegree := make([]int, 0) // 记录入度为2的节点的两条边的索引
for i := range edges {
if inDegree[edges[i][1]] == 2 {
twoDegree = append(twoDegree, i) // 找到入度为2的节点的一条边
}
}
if len(twoDegree) != 0 {
if isTreeAfterRemoveEdge(edges, twoDegree[1]) {
return edges[twoDegree[1]] // 去掉数组中靠后的该边能构成树
}
return edges[twoDegree[0]] // 去掉数组中靠前的该边能构成树
}
return getRemoveEdge(edges)
}
// isTreeAfterRemoveEdge 判断删除一条边之后能否成树
func isTreeAfterRemoveEdge(edges [][]int, deleteEdgeIdx int) bool {
n = len(edges)
father = make([]int, n+1)
initial()
for i := range edges {
if i == deleteEdgeIdx {
continue // 不添加要删除的边
}
if isSame(edges[i][0], edges[i][1]) {
return false // 该边加入会成环说明不能成树
}
join(edges[i][0], edges[i][1]) // 加入并查集
}
return true
}
// getRemoveEdge 找到加入后会成环的边
func getRemoveEdge(edges [][]int) []int {
n = len(edges)
father = make([]int, n+1)
initial()
for i := range edges {
if isSame(edges[i][0], edges[i][1]) {
return edges[i] // 找到加入后会成环的边
}
join(edges[i][0], edges[i][1]) // 加入并查集
}
return []int{}
}
// 并查集
func initial() {
for i := 0; i <= n; i++ {
father[i] = i
}
}
func isSame(a int, b int) bool {
a = find(a)
b = find(b)
return a == b
}
func find(u int) int {
if father[u] == u {
return u
}
father[u] = find(father[u])
return father[u]
}
func join(a int, b int) {
a = find(a)
b = find(b)
if a == b {
return
}
father[a] = b
}
|
最小生成树
卡码网:53. 寻宝
给定所有点和边的权值,求把所有点联通的最小总权值
1、prim算法
prim算法:从节点的角度,采用贪心的策略,每次寻找距离最小生成树最近的节点,找到后将其加入到最小生成树中
最小生成树是所有节点的最小连通子图, 即:以最小的成本(边的权值)将图中所有节点链接到一起
- 选距离生成树最近的节点
- 将最近的节点加入生成树
- 更新非生成树节点到生成树的距离(即更新
minDist
数组)
minDist
数组:记录每一个节点距离最小生成树的最近距离
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
|
package main
import (
"fmt"
"math"
)
func main() {
// v是节点个数,e是边个数
var v, e int
fmt.Scan(&v, &e)
// grid邻接矩阵保存图,权值初始化为10001,代表不相连
grid := make([][]int, v+1)
for i := range grid {
grid[i] = make([]int, v+1)
for j := range grid[i] {
grid[i][j] = 10001
}
}
// 填充grid
var x, y, k int
for ; e > 0; e-- {
fmt.Scan(&x, &y, &k)
grid[x][y] = k
grid[y][x] = k
}
// Prim算法
// 初始化minDist为10001
minDist := make([]int, v+1)
for i := range minDist {
minDist[i] = 10001
}
// 记录节点是否在最小生成树中
isInTree := make([]bool, v+1)
// 从节点1开始加入minDist
minDist[1] = 0
// 遍历节点1~v-1
for i := 1; i < v; i++ {
// 初始化最近节点为-1,最近距离为最大整型
cur := -1
minVal := math.MaxInt32
// 1. 遍历minDist找到离树的最近节点
for j := 1; j <= v; j++ {
// 判断节点是否不在树中且小于目前记录的最近距离
if !isInTree[j] && minDist[j] < minVal {
// 更新最近距离和节点
minVal = minDist[j]
cur = j
}
}
// 2. 最近节点加入树
isInTree[cur] = true
// 3. 遍历节点,更新非树中各节点到树的最近距离
for j := 1; j <= v; j++ {
// 判断是否非树中节点且离刚新入树的节点距离小于minDist记录的距离
if !isInTree[j] && grid[cur][j] < minDist[j] {
// 更新minDist
minDist[j] = grid[cur][j]
// 记录边
// parent[j] = cur
}
}
}
// 计算最小生成树的总权重
result := 0
// 遍历节点2~v
for i := 2; i <= v; i++ {
result += minDist[i]
}
fmt.Println(result)
}
|
2、Kruskal算法
Kruskal算法:从边的角度,采用贪心的策略,每次寻找权值最小的边,找到后将其加入到最小生成树中
- 将边按权值排序,优先选最小的边加入到生成树里
- 若该边首尾的两个节点在一个集合,则说明连上这条边图中会出现环
- 若该边首尾的两个节点不在同一个集合,则将该边加入到最小生成树,并把两个节点加入同一个集合
判断两个节点是否在同一个集合——并查集
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
|
package main
import (
"fmt"
"sort"
)
// 边结构体,包含边的两个端点和边的权值
type Edge struct {
From int
To int
Cost int
}
// 并查集结构体
type UnionFind struct {
parent []int
}
// 初始化并查集
func NewUnionFind(size int) *UnionFind {
parent := make([]int, size)
for i := range parent {
parent[i] = i
}
return &UnionFind{parent}
}
// 查找根节点
func (uf *UnionFind) Find(x int) int {
if uf.parent[x] != x {
uf.parent[x] = uf.Find(uf.parent[x]) // 路径压缩
}
return uf.parent[x]
}
// 加入集合
func (uf *UnionFind) Union(x, y int) {
rootX := uf.Find(x)
rootY := uf.Find(y)
if rootX != rootY {
uf.parent[rootY] = rootX
}
}
func main() {
// v是节点数,e是边数
var v, e int
fmt.Scan(&v, &e)
// 获取各个边
edges := make([]Edge, e)
for i := 0; i < e; i++ {
var v1, v2, val int
fmt.Scan(&v1, &v2, &val)
edges[i] = Edge{v1, v2, val}
}
// 按边的权值从小到大排序
sort.Slice(edges, func(i, j int) bool {
return edges[i].Cost < edges[j].Cost
})
// 初始化并查集
uf := NewUnionFind(v + 1)
result := 0
// 遍历边
for _, edge := range edges {
// 判断两个端点是否不在同一个集合中
if uf.Find(edge.From) != uf.Find(edge.To) {
// 计入结果集
result += edge.Cost
// 加入生成树中(入集)
uf.Union(edge.From, edge.To)
// 保存最小生成树的边
// res_edge = append(res_edge, edge)
}
}
fmt.Println(result)
}
|
总结:
在稀疏图中,用Kruskal
更优; 在稠密图中,用prim
算法更优。
边数量较少为稀疏图,接近或等于完全图(所有节点皆相连)为稠密图
Prim
算法 时间复杂度为 $O(n^2)$,其中n
为节点数量,它的运行效率和图中边无关,适用稠密图;
Kruskal
算法 时间复杂度 为 $O(nlogn)$,其中n
为边的数量,适用稀疏图
拓扑排序
卡码网:117. 软件构建
给定所有点和边,边表示依赖关系,判断能否按照依赖关系访问所有点
拓扑排序:给出一个有向图,把这个有向图转成线性的排序
由于拓扑排序会检测有向图是否有环,有环是不能做线性排序的,所以拓扑排序也是图论中判断有向无环图的常用方法
拓扑排序的过程,其实就两步:
- 找到入度为0 的节点,加入结果集
- 将该节点从图中移除
循环以上两步,直 所有节点都在图中被移除了
结果集的顺序,就是我们想要的拓扑排序顺序 (结果集里顺序可能不唯一)
若结果集元素个数不等于图中节点个数,则说明图中一定有环,因为找不到入度为0的节点入集
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
package main
import (
"fmt"
)
func main() {
// n个点,m个依赖关系,t依赖s
var n, m, s, t int
fmt.Scan(&n, &m)
// 记录每个文件的入度
inDegree := make([]int, n)
// 记录文件依赖关系
umap := make(map[int][]int)
result := make([]int, 0)
for i := 0; i < m; i++ {
fmt.Scan(&s, &t)
// t的入度加一
inDegree[t]++
// 记录s->t
umap[s] = append(umap[s], t)
}
queue := make([]int, 0)
// 遍历节点入度
for i := 0; i < n; i++ {
if inDegree[i] == 0 {
// 入度为0的节点作为开头加入队列
queue = append(queue, i)
}
}
for len(queue) > 0 {
// 从队列中取出当前入度为0的点
cur := queue[0]
queue = queue[1:]
result = append(result, cur)
// 获取cur指向的点
files := umap[cur]
// 遍历cur指向的点
for _, f := range files {
// cur的指向的点入度-1
inDegree[f]--
// 判断该点入度是否为0
if inDegree[f] == 0 {
// 入度为0入队
queue = append(queue, f)
}
}
}
// 判断结果集元素个数是否等于总节点数
if len(result) == n {
// 顺序打印拓扑排序序列
for i := 0; i < n-1; i++ {
fmt.Print(result[i], " ")
}
fmt.Println(result[n-1])
} else {
// 不是有向无环图,无法排序
fmt.Println(-1)
}
}
|
Dijkstra
卡码网:47. 参加科学大会
给出一个有向图,一个起点,一个终点,问起点到终点的最短路径
- 选离源点最近且未被访问过的节点
- 标记该最近节点被访问
- 更新非访问节点到源点的距离(即更新minDist数组)
minDist数组:记录每个节点到源点的最短距离
朴素版
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
|
package main
import (
"fmt"
"math"
)
func main() {
// n个节点m条边,p1->p2权值val
var n, m, p1, p2, val int
fmt.Scan(&n, &m)
// 初始化邻接矩阵,用于存储边的权重
grid := make([][]int, n+1)
for i := range grid {
grid[i] = make([]int, n+1)
for j := range grid[i] {
// 使用math.MaxInt32表示两点间无边
grid[i][j] = math.MaxInt32
}
}
// 读入边信息并构建邻接矩阵
for i := 0; i < m; i++ {
fmt.Scan(&p1, &p2, &val)
grid[p1][p2] = val
}
// 起点为节点1终点为节点n
start := 1
end := n
// 存储从源点到每个节点的最短距离
minDist := make([]int, n+1)
for i := range minDist {
// 初始化为无穷大
minDist[i] = math.MaxInt32
}
// 起始点到自身的距离为0
minDist[start] = 0
// 记录点是否被访问过
visited := make([]bool, n+1)
// 遍历节点1-n
for i := 1; i <= n; i++ {
minVal := math.MaxInt32
cur := 1
// 遍历minDist选取距离源点最近且未访问过的节点
for v := 1; v <= n; v++ {
if !visited[v] && minDist[v] < minVal {
minVal = minDist[v]
cur = v
}
}
// 标记该节点已被访问
visited[cur] = true
// 更新非访问节点到源点的距离(即更新minDist数组)
for v := 1; v <= n; v++ {
if !visited[v] && grid[cur][v] != math.MaxInt32 && minDist[cur]+grid[cur][v] < minDist[v] {
minDist[v] = minDist[cur] + grid[cur][v]
// 记录边
// parent[v] = cur;
}
}
}
if minDist[end] == math.MaxInt32 {
// 不能到达终点
fmt.Println(-1)
} else {
// 到达终点最短路径
fmt.Println(minDist[end])
}
}
|
- 时间复杂度:$O(n^2)$
- 空间复杂度:$O(n^2)$
堆优化版:邻接表(数组+链表)存储边,适用于稀疏图
在处理三部曲里的第一步(选离源点最近且未被访问过的节点)的时候 ,可以不用去遍历所有节点,直接把边(带权值)加入到小顶堆(利用堆来自动排序),每次从堆顶里取出边自然就是距离源点最近的节点所在的边,这样就不需要两层for循环来寻找最近的节点了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
|
package main
import (
"container/heap"
"fmt"
"math"
)
// 定义边的结构体
type Edge struct {
to int
val int
}
func main() {
var n, m, p1, p2, val int
fmt.Scan(&n, &m)
// 初始化邻接表
grid := make([][]Edge, n+1)
// 读入边信息并构建邻接表
for i := 0; i < m; i++ {
fmt.Scan(&p1, &p2, &val)
grid[p1] = append(grid[p1], Edge{p2, val})
}
start := 1
end := n
// 存储从源点到每个节点的最短距离
minDist := make([]int, n+1)
for i := range minDist {
minDist[i] = math.MaxInt32 // 初始化为无穷大
}
minDist[start] = 0 // 起始点到自身的距离为0
// 使用优先队列实现最小堆
pq := make(PriorityQueue, 0)
heap.Init(&pq)
heap.Push(&pq, &Item{value: start, priority: 0})
// 记录顶点是否被访问过
visited := make([]bool, n+1)
for pq.Len() > 0 {
// 从优先队列中取出当前距离起点最近的节点
item := heap.Pop(&pq).(*Item)
u := item.value
if visited[u] {
continue
}
visited[u] = true
// 更新与当前节点相邻的节点的最短距离
for _, edge := range grid[u] {
v := edge.to
w := edge.val
if !visited[v] && minDist[u]+w < minDist[v] {
minDist[v] = minDist[u] + w
heap.Push(&pq, &Item{value: v, priority: minDist[v]})
}
}
}
// 输出结果
if minDist[end] == math.MaxInt32 {
fmt.Println(-1) // 不能到达终点
} else {
fmt.Println(minDist[end]) // 到达终点最短路径
}
}
// 优先队列实现(最小堆)
type Item struct {
value int // 节点编号
priority int // 源点到该节点的距离
}
type PriorityQueue []*Item
func (pq PriorityQueue) Len() int { return len(pq) }
func (pq PriorityQueue) Less(i, j int) bool {
return pq[i].priority < pq[j].priority
}
func (pq PriorityQueue) Swap(i, j int) {
pq[i], pq[j] = pq[j], pq[i]
}
func (pq *PriorityQueue) Push(x interface{}) {
item := x.(*Item)
*pq = append(*pq, item)
}
func (pq *PriorityQueue) Pop() interface{} {
old := *pq
n := len(old)
item := old[n-1]
*pq = old[0 : n-1]
return item
}
|
- 时间复杂度:$O(ElogE)$, $E$ 为边的数量
- 空间复杂度:$O(N + E)$, $N$ 为节点的数量
Bellman_ford
卡码网:94. 城市间货物运输 I
给出一个有向图,一个起点,一个终点,问起点到终点的最短路径,边的权值有负数,但不存在任何负权回路。
负权回路是指一系列道路的总权值为负,这样的回路使得通过反复经过回路中的道路,理论上可以无限地减少总成本或无限地增加总收益。
minDist数组:表示起点到各个节点的最短距离,例如minDist[3] = 5
表示起点到达节点3的最小距离为5
Bellman_ford:对所有边松弛n-1
次,得出起点到终点的最短路径
- 松弛:动态规划,假设
minDist[B]
表示到达B节点的最小权值,minDist[B] = min(minDist[A] + value, minDist[B])
- 状态一:
minDist[A] + value
可以推出 minDist[B]
- 状态二:
minDist[B]
本身就有权值 (可能是C->B
的权值)
- 对所有边松弛一次,相当于计算起点到与起点一条边相连的节点的最短距离;对所有边松弛
n-1
次,就一定能得到起点到终点的最短距离
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
package main
import (
"fmt"
"math"
)
func main() {
var n, m, p1, p2, val int
fmt.Scan(&n, &m)
// 创建邻接表
grid := make([][]int, 0)
// 读入边信息并构建邻接表
for i := 0; i < m; i++ {
fmt.Scan(&p1, &p2, &val)
grid = append(grid, []int{p1, p2, val})
}
start := 1
end := n
// 存储从源点到每个节点的最短距离
minDist := make([]int, n+1)
for i := range minDist {
// 初始化为无穷大
minDist[i] = math.MaxInt32
}
// 起始点到自身的距离为0
minDist[start] = 0
// 进行 n-1 次松弛操作
for i := 1; i < n; i++ {
for _, side := range grid {
from := side[0]
to := side[1]
price := side[2]
if minDist[from] != math.MaxInt32 && minDist[to] > minDist[from]+price {
minDist[to] = minDist[from] + price
}
}
}
// 输出结果
if minDist[end] == math.MaxInt32 {
// 不能到达终点
fmt.Println("unconnected")
} else {
// 到达终点最短路径
fmt.Println(minDist[end])
}
}
|
- 时间复杂度: $O(N*E)$ ,$N$为节点数量,$E$为图中边的数量
- 空间复杂度: $O(N)$ ,即
minDist
数组所开辟的空间
Bellman_ford 队列优化算法
卡码网:94. 城市间货物运输 I
给出一个有向图,一个起点,一个终点,问起点到终点的最短路径,边的权值有负数,但不存在任何负权回路。
负权回路是指一系列道路的总权值为负,这样的回路使得通过反复经过回路中的道路,理论上可以无限地减少总成本或无限地增加总收益。
Bellman_ford
队列优化算法 ,也叫SPFA
算法(Shortest Path Faster Algorithm
)。
SPFA的称呼来自 1994年西南交通大学段凡丁的论文,其实Bellman_ford
提出后不久 (20世纪50年代末期) 就有队列优化的版本,国际上不承认这个算法是是国内提出的。 所以国际上一般称呼该算法为Bellman_ford
队列优化算法(Queue improved Bellman-Ford
)
Bellman_ford
算法每次都是对所有边进行松弛,其实是多做了一些无用功。只需要对上一次松弛的时候更新过的节点作为出发节点所连接的边进行松弛就够了。基于以上思路,用队列来记录上次松弛的时候更新过的节点;在加入队列的过程可以有一个优化:用visited
数组记录已经在队列里的元素,已经在队列的元素不用重复加入
依然使用minDist
数组表示起点到各个节点的最短距离,例如minDist[3] = 5
表示起点到节点3的最小距离为5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
|
package main
import (
"container/list"
"fmt"
"math"
)
// Edge 结构体表示图中的一条边
type Edge struct {
to int // 目标节点
val int // 边的权重
}
func main() {
var n, m, p1, p2, val int
fmt.Scan(&n, &m)
// 创建邻接表
grid := make([]*list.List, n+1)
for i := range grid {
grid[i] = list.New()
}
// 读取边信息并构建邻接表
for i := 0; i < m; i++ {
fmt.Scan(&p1, &p2, &val)
grid[p1].PushBack(Edge{to: p2, val: val})
}
start := 1
end := n
// 存储从源点到每个节点的最短距离
minDist := make([]int, n+1)
for i := range minDist {
// 初始化为无穷大
minDist[i] = math.MaxInt32
}
minDist[start] = 0
que := list.New()
que.PushBack(start)
// 记录已经在队列里的元素
isInQueue := make([]bool, n+1)
isInQueue[start] = true
// 遍历队列
for que.Len() > 0 {
front := que.Front()
node := front.Value.(int)
que.Remove(front)
// 从队列里取出的时候,要取消标记,只保证已经在队列里的元素不用重复加入
isInQueue[node] = false
for e := grid[node].Front(); e != nil; e = e.Next() {
edge := e.Value.(Edge)
to := edge.to
value := edge.val
if minDist[to] > minDist[node]+value {
// 开始松弛
minDist[to] = minDist[node] + value
// 已经在队列里的元素不用重复添加
if !isInQueue[to] {
que.PushBack(to)
isInQueue[to] = true
}
}
}
}
// 输出结果
if minDist[end] == math.MaxInt32 {
// 不能到达终点
fmt.Println("unconnected")
} else {
// 到达终点的最短路径
fmt.Println(minDist[end])
}
}
|
- 时间复杂度:最坏的情况下是 $O(N * E)$,一般情况下为$O(K * N)$
SPFA(队列优化版Bellman_ford)在理论上时间复杂度更胜一筹,但实际上,也要看图的稠密程度,如果图很大且非常稠密的情况下,虽然SPFA的时间复杂度接近Bellman_ford,但SPFA实际时间消耗可能更多
bellman_ford 判断负权回路
卡码网:95. 城市间货物运输 II
给出一个有向图,一个起点,一个终点,问起点到终点的最短路径,边的权值有负数,图中可能存在负权回路。
负权回路是指一系列道路的总权值为负,这样的回路使得通过反复经过回路中的道路,理论上可以无限地减少总成本或无限地增加总收益,算法需要能检测出这种特殊情况
在有负权回路的情况下,一直都会有更短的最短路,所以松弛第n
次后,minDist
数组也会发生改变。所有判断图中是否有负权回路的核心思路就是在松弛n-1
次的基础上,再多松弛一次,看minDist
数组是否发生变化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
package main
import (
"fmt"
"math"
)
func main() {
var n, m, p1, p2, val int
fmt.Scan(&n, &m)
var grid [][3]int
for i := 0; i < m; i++ {
fmt.Scan(&p1, &p2, &val)
// p1 指向 p2,权值为 val
grid = append(grid, [3]int{p1, p2, val})
}
// 起点
start := 1
// 终点
end := n
minDist := make([]int, n+1)
for i := range minDist {
// 初始化为无穷大
minDist[i] = math.MaxInt32
}
minDist[start] = 0
flag := false
// 松弛 n 次,最后一次判断负权回路
for i := 1; i <= n; i++ {
for _, side := range grid {
from := side[0]
to := side[1]
price := side[2]
if i < n {
if minDist[from] != math.MaxInt32 && minDist[to] > minDist[from]+price {
minDist[to] = minDist[from] + price
}
} else {
// 多加一次松弛判断负权回路
if minDist[from] != math.MaxInt32 && minDist[to] > minDist[from]+price {
flag = true
}
}
}
}
if flag {
fmt.Println("circle")
} else if minDist[end] == math.MaxInt32 {
fmt.Println("unconnected")
} else {
fmt.Println(minDist[end])
}
}
|
- 时间复杂度: $O(N * E)$ , $N$为节点数量,$E$为图中边的数量
- 空间复杂度: $O(N)$,即
minDist
数组所开辟的空间
bellman_ford 单源有限最短路
卡码网:96. 城市间货物运输 III
给出一个有向图,一个起点,一个终点,一个整数k
,问在最多经过 k 个点的条件下,起点到终点的最短路径,边的权值有负数,图中可能存在负权回路。
负权回路是指一系列道路的总权值为负,这样的回路使得通过反复经过回路中的道路,理论上可以无限地减少总成本或无限地增加总收益,算法需要能检测出这种特殊情况
本题是单源有限最短路问题,最多经过k
个点, 那么就是k + 1
条边相连的节点
对所有边松弛一次,相当于计算起点到与起点一条边相连的节点的最短距离,那么对所有边松弛k + 1
次,就得到从起点到与起点k + 1
条边相连的节点的最短距离
每次计算 minDist 时候,要基于对所有边上一次松弛的minDist
数值才行,所以要记录上一次松弛的minDist
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
package main
import (
"bufio"
"fmt"
"os"
"strconv"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
scanner.Split(bufio.ScanWords)
n, m := readTwoInts(scanner)
grid := make([][]int, m)
for i := 0; i < m; i++ {
p1, p2, val := readThreeInts(scanner)
grid[i] = []int{p1, p2, val}
}
src, dst, k := readThreeInts(scanner)
minDist := make([]int, n+1)
// 用来记录上一次遍历的结果
minDistCopy := make([]int, n+1)
for i := range minDist {
minDist[i] = int(^uint(0) >> 1) // Equivalent of INT_MAX in Go
// 获取上一次计算的结果
minDistCopy[i] = minDist[i]
}
minDist[src] = 0
for i := 1; i <= k+1; i++ {
copy(minDistCopy, minDist)
for _, side := range grid {
from, to, price := side[0], side[1], side[2]
// 注意使用 minDistCopy 来计算 minDist
if minDistCopy[from] != int(^uint(0)>>1) && minDist[to] > minDistCopy[from]+price {
minDist[to] = minDistCopy[from] + price
}
}
}
if minDist[dst] == int(^uint(0)>>1) {
// 不能到达终点
fmt.Println("unreachable")
} else {
// 到达终点最短路径
fmt.Println(minDist[dst])
}
}
func readTwoInts(scanner *bufio.Scanner) (int, int) {
scanner.Scan()
text := scanner.Text()
a, _ := strconv.Atoi(text)
scanner.Scan()
text = scanner.Text()
b, _ := strconv.Atoi(text)
return a, b
}
func readThreeInts(scanner *bufio.Scanner) (int, int, int) {
a, b := readTwoInts(scanner)
scanner.Scan()
text := scanner.Text()
c, _ := strconv.Atoi(text)
return a, b, c
}
|
- 时间复杂度: $O(K * E)$ ,$K$为至多经过$K$个节点,$E$为图中边的数量
- 空间复杂度: $O(N)$ ,即
minDist
数组所开辟的空间
对比本题与前面讲解过的 94.城市间货物运输I 和 95.城市间货物运输II
94.城市间货物运输I,没有负权回路的,那么多松弛多少次,对结果都没有影响
- 求节点1到节点n的最短路径,松弛
n-1
次就够了,松弛大于n-1
次,结果也不会变
- 在对所有边进行第一次松弛的时候,如果基于本次计算的
minDist
来计算minDist
(相当于多做松弛了),也是对最终结果没影响
95.城市间货物运输II,判断是否有负权回路,一旦有负权回路, 对所有边松弛n-1
次以后,再做松弛,minDist
数值一定会变,根据这一点来判断是否有负权回路
- 只需要判断
minDist
数值是否变化了就行,而minDist
的数值对不对,并不关心
本题计算minDist
一定要基于上次的minDist
数值,其关键在于本题的两个因素:
- 本题可以有负权回路,说明只要多做松弛,结果是会变的
- 本题要求最多经过
k
个节点,对松弛次数是有限制的
Floyd
卡码网:97. 小明逛公园
多源最短路问题,求多个起点到多个终点的多条最短路径
Floyd 算法对边的权值正负没有要求,核心思想是动态规划
grid[i][j][k] = m
:节点i
到节点j
,以[1...k]
集合为中间节点的最短距离为m
- 递推公式:分两种情况,节点
i
到节点j
的最短路径经过节点k
、节点i
到节点j
的最短路径不经过节点k
grid[i][j][k] = min(grid[i][k][k-1]+grid[k][j][k-1], grid[i][j][k-1])
- 初始化:
grid[i][j][0] = val
、grid[i][j][k] = max (k ≠ 0)
- 遍历顺序:三维中
i j
为底平面,k为纵轴,自底向上遍历
空间优化:
-
从滚动数组的角度来看,定义一个grid[i][j][2]
这么大的数组就可以,因为k
只依赖于k-1
的状态,那么只记录grid[i][j][1]
和grid[i][j][0]
就好,之后就是grid[i][j][1]
和grid[i][j][0]
交替滚动
-
如果本层刚计算好的grid[i][k]
比上一层 (即k-1
层)计算的grid[i][k]
小,说明确实有i
到k
的更短路径,那么基于更小的grid[i][k]
去计算gird[i][j]
没有问题。如果 本层刚计算好的grid[i][k]
比上一层 (即k-1
层)计算的grid[i][k]
大, 这不可能,因为这样也不会做更新grid[i][k]
的操作
所以本层计算中就没必要区分,grid[i][k]
和grid[k][j]
是属于k - 1
层的还是k
层的,故递归公式可以为:
grid[i][j] = min(grid[i][j], grid[i][k] + grid[k][j])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
package main
import (
"fmt"
)
func main() {
var n, m int
fmt.Scanf("%d %d", &n, &m)
grid := make([][]int, n+1)
for i := range grid {
grid[i] = make([]int, n+1)
for j := range grid[i] {
// 最大距离
grid[i][j] = 10005
}
}
for i := 0; i < m; i++ {
var p1, p2, val int
fmt.Scanf("%d %d %d", &p1, &p2, &val)
// 双向图
grid[p1][p2] = val
grid[p2][p1] = val
}
// 开始Floyd
for k := 1; k <= n; k++ {
for i := 1; i <= n; i++ {
for j := 1; j <= n; j++ {
grid[i][j] = min(grid[i][j], grid[i][k]+grid[k][j])
}
}
}
// 输出结果
var z int
fmt.Scanf("%d", &z)
for z > 0 {
z--
var start, end int
fmt.Scanf("%d %d", &start, &end)
if grid[start][end] == 10005 {
fmt.Println(-1)
} else {
fmt.Println(grid[start][end])
}
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
|
- 时间复杂度:$O(n^3)$
- 空间复杂度:$O(n^2)$
A star
卡码网:126. 骑士的攻击
Astar是一种广搜的改良版,有的是Astar是 dijkstra 的改良版;其实只是场景不同,在搜索最短路的时候,如果是无权图(边的权值都是1) 就用广搜,代码简洁,时间效率和 dijkstra 差不多 (具体要取决于图的稠密),如果是有权图(边有不同的权值),优先考虑 dijkstra;而 Astar 关键在于启发式函数, 也就是影响广搜或者 dijkstra 从容器(队列)里取元素的优先顺序;下面介绍BFS版本的A star
对队列中的节点按权值F进行排序:F = G + H
- G:起点达目前遍历节点的距离
- H:目前遍历的节点到终点的距离
起点到终点的距离 = 起点到目前遍历节点的距离 + 目前遍历节点到终点的距离
本题的图是无权网格,在计算两点距离时通常有三种计算方式:
- 曼哈顿距离:$d = \abs{(x_1-x_2)+\abs{y_1-y_2}}$
- 欧氏距离(欧拉距离):$d = \sqrt{(x_1-x_2)^2 + (y_1-y_2)^2 )}$
- 切比雪夫距离:$d = max(\abs{x_1 - x_2}, \abs{y_1 - y_2})$
选择不同的距离计算方式也会导致 Astar算法的结果不同,本题需要采用欧拉距离才能最大程度体现点与点之间的距离
可以使用优先级队列排序,每次出队的就是F最小的节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
|
package main
import (
"container/heap"
"fmt"
)
const (
maxN = 1001
)
var moves [maxN][maxN]int
var dir = [8][2]int{
{-2, -1}, {-2, 1}, {-1, 2}, {1, 2},
{2, 1}, {2, -1}, {1, -2}, {-1, -2},
}
type Knight struct {
x, y, g, h, f int
}
type KnightHeap []Knight
func (h KnightHeap) Len() int { return len(h) }
func (h KnightHeap) Less(i, j int) bool { return h[i].f < h[j].f }
func (h KnightHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *KnightHeap) Push(x interface{}) {
*h = append(*h, x.(Knight))
}
func (h *KnightHeap) Pop() interface{} {
old := *h
n := len(old)
x := old[n-1]
*h = old[0 : n-1]
return x
}
// 欧拉距离
func heuristic(k Knight, b1, b2 int) int {
// 统一不开根号,这样可以提高精度
return (k.x-b1)*(k.x-b1) + (k.y-b2)*(k.y-b2)
}
func astar(start Knight, b1, b2 int) int {
h := &KnightHeap{}
heap.Init(h)
heap.Push(h, start)
for h.Len() > 0 {
cur := heap.Pop(h).(Knight)
if cur.x == b1 && cur.y == b2 {
return moves[b1][b2]
}
for _, d := range dir {
nextX := cur.x + d[0]
nextY := cur.y + d[1]
if nextX < 1 || nextX >= maxN || nextY < 1 || nextY >= maxN {
continue
}
if moves[nextX][nextY] == 0 {
moves[nextX][nextY] = moves[cur.x][cur.y] + 1
// 开始计算F
next := Knight{
x: nextX,
y: nextY,
g: cur.g + 5,
h: heuristic(Knight{x: nextX, y: nextY}, b1, b2),
}
next.f = next.g + next.h
heap.Push(h, next)
}
}
}
// unreachable case
return -1
}
func main() {
var n, a1, a2, b1, b2 int
fmt.Scan(&n)
for n > 0 {
fmt.Scan(&a1, &a2, &b1, &b2)
for i := range moves {
for j := range moves[i] {
moves[i][j] = 0
}
}
if a1 < 1 || a1 >= maxN || a2 < 1 || a2 >= maxN || b1 < 1 || b1 >= maxN || b2 < 1 || b2 >= maxN {
fmt.Println(-1)
n--
continue
}
start := Knight{
x: a1,
y: a2,
g: 0,
h: heuristic(Knight{x: a1, y: a2}, b1, b2),
}
start.f = start.g + start.h
result := astar(start, b1, b2)
fmt.Println(result)
n--
}
}
|
- 时间复杂度:$O(nlogn)$ ,n为节点数量。
- 空间复杂度:$O(b ^ d)$ ,d 为起点到终点的深度,b 是图中节点间的连接数量
最短路算法总结
四大最短路算法:Dijkstra、Bellman_ford、SPFA 和 Floyd
(因为A * 属于启发式搜索,和上面最短路算法并不是一类,不适合一起对比,所以没有放在一起)
- 如果遇到单源且边为正数,直接Dijkstra,至于使用朴素版还是堆优化版还是取决于图的稠密度,一般情况下,可以直接用堆优化版本
- 如果遇到单源边可为负数,直接 Bellman-Ford,同样 SPFA 还是 Bellman-Ford 取决于图的稠密度,一般情况下,直接用 SPFA
- 如果有负权回路,优先 Bellman-Ford, 如果是有限节点最短路也优先 Bellman-Ford,理由是写代码比较方便
- 如果是遇到多源点求最短路,直接 Floyd,除非源点特别少,且边都是正数,那可以多次 Dijkstra 求出最短路径,但这种情况很少,一般出现多个源点了,就用 Floyd
- A star由于其高效性,在实际工程应用中使用最为广泛,游戏开发、地图导航、数据包路由等都广泛使用 A * 算法 ,但由于其结果的不唯一性,也就是可能是次短路的特性,一般不适合作为算法题,