題目 P4568 [JLOI2011]飛行路線 要求找到在最多可以免費乘坐k條航線的情況下,從城市s到城市t的最少花費。這是一個典型的分層圖問題。
分層圖的建模
1. 建立層次
將原圖分成k+1層,表示在0到k次免費乘坐的情況下的狀態。第i層表示已經使用了i次免費乘坐機會的狀態。
2. 建立節點和邊
每層圖的節點和原圖的節點對應,每層之間透過邊相連:
- 同層邊: 從第i層的一個節點到同一層的另一個節點,邊權保持不變。
- 跨層邊: 從第i層的一個節點到下一層(第i+1層)的對應節點,邊權設為0,表示使用一次免費乘坐機會。
這樣,每個節點(i, v)
表示在使用了i次免費乘坐機會時在節點v。
演算法步驟
1. 初始化
建立一個距離陣列dist
,dist[i][j]
表示在使用了i次免費機會後到達節點j的最短距離。初始時,所有值設為無窮大,起點的距離為0。
vector<vector<int>> dist(k + 1, vector<int>(n, INF));
dist[0][s] = 0;
2. 優先佇列
使用優先佇列(最小堆)來儲存待處理的節點,每個節點儲存當前距離、層次和節點編號。
priority_queue<tuple<int, int, int>, vector<tuple<int, int, int>>, greater<tuple<int, int, int>>> pq;
pq.push({0, 0, s});
3. 處理節點
從優先佇列中取出距離最小的節點,對其進行鬆弛操作:
- 同層鬆弛: 更新同一層的鄰接節點的距離。
- 跨層鬆弛: 更新下一層的對應節點的距離。
while (!pq.empty()) {
auto [current_distance, current_layer, current_node] = pq.top();
pq.pop();
// 同層鬆弛
for (const auto& edge : graph[current_node]) {
int next_node = edge.to;
int weight = edge.weight;
if (dist[current_layer][current_node] + weight < dist[current_layer][next_node]) {
dist[current_layer][next_node] = dist[current_layer][current_node] + weight;
pq.push({dist[current_layer][next_node], current_layer, next_node});
}
}
// 跨層鬆弛
if (current_layer < k) {
for (const auto& edge : graph[current_node]) {
int next_node = edge.to;
if (dist[current_layer][current_node] < dist[current_layer + 1][next_node]) {
dist[current_layer + 1][next_node] = dist[current_layer][current_node];
pq.push({dist[current_layer + 1][next_node], current_layer + 1, next_node});
}
}
}
}
4. 找最小值
在最後的所有層次中找到到達終點t的最小值。
int result = INF;
for (int i = 0; i <= k; ++i) {
result = min(result, dist[i][t]);
}
示例程式碼
以下是完整的C++程式碼實現:
#include <iostream>
#include <vector>
#include <queue>
#include <tuple>
#include <limits>
using namespace std;
const int INF = numeric_limits<int>::max();
struct Edge {
int to;
int weight;
};
int main() {
int n, m, k, s, t;
cin >> n >> m >> k >> s >> t;
vector<vector<Edge>> graph(n);
for (int i = 0; i < m; ++i) {
int u, v, w;
cin >> u >> v >> w;
graph[u].push_back({v, w});
graph[v].push_back({u, w});
}
vector<vector<int>> dist(k + 1, vector<int>(n, INF));
dist[0][s] = 0;
priority_queue<tuple<int, int, int>, vector<tuple<int, int, int>>, greater<tuple<int, int, int>>> pq;
pq.push({0, 0, s});
while (!pq.empty()) {
auto [current_distance, current_layer, current_node] = pq.top();
pq.pop();
if (current_distance > dist[current_layer][current_node]) continue;
for (const auto& edge : graph[current_node]) {
int next_node = edge.to;
int weight = edge.weight;
if (dist[current_layer][current_node] + weight < dist[current_layer][next_node]) {
dist[current_layer][next_node] = dist[current_layer][current_node] + weight;
pq.push({dist[current_layer][next_node], current_layer, next_node});
}
}
if (current_layer < k) {
for (const auto& edge : graph[current_node]) {
int next_node = edge.to;
if (dist[current_layer][current_node] < dist[current_layer + 1][next_node]) {
dist[current_layer + 1][next_node] = dist[current_layer][current_node];
pq.push({dist[current_layer + 1][next_node], current_layer + 1, next_node});
}
}
}
}
int result = INF;
for (int i = 0; i <= k; ++i) {
result = min(result, dist[i][t]);
}
cout << result << endl;
return 0;
}
這個程式碼實現了從起點到終點的最短路徑計算,考慮了最多k次的免費乘坐機會.
接下來介紹分層圖
什麼是分層圖Dijkstra演算法?
想象一下,你在一個大房子裡,每一層樓代表不同的階段或狀態。在這個房子裡,你可以從一個房間走到另一個房間,也可以坐電梯到不同的樓層。我們需要找到從一個房間到另一個房間的最短路徑,但有一個特別的優惠:你最多可以免費坐幾次電梯。
分層圖的解釋
-
樓層和房間:
- 房子的每一層樓代表一個狀態,每層樓有很多房間。
- 你在某一層的某個房間,想要去另一層的另一個房間。
-
路徑和電梯:
- 在同一層樓裡走路(普通路徑),每走一步需要花費時間(邊權重)。
- 坐電梯到另一層,可以免費(跨層邊)。
分層圖Dijkstra演算法怎麼做?
1. 初始化
首先,我們要記下每個房間到達的最短時間。開始時,我們不知道去哪裡需要多少時間,所以把每個房間的時間都設為很大很大的數(無窮大)。但是我們知道從自己開始的房間出發時間是0。
2. 使用優先佇列
我們用一個特殊的佇列來幫我們記錄哪些房間需要處理,先處理最容易到達的房間。這個佇列就像一個排序的清單,每次我們從中取出最靠前的房間來處理。
3. 處理房間
每次我們從佇列中取出一個房間,然後看看從這個房間出發,能走到哪些其他房間:
- 同層樓房間:如果你在同一層樓走到另一個房間,就更新這個房間的最短時間。
- 跨層電梯:如果你坐電梯到另一層樓的房間,免費,但使用一次免費機會。
4. 找到最短路徑
最後,當所有房間都處理完,我們找到最小的時間值,就是從起點到終點的最短路徑時間。
示例
假設我們有一個兩層的房子,每層樓有三個房間:
-
樓層0:
- 房間0到房間1的時間是2
- 房間0到房間2的時間是4
- 房間1到房間2的時間是1
-
樓層1:
- 房間0到房間1的時間是3
- 房間1到房間2的時間是1
-
跨層電梯:
- 從樓層0的房間0到樓層1的房間1的時間是0(免費)
我們想從樓層0的房間0到樓層1的房間2,最多可以免費坐一次電梯。我們就可以用分層圖Dijkstra演算法來計算最短時間。
C++程式碼示例
#include <iostream>
#include <vector>
#include <queue>
#include <tuple>
#include <limits>
using namespace std;
const int INF = numeric_limits<int>::max();
struct Edge {
int to;
int weight;
};
int main() {
int n = 3, m = 5, k = 1, s = 0, t = 2;
vector<vector<Edge>> graph(n);
// 圖的邊
graph[0].push_back({1, 2});
graph[0].push_back({2, 4});
graph[1].push_back({2, 1});
graph[0].push_back({1, 1}); // 跨層電梯
vector<vector<int>> dist(k + 1, vector<int>(n, INF));
dist[0][s] = 0;
priority_queue<tuple<int, int, int>, vector<tuple<int, int, int>>, greater<tuple<int, int, int>>> pq;
pq.push({0, 0, s});
while (!pq.empty()) {
auto [current_distance, current_layer, current_node] = pq.top();
pq.pop();
if (current_distance > dist[current_layer][current_node]) continue;
for (const auto& edge : graph[current_node]) {
int next_node = edge.to;
int weight = edge.weight;
if (dist[current_layer][current_node] + weight < dist[current_layer][next_node]) {
dist[current_layer][next_node] = dist[current_layer][current_node] + weight;
pq.push({dist[current_layer][next_node], current_layer, next_node});
}
}
if (current_layer < k) {
for (const auto& edge : graph[current_node]) {
int next_node = edge.to;
if (dist[current_layer][current_node] < dist[current_layer + 1][next_node]) {
dist[current_layer + 1][next_node] = dist[current_layer][current_node];
pq.push({dist[current_layer + 1][next_node], current_layer + 1, next_node});
}
}
}
}
int result = INF;
for (int i = 0; i <= k; ++i) {
result = min(result, dist[i][t]);
}
cout << result << endl;
return 0;
}
透過這個例子,你可以看到我們如何一步一步地找到最短路徑。這種方法在有很多狀態和路徑選擇時非常有用。希望這個解釋能幫助你更好地理解分層圖Dijkstra演算法!