代码随想录Day 62|Floyd 算法精讲、A \* 算法精讲 (A star算法),题目:97. 小明逛公园、127. 骑士的攻击

提示:DDU,供自己复习使用。欢迎大家前来讨论~

图论part11

Floyd 算法精讲

可以有效地解决多源最短路径问题,即计算图中所有节点对之间的最短路径。

题目:97. 小明逛公园

97. 小明逛公园 (kamacoder.com)

解题思路:

Floyd算法是一种用于解决多源最短路径问题的算法。

  1. 问题背景:传统的最短路径算法如Dijkstra和Bellman-Ford算法都是单源最短路径算法,而Floyd算法可以解决多源最短路径问题。
  2. 算法特点:Floyd算法可以处理边权值为正或负的情况。
  3. 核心思想:Floyd算法基于动态规划,通过逐步增加中间节点集合来迭代计算最短路径。
  4. dp数组定义:使用一个三维数组grid[i][j][k]来表示从节点i到节点j,以[1…k]集合为中间节点的最短距离。
  5. 递推公式
    • 如果路径经过节点k,则grid[i][j][k] = grid[i][k][k - 1] + grid[k][j][k - 1]
    • 如果路径不经过节点k,则grid[i][j][k] = grid[i][j][k - 1]
    • 最终取两者的最小值作为grid[i][j][k]的值。
  6. 初始化grid[i][j][0]初始化为直接连接节点i和j的边的权值,如果没有直接连接,则为无穷大。
  7. 遍历顺序:按照k的值从小到大遍历,每一步都更新grid数组。
  8. 算法步骤
    • 初始化grid数组。
    • 遍历k,对于每个k,更新grid数组。
    • 对于每个i和j,更新grid[i][j][k]的值。
  9. 算法优势:Floyd算法的时间复杂度为O(n^3),适合于节点数量不是特别大的图。
  10. 应用场景:Floyd算法适用于需要计算图中所有节点对之间最短路径的情况。

grid数组是一个三维数组,那么我们初始化的数据在 i 与 j 构成的平层,如图:

img

红色的 底部一层是我们初始化好的数据,注意:从三维角度去看初始化的数据很重要

解题思路总结:

  1. 初始化:创建一个三维数组grid,用于存储任意两个节点间的最短路径。数组的每个元素grid[i][j][k]表示从节点i到节点j经过节点集合[1…k]的最短路径长度。初始时,如果节点i和节点j之间有直接的边,则grid[i][j][0]设为边的权重;如果没有直接的边,则设为一个足够大的数(如10005),表示无穷大。

  2. 递推公式:通过动态规划的方式,逐步更新grid数组。对于每个节点k,更新所有节点i和j之间的最短路径,考虑是否经过节点k。递推公式为:
    [ grid[i][j][k] = \min(grid[i][j][k-1], grid[i][k][k-1] + grid[k][j][k-1]) ]

  3. 遍历顺序:遍历顺序至关重要,k的循环应该在外层,因为k的更新依赖于k-1的结果。i和j的循环可以任意顺序,因为它们不依赖于彼此的更新。

  4. 遍历实现:通过三层循环遍历所有节点,对于每一层k,更新i到j的最短路径。外层循环遍历k,中层循环遍历i,内层循环遍历j。

  5. 举例推导:可以通过打印每一层k的grid数组来理解算法的执行过程,观察如何逐步更新最短路径。

  6. 结果输出:最终,grid[i][j][n]将包含节点i到节点j的最短路径长度。

  7. 注意事项:确保初始化正确,以及遍历顺序正确,否则算法可能无法得到正确的结果。

以上分析完毕,最后代码如下:

#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;
    }
}

A * 算法精讲 (A star算法)

题目:127. 骑士的攻击

127. 骑士的攻击 (kamacoder.com)

