P4568 [JLOI2011] 飛行路線

luyouling發表於2024-06-09

題目 P4568 [JLOI2011]飛行路線 要求找到在最多可以免費乘坐k條航線的情況下,從城市s到城市t的最少花費。這是一個典型的分層圖問題。

分層圖的建模

1. 建立層次

將原圖分成k+1層,表示在0到k次免費乘坐的情況下的狀態。第i層表示已經使用了i次免費乘坐機會的狀態。

2. 建立節點和邊

每層圖的節點和原圖的節點對應,每層之間透過邊相連:

  • 同層邊: 從第i層的一個節點到同一層的另一個節點,邊權保持不變。
  • 跨層邊: 從第i層的一個節點到下一層(第i+1層)的對應節點,邊權設為0,表示使用一次免費乘坐機會。

這樣,每個節點(i, v)表示在使用了i次免費乘坐機會時在節點v。

演算法步驟

1. 初始化

建立一個距離陣列distdist[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演算法?

想象一下,你在一個大房子裡,每一層樓代表不同的階段或狀態。在這個房子裡,你可以從一個房間走到另一個房間,也可以坐電梯到不同的樓層。我們需要找到從一個房間到另一個房間的最短路徑,但有一個特別的優惠:你最多可以免費坐幾次電梯。

分層圖的解釋

  1. 樓層和房間

    • 房子的每一層樓代表一個狀態,每層樓有很多房間。
    • 你在某一層的某個房間,想要去另一層的另一個房間。
  2. 路徑和電梯

    • 在同一層樓裡走路(普通路徑),每走一步需要花費時間(邊權重)。
    • 坐電梯到另一層,可以免費(跨層邊)。

分層圖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演算法!

相關文章