97. 小明逛公园(Floyd 算法精讲)
感觉就是图论上的dp问题,不过还是理解上遇到一点问题,就是中间这个k代表什么意思,以及为什么需要这个K。
1. 多源最短路径问题:
Floyd算法是解决多源最短路径问题 的经典算法,意味着它可以一次性求出所有节点到所有其他节点的最短路径 ,而不仅仅是一个起点到其他点的最短路径(如Dijkstra算法那样)。因此,Floyd算法是一种全源最短路径算法。
2. Floyd算法的核心思想:
Floyd算法的核心是动态规划。算法通过逐步增加中间节点(或称为"候选节点"),不断更新最短路径的结果。每次,我们通过一个新的中间节点来考虑是否能够通过它使得两点之间的最短路径变得更短。最终,在所有节点都作为中间节点考虑过后,我们可以得到所有节点对之间的最短路径。
具体来说,Floyd算法通过一个三维数组来表示路径:
grid[i][j][k]表示从节点i到节点j,且**中间节点编号不超过k**时的最短路径。
3. 动态规划递推关系:
动态规划的递推公式帮助我们通过已知的子问题解来得到整体问题的解。假设我们已经知道了节点间的最短路径信息,递推的过程如下:
- 如果最短路径 经过节点 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-1]:表示从节点i到节点j,在中间节点集合为[1...k-1]的情况下的最短路径。grid[i][k][k-1] + grid[k][j][k-1]:表示从节点i到节点j的路径经过节点k,此时可以分为两段路径:一段是从i到k,一段是从k到j。
关于为什么是一个三元的dp:
首先起点和终点肯定要有,这就会占去两个维度,但他们之间的递推关系并不明朗,假设我要从i到j,有可能经过3个节点更短,也有可能直接连接更短,难道为了求i到j把所有从i出发的边全都遍历一遍吗?这样显然是不现实的。因此通过额外加入一个维度来实现递推关系,这个递推关系实际上就是一步步扩大中间节点的范围。
-
为什么要有
k:
k是为了逐步引入更多的中间节点,它在动态规划中起到了决定性作用,帮助我们通过考虑越来越多的中间节点来更新最短路径。 -
它的作用是:
k代表着路径中允许使用的中间节点集合。随着k的增加,我们允许路径中使用更多的中间节点,最终使得路径更加优化。通过这种逐步引入中间节点的方法,Floyd算法最终可以计算出所有节点对之间的最短路径。
此外,关于遍历顺序的话其实还可以这样理解:
因为递推公式当中我们需要grid[i][k][k-1] + grid[k][j][k-1],所以在我们遍历的时候一定要保证在访问grid[i][k][k]的时候,grid[i][k][k-1]和grid[k][j][k-1]都已被访问过了。那如果想做到这一点,就只能把k放在最外面才行。
理解上面确实稍微复杂一些,看卡哥解析看了半天,不过写起来相对还是容易的:
cpp
#include<iostream>
#include<vector>
using namespace std;
int main(){
int n,m;
cin >> n >> m;
vector<vector<vector<int>>> grid(n+1, vector<vector<int>>(n+1, vector<int>(n+1, 10001)));
for(int i=0; i<m; i++){
int left, right, val;
cin >> left >> right >> val;
grid[left][right][0] = val;
grid[right][left][0] = val;
}
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 q;
cin >> q;
for(int i=0; i<q; i++){
int left, right;
cin >> left >> right;
if(grid[left][right][n] == 10001){
cout << -1 << endl;
}
else{
cout << grid[left][right][n] <<endl;
}
}
}
127. 骑士的攻击(A * 算法精讲)
这回进化到了看代码也看不太明白的地步了,边问gpt先把代码过一遍:
1 knight结构体(注释版)
cpp
struct Knight{
int x, y; // 骑士当前位置的坐标
int g, h, f; // 用于 A* 算法的参数:
// g: 从起点到当前节点的实际消耗(路径长度)
// h: 当前节点到终点的预估消耗(即启发式函数值,通常使用欧几里得距离)
// f: 总的估计成本,f = g + h
bool operator < (const Knight & k) const { // 重载小于运算符,优先队列会按照 f 值从小到大排序
return k.f < f;
}
};
第一个 const表示函数的参数 k 是一个常量引用,也就是说,k 不能在函数体内被修改。这个修饰符告诉编译器你不会改变 k 对象的状态,避免了不必要的副作用。
第二个 const修饰的是成员函数的声明,表示这个成员函数不会修改它所作用的对象(即 this 指针指向的对象)。通常用于标识该成员函数是"只读"的,不会修改对象的任何成员变量。
2 moves 数组
cpp
int moves[1001][1001];
moves 是一个 1001 × 1001 的二维数组,用来记录每个位置(棋盘上的每个坐标)到达的最短步数。数组的下标表示棋盘上的坐标,值表示到达该位置所需的步数。
moves[x][y]的值表示从起点到位置(x, y)的最短路径所需的步数。- 如果
moves[x][y]为 0,表示该位置尚未访问。 - 如果
moves[x][y]大于 0,表示该位置已经访问,并且记录了从起点到该位置的最短步数。
简单来说是一个棋盘格。
3 为什么要引入<string.h>库函数
#include <string.h> 主要用于提供 C 标准库中的 memset 函数,用于初始化数组。
memset 函数的作用
memset 是一个 C 标准库函数,用于将一块内存区域的内容设置为某个特定的值。在这段代码中,memset(moves, 0, sizeof(moves)) 用来将 moves 数组的所有元素初始化为 0。
具体来说:
cpp
memset(moves, 0, sizeof(moves));
moves:是要初始化的数组(在这段代码中是一个 1001 × 1001 的二维数组)。0:表示将数组的所有元素设置为 0。sizeof(moves):是数组的总大小(单位是字节)。sizeof(moves)会返回moves数组所占的字节数,确保memset会填充整个数组。
为什么要初始化 moves 数组?
在 A* 算法中,moves 数组用于记录每个位置到达的最短步数。每次处理新的查询时,必须清空这个数组,以确保不受之前计算的影响。因此,在每次新的查询开始时,都会调用 memset 将 moves 数组重置为 0,表示所有的节点都未被访问过。
如果不清空 moves 数组,可能会导致之前查询结果的干扰,影响后续查询的正确性。
4 为什么在调用完astar函数之后,还有一句while(!que.empty()) que.pop();?作用是什么?
作用是清空 priority_queue(优先队列)。
为什么需要清空队列?
-
重复使用队列 :每次进入新的循环时,
astar()函数会使用同一个priority_queue。如果你不清空它,队列中将保留之前的计算结果,这会导致程序出现问题或者不符合预期,因为每次运行 A* 算法时,队列的状态应该是空的,新的计算应该从一个空队列开始。 -
防止数据干扰 :
astar()运行时,priority_queue存放的是搜索过程中每个Knight位置的状态。如果不清空它,前一次的计算状态可能干扰到下一次计算,导致结果错误。
5 为什么要把f分成g和h来计算?
一个是当前位置距离成功还有多远,另一个是自己已经走了多远。两个都算上才足以表示当前的状态,只用一个数的话是做不到的。
cpp
#include<iostream>
#include<queue>
#include<string.h>
using namespace std;
int moves[1001][1001];
int dir[8][2] = {-2,-1,-2,1,2,-1,2,1,1,-2,1,2,-1,2,-1,-2};
int b1, b2;
struct Knight{
int x,y;
int g,h,f;
bool operator <(const Knight& k)const{
return k.f < f;
}
};
priority_queue<Knight> que;
int distance(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;
next.g = cur.g + 5;
next.h = distance(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 = distance(start);
start.f = start.g + start.h;
astar(start);
while(!que.empty()) que.pop();
cout << moves[b1][b2] << endl;
}
return 0;
}