1. 程式人生 > 其它 >AcWing 340. 通訊線路

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;
}