AcWing 340. 通訊線路
分析:
本題的題意是找到一條路徑,邊權最大的k條邊忽略,第k + 1大的邊權就作為該條路徑的代價,求最小代價是多少,換而言之,就是求從起點到終點的所有路徑中第k + 1大的邊權最小是多少。雖然最後寫出來很簡單,但是想到這個思路是相當不容易的。演算法競賽進階指南上給出了動態規劃的思路和二分的思路,本題也可以用分層圖去做。相對於動態規劃和分層圖的思路,二分的解法思維的難度和求解的時間複雜度都是比較低的。
拿到一道陌生的題,沒思路的情況下首先考慮暴力求解的思路,求第k + 1長的邊最小是多少,暴力求解我們只能去列舉所有的路徑,然後依次求出各種路徑中第k + 1長的邊是多少,比較下求出最小的是多少。顯然要想求從起點到終點的所有路徑是很不容易的,而且複雜度也相當高,暴力做法實現起來不容易。
本題的難點有兩個,第一個是想到用二分去求解,第二個是將題目轉化為雙端佇列BFS問題。首先,我們思考下二分一般應用於上面情況下,很簡單,就是給一組數,先判斷下要找的數在不在左半部分,在就在左半部分繼續二分,不在就到右半部分去找。可以用二分取解決的問題要滿足兩個特性,其一是解在一定的範圍內,以便我們確定二分的左右端點;其二是這組資料具有一定的單調性。這種單調性既可以是顯性的,比如有序的陣列,也可以是隱性的,只要知道中間數滿不滿足條件就可以確定下一步查詢的範圍。既然我們不知道這題的解如何求,那麼是否有辦法說給我x元錢,我有沒有辦法確定這麼多錢能否去升級一條路徑呢?肯定是有辦法的。題目給定的L在1到100w間,這就說明本題的解一定在0到100w之間,否則就是無解,輸出-1。解為0的情況是這條路徑上的邊不超過k條,意味著不用花錢就可以升級線路;無解的情況是從起點無法到達終點。既然本題的解有一定的範圍,並且如果x元能夠升級某條路徑,那麼解一定不會超過x,這就是單調性,也就意味著本題可以用二分解決。
接著來談第二個難點,我們如何去確定x元錢能否升級一條線路,給我們一條路徑,我們把這條路徑上的邊權與x意義比較,只要大於x的邊不超過k條就說明可以用不超過x元去升級這條線路。繼續思考會發現,我們不關心這條路徑上的每條邊的具體權值是多少,只關心其與x的大小關係,因此整個圖上的邊就分為兩類,邊權大於x的和不大於x的,大於x的邊我們將其邊權視為1,否則邊權視為0,只要從起點到終點的最短路徑長度不超過k就說明x元升級線路是可行的。邊權只有0和1兩種情況的最短路問題可以用雙端佇列BFS解決,雙端佇列BFS相關的問題題解見AcWing 175 電路維修,如果不想再去看那麼多的文字,我這裡簡單的解釋下雙端佇列BFS的思想。dijkstra演算法的思路是維護一個小根堆,堆頂元素的距離永遠是最小的,然後不斷取出堆頂元素去鬆弛周圍點的距離。而邊權只有01兩種情況的圖我們無需維護一個小根堆,只需要維護一個雙端佇列,保證雙端佇列的隊頭元素離起點的距離永遠是最小的即可,為此,從起點開始我們將起點加入佇列,然後嘗試去鬆弛周圍的點,只要周圍的點還未出隊過,並且可以被鬆弛,就將該點鬆弛後加入佇列中,邊權是0就加入隊頭,邊權是1就加入隊尾。這就是雙端佇列BFS的基本思路。(詳細介紹還是看之前的題解吧)。
整理下本題的求解思路,在0到100w間二分答案,每次二分時通過雙端佇列BFS的方法判斷起點到終點的最短路徑是否不超過mid,是的話就在左半部分繼續二分,否則在右半部分二分,直到找到答案為止。在其中任意一次BFS的過程中,一旦發現BFS完終點離起點的距離還是無窮大。說明終點不可達,直接輸出-1終止程式。
實現程式碼
#include <bits/stdc++.h>
using namespace std;
const int N = 1010; // 1000個點
const int M = 20010; // 10000條,記錄無向邊需要兩倍空間
int idx, h[N], e[M], w[M], ne[M];
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
int n; //點數
int m; //邊數
deque<int> q; //雙端佇列bfs模擬最短路徑
bool st[N]; //記錄是不是在佇列中
int k; //不超過K條電纜,由電話公司免費提供升級服務
int d[N]; //記錄最短距離
bool check(int cost) {
//多次檢查,每次初始化
memset(d, 0x3f, sizeof d);
memset(st, false, sizeof st);
// 1號基站是通訊公司的總站
q.push_front(1);
d[1] = 0;
while (q.size()) {
int u = q.front();
q.pop_front();
//以後這種continue寫法應該優先選擇,因為可以使下面的程式碼減少括號層數
if (st[u]) continue;
st[u] = true;
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
//如果邊權大於二分值,視為1,否則為0,相當於利用最短路求大於cost的邊個數
int dist = d[u] + (w[i] > cost);
if (dist < d[j]) {
d[j] = dist;
//大的靠後
if (w[i] > cost)
q.push_back(j);
else
//小的靠前
q.push_front(j);
}
}
}
//如果按上面的方法計算後,n結點沒有被鬆弛操作修改距離,則表示n不可達
if (d[n] == 0x3f3f3f3f) {
puts("-1"); //不可達,直接輸出-1
exit(0);
}
return d[n] <= k;
}
int main() {
memset(h, -1, sizeof h);
cin >> n >> m >> k;
int a, b, c;
for (int i = 0; i < m; i++) {
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
/*這裡二分的是直接面對答案設問:最少花費
依題意,最少花費其實是所有可能的路徑中,第k+1條邊的花費
如果某條路徑不存在k+1條邊(邊數小於k+1),此時花費為0
同時,任意一條邊的花費不會大於1e6
整理一下,這裡二分列舉的值其實是0 ~ 1e6*/
int l = 0, r = 1e6;
while (l < r) {
int mid = l + r >> 1;
if (check(mid)) // check函式的意義:如果當前花費可以滿足要求,那麼嘗試更小的花費
r = mid;
else
l = mid + 1;
}
printf("%d\n", l);
return 0;
}