问题描述

  • 题目要求在一个1000x1000的地图上,找到从起点(a1, a2)到终点(b1, b2)的最短路径。
  • 地图上可能有障碍物,障碍物的位置由moves数组标记,如果moves[mm][nn]为非零,则表示该位置有障碍物。

算法选择

  • 广度优先搜索(BFS)是解决此类最短路径问题的经典算法。

解题思路

C++代码实现

cpp

#include<iostream>
#include<queue>
#include<string.h>
using namespace std;

int moves[1001][1001];
int dir[8][2] = {-2, -1, -2, 1, -1, 2, 2, 1, 1, 2, 2, -1, 1, -2, -1, -2};

void bfs(int a1, int a2, int b1, int b2) {
    queue<pair<int, int>> q;
    q.push({a1, a2});
    while (!q.empty()) {
        int m = q.front().first; q.pop();
        int n = q.front().second; q.pop();
        if (m == b1 && n == b2) break;
        for (int i = 0; i < 8; i++) {
            int mm = m + dir[i][0];
            int nn = n + dir[i][1];
            if (mm < 1 || mm > 1000 || nn < 1 || nn > 1000) continue;
            if (!moves[mm][nn]) {
                moves[mm][nn] = moves[m][n] + 1;
                q.push({mm, nn});
            }
        }
    }
}

int main() {
    int n, a1, a2, b1, b2;
    cin >> n;
    while (n--) {
        cin >> a1 >> a2 >> b1 >> b2;
        memset(moves, 0, sizeof(moves));
        bfs(a1, a2, b1, b2);
        cout << moves[b1][b2] << endl;
    }
    return 0;
}
  • 代码在处理大规模数据时超时,因为地图很大,且查询次数可能很多。

解决方案

  • 需要优化算法以减少计算时间。
  • 可以考虑使用启发式搜索(如A*搜索算法)来减少搜索空间。
  • 可以使用记忆化搜索(Memoization)来避免重复计算。
  • 可以考虑使用并查集等数据结构来优化地图的障碍物处理。

总结

  • BFS是解决最短路径问题的有效方法,但在大规模数据下可能会超时。
  • 需要考虑算法优化和数据结构的选择来提高效率。

Astar

Astar 是一种 广搜的改良版。 有的是 Astar是 dijkstra 的改良版。

其实只是场景不同而已 我们在搜索最短路的时候, 如果是无权图(边的权值都是1) 那就用广搜,代码简洁,时间效率和 dijkstra 差不多 (具体要取决于图的稠密)

如果是有权图(边有不同的权值),优先考虑 dijkstra。

而 Astar 关键在于 启发式函数, 也就是 影响 广搜或者 dijkstra 从 容器(队列)里取元素的优先顺序。

以下,我用BFS版本的A * 来进行讲解。

在BFS中,我们想搜索,从起点到终点的最短路径,要一层一层去遍历。

img

如果 使用A * 的话,其搜索过程是这样的,如图,图中着色的都是我们要遍历的点。

img

(上面两图中 最短路长度都是8,只是走的方式不同而已)

大家可以发现 BFS 是没有目的性的 一圈一圈去搜索, 而 A * 是有方向性的去搜索

看出 A * 可以节省很多没有必要的遍历步骤。

为了让大家可以明显看到区别,我将 BFS 和 A * 制作成可视化动图,大家可以自己看看动图,效果更好。

地址:https://kamacoder.com/tools/knight.html

那么 A * 为什么可以有方向性的去搜索,它的如何知道方向呢?

其关键在于 启发式函数

那么启发式函数落实到代码处,如果指引搜索的方向?

在本篇开篇中给出了BFS代码,指引 搜索的方向的关键代码在这里:

int m=q.front();q.pop();
int n=q.front();q.pop();

从队列里取出什么元素,接下来就是从哪里开始搜索。

所以 启发式函数 要影响的就是队列里元素的排序

这是影响BFS搜索方向的关键。

对队列里节点进行排序,就需要给每一个节点权值,如何计算权值呢?

每个节点的权值为F,给出公式为:F = G + H

