《代码随想录第六十六天》——Floyd 算法、A * 算法

《代码随想录第六十六天》——Floyd 算法、A * 算法

本篇文章的所有内容仅基于C++撰写。

1. 基础知识

1.1 题目

小明逛公园
题目描述
小明喜欢去公园散步,公园内布置了许多的景点,相互之间通过小路连接,小明希望在观看景点的同时,能够节省体力,走最短的路径。
给定一个公园景点图,图中有 N 个景点(编号为 1 到 N),以及 M 条双向道路连接着这些景点。每条道路上行走的距离都是已知的。
小明有 Q 个观景计划,每个计划都有一个起点 start 和一个终点 end,表示他想从景点 start 前往景点 end。由于小明希望节省体力,他想知道每个观景计划中从起点到终点的最短路径长度。 请你帮助小明计算出每个观景计划的最短路径长度。

输入描述
第一行包含两个整数 N, M, 分别表示景点的数量和道路的数量。
接下来的 M 行,每行包含三个整数 u, v, w,表示景点 u 和景点 v 之间有一条长度为 w 的双向道路。
接下里的一行包含一个整数 Q,表示观景计划的数量。
接下来的 Q 行,每行包含两个整数 start, end,表示一个观景计划的起点和终点。
输出描述
对于每个观景计划,输出一行表示从起点到终点的最短路径长度。如果两个景点之间不存在路径,则输出 -1。

输入示例
7 3
2 3 4
3 6 6
4 7 8
2
2 3
3 4
输出示例
4
-1

提示信息
从 2 到 3 的路径长度为 4,3 到 4 之间并没有道路。
1 <= N, M, Q <= 1000.
1 <= w <= 10000.

1.2 分析

本题是经典的多源最短路问题,即 求多个起点到多个终点的多条最短路径。

动规五部曲:

1、确定dp数组(dp table)以及下标的含义
用 grid数组来存图,那就把dp数组命名为 grid。
grid[i][j][k] = m,表示 节点i 到 节点j 以[1…k] 集合中的一个节点为中间节点的最短距离为m。
节点i 到 节点j 的最短距离为m,这句话可以理解。节点i 到 节点j 的最短路径中 一定是经过很多节点,那么这个集合用[1…k] 来表示。这里的k不能单独指某个节点,k 一定要表示一个集合,即[1…k] ,表示节点1 到 节点k 一共k个节点的集合。

2、确定递推公式
分两种情况:

  • 节点i 到 节点j 的最短路径经过节点k
  • 节点i 到 节点j 的最短路径不经过节点k

第一种情况,grid[i][j][k] = grid[i][k][k - 1] + grid[k][j][k - 1]。
节点i 到 节点k 的最短距离 是不经过节点k,中间节点集合为[1…k-1],所以 表示为grid[i][k][k - 1]。
节点k 到 节点j 的最短距离 也是不经过节点k,中间节点集合为[1…k-1],所以表示为 grid[k][j][k - 1]。

第二种情况,grid[i][j][k] = grid[i][j][k - 1]。
如果节点i 到 节点j的最短距离 不经过节点k,那么 中间节点集合[1…k-1],表示为 grid[i][j][k - 1]。

因为我们是求最短路,对于这两种情况自然是取最小值。即: grid[i][j][k] = min(grid[i][k][k - 1] + grid[k][j][k - 1], grid[i][j][k - 1])

3、dp数组如何初始化
grid[i][j][k] = m,表示 节点i 到 节点j 以[1…k] 集合为中间节点的最短距离为m。最初只能 把k 赋值为 0,本题 节点0 是无意义的,节点是从1 到 n。这样我们在下一轮计算的时候,就可以根据 grid[i][j][0] 来计算 grid[i][j][1],此时的 grid[i][j][1] 就是 节点i 经过节点1 到达 节点j 的最小距离了。grid数组是一个三维数组,那么我们初始化的数据在 i 与 j 构成的平层,如图:
在这里插入图片描述红色的 底部一层是我们初始化好的数据,注意:从三维角度去看初始化的数据很重要。

4、确定遍历顺序
从递推公式:grid[i][j][k] = min(grid[i][k][k - 1] + grid[k][j][k - 1], grid[i][j][k - 1]) 可以看出,我们需要三个for循环,分别遍历i,j 和k,而 k 依赖于 k - 1, i 和j 的到 并不依赖与 i - 1 或者 j - 1 等等。所以遍历k 的for循环一定是在最外面,这样才能一层一层去遍历。

注意,由于k表示的是0-k个节点中的某些中间节点,而不是全部的中间节点,所以可以将数组简化为 grid[i][k] 和 grid[k][j] ,也就是下面代码的空间优化版。

1.3 代码

  1. 原版
#include <iostream>
#include <vector>
#include <list>
using namespace std;

