- Floyd演算法
- 題目:97. 小明逛公園
- A * 演算法
- 題目:126.騎士的攻擊
- 最短路演算法總結
Floyd演算法
Floyd演算法用於求解多源最短路問題(求多個起點到多個終點的多條最短路徑)。在前面學習的dijkstra演算法、Bellman演算法都是求解單源最短路的問題(即只能有一個起點)。
注意:Floyd演算法對邊的權值正負沒有要求,都可以處理。
Floyd的核心思想是動態規劃。
動態規劃的五部曲:
- 確定dp陣列(dp table)以及下標的含義。
- 確定遞推公式。
- dp陣列如何初始化。
- 確定遍歷順序。
- 舉例推導dp陣列。
根據動態規劃的五部曲來解釋Floyd演算法的遍歷過程。
1. 確定dp陣列(dp table)以及下標的含義。
把dp陣列命名為grid陣列,用來來存圖。
grid[i][j][k] = m
表示節點i到節點j中以[i...k]集合為中間節點的最短距離為m。
2. 確定遞推公式。
分兩種情況:
- 節點i到節點j的最短路徑經過節點k。
- 節點i到節點j的最短路徑不經過節點k。
對於第一種情況:grid[i][j][k] = grid[i][k][k - 1] + grid[k][j][k - 1]
。
對於第二種情況:grid[i][j][k] = 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。
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迴圈一定是在最外面,這樣才能一層一層去遍歷。
暫不舉例推導。
題目:97. 小明逛公園
題目連結:https://kamacoder.com/problempage.php?pid=1155
文章講解:https://www.programmercarl.com/kamacoder/0097.小明逛公園.html
題目狀態:看題解
思路:
Floyd演算法
程式碼:
#include <iostream>
#include <vector>
#include <list>
using namespace std;
int main()
{
int n, m, p1, p2, val;
cin >> n >> m;
// 因為邊的最大距離是10^4
vector<vector<vector<int>>> grid(n + 1, vector<vector<int>>(n + 1, vector<int>(n + 1, 10005)));
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;
}
}
空間最佳化:
我們只需要記錄grid[i][j][1]
和grid[i][j][0]
就好,之後就是grid[i][j][1]
和grid[i][j][0]
交替滾動。
#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;
}
}
A * 演算法
Astar是一種廣搜的改良版。有的是Astar是dijkstra的改良版。
其實只是場景不同而已 我們在搜尋最短路的時候,如果是無權圖(邊的權值都是1)那就用廣搜,程式碼簡潔,時間效率和dijkstra差不多(具體要取決於圖的稠密)
如果是有權圖(邊有不同的權值),優先考慮dijkstra。
而Astar關鍵在於啟發式函式, 也就是影響廣搜或者dijkstra從容器(佇列)裡取元素的優先順序。
本部落格中使用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 * 演算法的結果不同。
動態模擬地址:https://kamacoder.com/tools/knight.html
題目:126.騎士的攻擊
題目連結:https://kamacoder.com/problempage.php?pid=1203
文章講解:https://www.programmercarl.com/kamacoder/0126.騎士的攻擊astar.html
題目狀態:看題解
思路:
A*演算法
程式碼:
#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;
}