PAT Advanced 1003 Emergency 詳解
題目與翻譯
1003 Emergency 緊急情況 (25分)
As an emergency rescue team leader of a city, you are given a special map of your country. The map shows several scattered cities connected by some roads. Amount of rescue teams in each city and the length of each road between any pair of cities are marked on the map. When there is an emergency call to you from some other city, your job is to lead your men to the place as quickly as possible, and at the mean time, call up as many hands on the way as possible.
作為一個城市的緊急救援隊隊長,你會得到一張特殊的國家地圖。這張地圖顯示了由一些道路連線起來的幾個零散的城市。每個城市的救援隊伍數量以及每一對城市之間的道路長度都在地圖上標註出來。當其他城市給你打緊急電話時,你的工作就是帶領你的人儘快趕到那個地方,同時,在路上儘可能多的召集人手。
Input Specification:
輸入規格:
Each input file contains one test case. For each test case, the first line contains 4 positive integers: N (≤500) - the number of cities (and the cities are numbered from 0 to N
每個輸入檔案包含一個測試用例。對於每個測試案例,第一行包含4個正整數: n (≤500)-城市數量(城市數量從0到 n-1) ,m-道路數量,c1和 c2-你目前所在的城市和你必須儲存的城市。下一行包含 n 個整數,其中 i-th 整數表示第 i 城市救援隊的數量。然後是 m 線,每條線描述一條有三個整數 c1,c2和 l 的道路,這三個整數分別是由一條道路連線起來的兩個城市和這條道路的長度。它保證至少存在一條從 c1到 c2的路徑。
Output Specification:
輸出規格:
For each test case, print in one line two numbers: the number of different shortest paths between C1 and C2, and the maximum amount of rescue teams you can possibly gather. All the numbers in a line must be separated by exactly one space, and there is no extra space allowed at the end of a line.
對於每個測試用例,用一行列印兩個數字: c1和 c2之間不同的最短路徑的數量,以及您可能召集的救援隊伍的最大數量。一行中的所有數字必須正好用一個空格隔開,並且在一行的末尾不允許有額外的空格。
Sample Input:
樣本輸入:
5 6 0 2
1 2 1 5 3
0 1 1
0 2 2
0 3 1
1 2 1
2 4 1
3 4 1
Sample Output:
示例輸出:
2 4
理解與演算法
城市,道路,路程長度,這幾個要素很容易聯想到圖。
首先我們來定義需要的資料結構:
- 起始節點到其他各個節點的距離陣列,dis,當還無法到達某個節點時,就將這個節點的距離設定為無窮遠
- 起始節點到其他各個節點的最短路徑的數量陣列,初始值為0,除了到起始節點本身的路徑數量為1
- 起始節點到其他各個節點的能召集的最大救援隊數量
- 起始節點到其他各個節點的訪問陣列,代表是否訪問過這個節點
vector<vector<int>> edge(N, vector<int>(N, INT_MAX));
// 到i頂點的距離
vector<int> dis(N, INT_MAX);
// c1和 c2之間不同的最短路徑的數量
vector<int> roads(N, 0);
// 到i頂點的救援隊的數量
vector<int> teams(N, 0);
// 是否訪問過i頂點
vector<bool> visit(N, false);
因為我們有眾多的節點需要遍歷,所以不可避免地用到迴圈,確定迴圈前我們需要知道迴圈條件,很顯然,每個節點至少要訪問一次,此處的訪問是指訪問它的鄰接連結串列,也就是從該點出發能夠到達的其他節點。
而後我們還要知道如何找到下一個訪問的節點,圖並不是線性的,圖是網狀結構,沒有明確的先後關係,理想狀態中,最佳的訪問順序是拓撲序,當一個節點被訪問之後就拿掉所有的出邊,再去尋找下一個沒有入邊的節點,理由是這樣就不可能出現訪問其他節點導致以前的最短路程出現問題。例如下面這個圖:
如果從C1出發,先走C2,再走到C4之類的節點就會導致C1-C2這條邊不是最短路程,而出現這個問題的原因就是因為直接走C2它的入度並不為0,而是1,因為C3還可以到C2!
但是維護一個入度陣列十分複雜,代價較大,我們可以換一種方式,在遍歷完一個節點之後我們選擇下一個未被訪問過的節點中,路程最近的節點,這個節點有可能不是初始節點的鄰接節點,這樣做的目的就是不可能從起始節點出發經過其他未訪問的節點再到這個節點的路程小於當前的路程!
還是那上面這張圖舉個例子,C1-C3的距離為2,而C1-C2的距離為4,4 > 2是既定事實,不管怎麼走只要經過C2再繞回C3就不可能出現路徑更短的情況!
for (int j = 0; j < N; j++) {
// 找出沒訪問過的頂點中距離最近的點
if (!visit[j] && dis[j] < minn) {
u = j;
minn = dis[j];
}
}
然後我們再來尋找最短路徑的最大數量以及救援隊的數量。很顯然,我們要先找到最短路徑,方法就是用Dijkstra演算法:遍歷當前訪問節點的所有鄰接節點,如果路程比記錄的路程更短,就覆蓋原有資料:
if (dis[u] + edge[u][v] < dis[v]) {
dis[v] = dis[u] + edge[u][v];
// 因為找到了有更短路程的路徑,就用新的路徑的條數覆蓋原有的路徑條數
roads[v] = roads[u];
teams[v] = teams[u] + weight[v];
}
這裡的u為當前訪問節點,v為u的鄰接節點。因為找到了更短的路,所以原本的路徑都失效了,現在有效的路徑是從u觸發到v的路徑,而根據題目的描述,兩個城市之間只有一條路,所以上面的賦值語句實際含義為:
roads[v] = roads[u] * 1;
如果有多條路徑,那可以把1換成路徑的數量。
而當路程等於最短路程時,就相當於新發現了一條路,並且由於每個節點僅僅訪問一次,所以不會有重複的問題,不需要考慮去重。
else if (dis[u] + edge[u][v] == dis[v]) {
// 如果v這個點沒有被訪問過,並且從起始點到v點的距離=從起始點到u再到v的距離
// 那就是說到相同的目的地,路程一樣長,但是走過的路是不一樣的,就把這些不同的路和原本的路徑的數量相加。
roads[v] = roads[v] + roads[u];
if (teams[u] + weight[v] > teams[v]) {
teams[v] = teams[u] + weight[v];
}
}
因為到下個節點v經過的路徑每一條都是完全不同的,所以可以直接相加,並且也是因為倆城市只有一條路,所以實際含義為:
roads[v] = roads[v] + roads[u] * 1;
如果這個最短路徑能夠攜帶的救援隊數量比原來的要多,就更新原來的資料,因為最短路徑都是一樣長的,所以救援隊並不需要考慮歸屬問題,我們只要求數量!
程式碼[1]
#include <iostream>
#include <vector>
#include <limits.h>
using namespace std;
int main() {
//讀取第一行資料
int N, M, C1, C2;
cin >> N >> M >> C1 >> C2;
vector<int> weight(N, 0);
for (int i = 0; i < N; i++) {
cin >> weight[i];
}
vector<vector<int>> edge(N, vector<int>(N, INT_MAX));
// 到i頂點的距離
vector<int> dis(N, INT_MAX);
// c1和 c2之間不同的最短路徑的數量
vector<int> roads(N, 0);
// 到i頂點的救援隊的數量
vector<int> teams(N, 0);
// 是否訪問過i頂點
vector<bool> visit(N, false);
//初始化邊權值表
int c1, c2, L;
for (int i = 0; i < M; i++) {
cin >> c1 >> c2 >> L;
edge[c1][c2] = edge[c2][c1] = L;
}
dis[C1] = 0;
teams[C1] = weight[C1];
roads[C1] = 1;
for (int i = 0; i < N; ++i) {
int u = -1, minn = INT_MAX;
for (int j = 0; j < N; j++) {
// 找出沒訪問過的頂點中距離最近的點
if (!visit[j] && dis[j] < minn) {
u = j;
minn = dis[j];
}
}
if (u == -1) break;
visit[u] = true;
for (int v = 0; v < N; v++) {
// 如果v這個點沒有訪問過,且u到v是有路徑的,那麼就有可能重新整理最短距離。
if (!visit[v] && edge[u][v] != INT_MAX) {
if (dis[u] + edge[u][v] < dis[v]) {
dis[v] = dis[u] + edge[u][v];
// 因為找到了有更短路程的路徑,就用新的路徑的條數覆蓋原有的路徑條數
roads[v] = roads[u];
teams[v] = teams[u] + weight[v];
} else if (dis[u] + edge[u][v] == dis[v]) {
// 如果v這個點沒有被訪問過,並且從起始點到v點的距離=從起始點到u再到v的距離
// 那就是說到相同的目的地,路程一樣長,但是走過的路是不一樣的,就把這些不同的路和原本的路徑的數量相加。
roads[v] = roads[v] + roads[u];
if (teams[u] + weight[v] > teams[v]) {
teams[v] = teams[u] + weight[v];
}
}
}
}
}
cout << roads[C2] << " " << teams[C2] << endl;
return 0;
}