int main() {
    int n, m, p1, p2, val;
    cin >> n >> m;

    vector<vector<vector<int>>> grid(n + 1, vector<vector<int>>(n + 1, vector<int>(n + 1, 10005)));  // 因为边的最大距离是10^4
    for(int i = 0; i < m; i++){
        cin >> p1 >> p2 >> val;
        grid[p1][p2][0] = val;
        grid[p2][p1][0] = val; // 注意这里是双向图

    }
    // 开始 floyd
    for (int k = 1; k <= n; k++) {
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                grid[i][j][k] = min(grid[i][j][k-1], grid[i][k][k-1] + grid[k][j][k-1]);
            }
        }
    }
    // 输出结果
    int z, start, end;
    cin >> z;
    while (z--) {
        cin >> start >> end;
        if (grid[start][end][n] == 10005) cout << -1 << endl;
        else cout << grid[start][end][n] << endl;
    }
}

  1. 空间优化版(滚动数组)
#include <iostream>
#include <vector>
using namespace std;

int main() {
    int n, m, p1, p2, val;
    cin >> n >> m;

    vector<vector<int>> grid(n + 1, vector<int>(n + 1, 10005));  // 因为边的最大距离是10^4

    for(int i = 0; i < m; i++){
        cin >> p1 >> p2 >> val;
        grid[p1][p2] = val;
        grid[p2][p1] = val; // 注意这里是双向图

    }
    // 开始 floyd
    for (int k = 1; k <= n; k++) {
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                grid[i][j] = min(grid[i][j], grid[i][k] + grid[k][j]);
            }
        }
    }
    // 输出结果
    int z, start, end;
    cin >> z;
    while (z--) {
        cin >> start >> end;
        if (grid[start][end] == 10005) cout << -1 << endl;
        else cout << grid[start][end] << endl;
    }
}
  • 时间复杂度: O(n^3)
  • 空间复杂度:O(n^2)

2. 题目1

2.1 题目

骑士的攻击
题目描述
在象棋中,马和象的移动规则分别是“马走日”和“象走田”。现给定骑士的起始坐标和目标坐标,要求根据骑士的移动规则,计算从起点到达目标点所需的最短步数。棋盘大小 1000 x 1000(棋盘的 x 和 y 坐标均在 [1, 1000] 区间内,包含边界)

输入描述
第一行包含一个整数 n,表示测试用例的数量,1 <= n <= 100。
接下来的 n 行,每行包含四个整数 a1, a2, b1, b2,分别表示骑士的起始位置 (a1, a2) 和目标位置 (b1, b2)。
输出描述
输出共 n 行,每行输出一个整数,表示骑士从起点到目标点的最短路径长度。

输入示例
6
5 2 5 4
1 1 2 2
1 1 8 8
1 1 8 7
2 1 3 3
4 6 4 6
输出示例
2
4
6
5
1
0
提示信息
骑士移动规则如图,红色是起始位置,黄色是骑士可以走的地方。

2.2 分析

BFS 是没有目的性的 一圈一圈去搜索, 而 A * 是有方向性的去搜索。其关键在于 启发式函数。
启发式函数 要影响的就是队列里元素的排序!这是影响BFS搜索方向的关键。对队列里节点进行排序,就需要给每一个节点权值,如何计算权值呢?
每个节点的权值为F,给出公式为:F = G + H
G:起点达到目前遍历节点的距离
H:目前遍历的节点到达终点的距离
起点达到目前遍历节点的距离 + 目前遍历的节点到达终点的距离 就是起点到达终点的距离。

本题的图是无权网格状,在计算两点距离通常有如下三种计算方式:

  • 曼哈顿距离,计算方式: d = abs(x1-x2)+abs(y1-y2)
  • 欧氏距离(欧拉距离) ,计算方式:d = sqrt( (x1-x2)^2 + (y1-y2)^2 )
  • 切比雪夫距离,计算方式:d = max(abs(x1 - x2), abs(y1 - y2))
    x1, x2 为起点坐标,y1, y2 为终点坐标 ,abs 为求绝对值,sqrt 为求开根号,

选择哪一种距离计算方式 也会导致 A * 算法的结果不同。本题,采用欧拉距离才能最大程度体现 点与点之间的距离。所以 使用欧拉距离计算 和 广搜搜出来的最短路的节点数是一样的。 (路径可能不同,但路径上的节点数是相同的)

计算出来 F 之后,按照 F 的 大小,选出队列的节点。可以使用 优先级队列 帮我们排好序,每次出队列,就是F最小的节点。

2.3 代码

#include<iostream>
#include<queue>
#include<string.h>
using namespace std;
int moves[1001][1001];
int dir[8][2]={-2,-1,-2,1,-1,2,1,2,2,1,2,-1,1,-2,-1,-2};
int b1, b2;
// F = G + H
// G = 从起点到该节点路径消耗
// H = 该节点到终点的预估消耗