G:起点达到目前遍历节点的距离

F:目前遍历的节点到达终点的距离

起点达到目前遍历节点的距离 + 目前遍历的节点到达终点的距离 就是起点到达终点的距离。

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

  1. 曼哈顿距离,计算方式: d = abs(x1-x2)+abs(y1-y2)
  2. 欧氏距离(欧拉距离) ,计算方式:d = sqrt( (x1-x2)^2 + (y1-y2)^2 )
  3. 切比雪夫距离,计算方式:d = max(abs(x1 - x2), abs(y1 - y2))

x1, x2 为起点坐标,y1, y2 为终点坐标 ,abs 为求绝对值,sqrt 为求开根号,

选择哪一种距离计算方式 也会导致 A * 算法的结果不同。

本题,采用欧拉距离才能最大程度体现 点与点之间的距离。

所以 使用欧拉距离计算 和 广搜搜出来的最短路的节点数是一样的。 (路径可能不同,但路径上的节点数是相同的)

我在制作动画演示的过程中,分别给出了曼哈顿、欧拉以及契比雪夫 三种计算方式下,A * 算法的寻路过程,大家可以自己看看看其区别。

动画地址:https://kamacoder.com/tools/knight.html

计算出来 F 之后,按照 F 的 大小,来选去出队列的节点。

可以使用 优先级队列 帮我们排好序,每次出队列,就是F最小的节点。

实现代码如下:(启发式函数 采用 欧拉距离计算方式)

#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;
}

A算法是一种启发式搜索算法,它结合了最佳优先搜索和Dijkstra算法的优点,用于寻找两点间的最短路径。以下是对A算法复杂度分析的拓展:

时间复杂度

  • 最坏情况:A算法的时间复杂度为(O(b^d)),其中(b)是每个节点的邻居节点数(在网格图中通常是4或8),(d)是从起点到终点的路径长度。在最坏情况下,A算法需要探索所有可能的路径才能找到最短路径。
  • 最佳情况:如果启发式函数能够完美地预测剩余距离,那么A*算法的时间复杂度可以降低到(O(d)),即路径长度。
  • 平均情况:通常认为A*算法的时间复杂度为(O(n\log n)),这里(n)是节点数量。这个估计是基于启发式函数通常能够减少搜索空间的大小,并且搜索过程中的堆排序操作导致的。

空间复杂度

  • A*算法的空间复杂度为(O(b^d)),其中(b)是每个节点的邻居节点数,(d)是从起点到终点的路径长度。这是因为算法需要存储所有已经探索过的节点。

启发式函数

  • 启发式函数的选择对A*算法的性能有重大影响。一个好的启发式函数可以减少搜索空间,从而减少计算时间。
  • 常用的启发式函数包括曼哈顿距离(Manhattan distance)和欧几里得距离(Euclidean distance),它们分别适用于不同规则的网格图。

优化

  • 使用优先队列:A*算法通常使用优先队列来存储待探索的节点,这样可以快速找到具有最低(f(n) = g(n) + h(n))值的节点,其中(g(n))是从起点到当前节点的实际代价,(h(n))是启发式估计的从当前节点到终点的代价。
  • 内存优化:在实际应用中,可以通过实现迭代加深或使用内存池等技术来减少A*算法的空间复杂度。

小结

A算法是一种非常有效的路径搜索算法,其性能依赖于启发式函数的设计。在最坏情况下,它可能退化为广度优先搜索,但在最佳情况下,它可以非常高效地找到最短路径。实际应用中,A算法通常介于这两种极端情况之间,其性能通常优于Dijkstra算法和广度优先搜索。

最短路算法总结篇

  • dijkstra朴素版
  • dijkstra堆优化版
  • Bellman_ford
  • Bellman_ford 队列优化算法(又名SPFA)
  • bellman_ford 算法判断负权回路
  • bellman_ford之单源有限最短路
  • Floyd 算法精讲
  • 启发式搜索:A * 算法