struct Knight{
    int x,y;
    int g,h,f;
    bool operator < (const Knight & k) const{  // 重载运算符, 从小到大排序
     return k.f < f;
    }
};

priority_queue<Knight> que;

int Heuristic(const Knight& k) { // 欧拉距离
    return (k.x - b1) * (k.x - b1) + (k.y - b2) * (k.y - b2); // 统一不开根号,这样可以提高精度
}
void astar(const Knight& k)
{
    Knight cur, next;
	que.push(k);
	while(!que.empty())
	{
		cur=que.top(); que.pop();
		if(cur.x == b1 && cur.y == b2)
		break;
		for(int i = 0; i < 8; i++)
		{
			next.x = cur.x + dir[i][0];
			next.y = cur.y + dir[i][1];
			if(next.x < 1 || next.x > 1000 || next.y < 1 || next.y > 1000)
			continue;
			if(!moves[next.x][next.y])
			{
				moves[next.x][next.y] = moves[cur.x][cur.y] + 1;

                // 开始计算F
				next.g = cur.g + 5; // 统一不开根号,这样可以提高精度,马走日,1 * 1 + 2 * 2 = 5
                next.h = Heuristic(next);
                next.f = next.g + next.h;
                que.push(next);
			}
		}
	}
}

int main()
{
    int n, a1, a2;
    cin >> n;
    while (n--) {
        cin >> a1 >> a2 >> b1 >> b2;
        memset(moves,0,sizeof(moves));
        Knight start;
        start.x = a1;
        start.y = a2;
        start.g = 0;
        start.h = Heuristic(start);
        start.f = start.g + start.h;
		astar(start);
        while(!que.empty()) que.pop(); // 队列清空
		cout << moves[b1][b2] << endl;
	}
	return 0;
}
  • 时间复杂度是 O(nlogn)
  • 空间复杂度 O(b ^ d)
### Floyd算法代码实现与解析 #### 一、Floyd-Warshall算法简介 Floyd-Warshall算法是一种用于解决加权图中最短路径问题的经典算法。此算法能够在 \(O(V^3)\) 时间复杂度内计算出所有顶点对之间的最短路径,其中 \(V\) 是图中的顶点数[^3]。 #### 二、C语言实现细节 已知邻接矩阵 `G`,存在两个二维辅助数组 `Dist[][]` 和 `Path[][]`: - `Dist[][]` 数组用来存储两顶点之间最短距离; - `Path[][]` 数组则记录了两顶点间最短路径需经过的前一个顶点索引[^4]。 以下是具体的C语言实现方式: ```c #include <stdio.h> #define INF 99999 #define MAX_V 100 void floyd(int graph[MAX_V][MAX_V], int dist[MAX_V][MAX_V], int path[MAX_V][MAX_V], int n){ int i, j, k; // 初始化dist和path for(i = 0; i < n; ++i){ for(j = 0; j < n; ++j){ dist[i][j] = graph[i][j]; if(graph[i][j] != INF && i != j) path[i][j] = i; else path[i][j] = -1; } } // 动态规划更新最短路径 for(k = 0; k < n; ++k){ for(i = 0; i < n; ++i){ for(j = 0; j < n; ++j){ if(dist[i][k] + dist[k][j] < dist[i][j]){ dist[i][j] = dist[i][k] + dist[k][j]; path[i][j] = path[k][j]; } } } } } ``` 这段程序首先初始化了 `dist[][], path[][]` 数组;接着利用三重循环迭代地寻找更优解来优化每一对节点间的最小代价路径长度,并相应调整中间结点的信息[^1]。 #### 三、Python版本实现及其解释 对于希望使用更高层次编程语言的人来说,这里也给出了一版简洁明了的 Python 实现方法: ```python def floyd_warshall(graph): V = len(graph) dist = list(map(lambda i : list(map(lambda j : j , i)) , graph)) next_node = [[None]*V for _ in range(V)] for u in range(V): for v in range(V): if graph[u][v] != float('inf') and u != v: next_node[u][v] = v for k in range(V): for i in range(V): for j in range(V): # 如果通过第k个节点可以得到一条更短路劲,则更新 if dist[i][j] > dist[i][k] + dist[k][j]: dist[i][j] = dist[i][k] + dist[k][j] next_node[i][j] = next_node[i][k] return dist, next_node ``` 上述Python函数接收一个表示带权重有向图的邻接矩阵作为输入参数,并返回两个列表:一个是包含了各点对间最短路径的距离表;另一个则是指示如何构建这些路径的方向指引表。这有助于后续追踪具体哪条路线构成了给定起点至终点的最佳选择。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值