这段文字提供了一个关于最短路径算法选择的指导,以及它们各自的使用场景。以下是对这段文字的整理:

最短路径算法使用场景分析

  1. Dijkstra算法

    • 适用场景:单源最短路径问题,所有边的权重必须为正数。
    • 选择版本:图的稠密度高时,使用堆优化版;稠密度低时,朴素版或堆优化版均可。
    • 一般推荐:直接使用堆优化版。
  2. Bellman-Ford算法

    • 适用场景:单源最短路径问题,边的权重可以为负数,但不能有负权回路。
    • 选择版本:图的稠密度高时,使用SPFA;稠密度低时,Bellman-Ford或SPFA均可。
    • 一般推荐:直接使用SPFA。
  3. Floyd算法

    • 适用场景:多源最短路径问题,可以处理边权重为正或负的情况。
    • 一般推荐:直接使用Floyd算法。
  4. A*算法

    • 适用场景:启发式搜索,适用于路径规划问题,如游戏开发、地图导航、数据包路由等。
    • 特点:高效,但结果可能不是最短路径,而是近似解。

算法特性总结

  • Dijkstra算法:适用于边权重全为正的情况。
  • Bellman-Ford算法:适用于边权重可以为负的情况,可以检测负权回路。
  • SPFA算法:Bellman-Ford算法的优化版本,适用于稠密图。
  • Floyd算法:适用于多源点最短路径问题,可以处理边权重为正或负的情况。

算法选择建议

  • 单源正权:使用Dijkstra算法。
  • 单源负权:使用Bellman-Ford算法或SPFA算法。
  • 多源点:使用Floyd算法。
  • 路径规划:使用A*算法。

注意事项

  • 图的稠密度:没有明确的量化标准,可以通过实验测试来确定使用哪种版本。
  • 负权回路:如果存在负权回路,优先使用Bellman-Ford算法。
  • 有限节点最短路:如果节点数量有限,优先使用Bellman-Ford算法,因为代码实现简单。
  • 算法题:A*算法由于结果的不唯一性,一般不适合作为算法题。
img

图论总结

图论算法和数据结构

  1. 图的存储方式

    • 邻接表和邻接矩阵是图的两种基本存储方式。
  2. 深度优先搜索(DFS)和广度优先搜索(BFS)

    • 搜索方式:DFS深入搜索,BFS逐层扩展。
    • 代码模板:掌握DFS和BFS的基本代码实现。
    • 应用场景:根据问题选择合适的搜索方法。
  3. DFS和BFS的注意事项

    • DFS可能需要回溯,特别是在需要计算路径的问题中。
    • BFS需要在加入队列时标记节点,避免重复访问。
  4. 并查集

    • 用途:用于处理一些不交集的合并及查询问题。
    • 原理:通过父指针和路径压缩来优化查询和合并操作。
    • 应用:例如判断图是否为树,解决冗余连接问题。
  5. 最小生成树

    • Prim算法:适用于稠密图,维护节点集合。
    • Kruskal算法:适用于稀疏图,维护边集合。
    • 选择依据:根据图的稠密度选择算法。
  6. 拓扑排序

    • 定义:对有向图的节点进行线性排序,使得所有有向边都从排序前的节点指向排序后的节点。
    • 应用场景:如大学排课、文件下载依赖等。
    • 过程:找到入度为0的节点,加入结果集并从图中移除。
  7. 最短路径算法

    • 包括Dijkstra算法、Bellman-Ford算法、Floyd算法等,各有其适用场景。

总结

  • 图论是算法中的重要部分,涵盖了多种算法和数据结构。
  • 理解各种算法的原理和适用场景对于解决实际问题至关重要。
  • 掌握图的存储方式、DFS/BFS、并查集、最小生成树、拓扑排序和最短路径算法是图论学习的关键。
  • 通过不断回顾和实践,可以更深入地理解图论算法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值