c++學習筆記:無向圖的連通性(割邊) —— Tarjan

題目

描述

在赤壁之戰中,曹操被諸葛亮和周瑜擊敗。但他不會放棄。曹操的軍隊仍然不善於水戰,所以他提出了另一個想法。他在長江建造了許多島嶼,在這些島嶼的基礎上,曹操的軍隊很容易攻擊周瑜的部隊。曹操還建造了連接島嶼的橋樑。如果所有島嶼都通過橋樑相連,那麼曹操的軍隊可以在這些島嶼中非常方便地部署。周瑜無法忍受,所以他想要摧毀一些曹操的橋樑,這樣一個或多個島嶼就會與其他島嶼分開。但周瑜只有一枚由諸葛亮留下的炸彈,所以他只能摧毀一座橋。周瑜必須派人攜帶炸彈來摧毀這座橋。橋上可能有守衛。轟炸隊的士兵數量不能低於橋樑的守衛數量,否則任務就會失敗。請弄清楚周瑜至少需要多少士兵。

Input

測試用例不超過12個。
在每個測試用例中:
第一行包含兩個整數N和M,意味着有N個島和M個橋。所有島都從1到N編號。(2 <= N <= 1000,0 <M <= N²)
接下來的M行描述了M個橋。每條線包含三個整數U,V和W,意味着有一個連接島U和島V的橋,並且在該橋上有W守衛。(U≠V且0 <= W <= 10,000)
輸入以N = 0且M = 0結束。
Output
對於每個測試用例,輸出周瑜完成任務所需的最少士兵數量。如果周瑜無法成功,請輸出-1。

Sample Input

3 3
1 2 7
2 3 4
3 1 4
3 2
1 2 7
2 3 4
0 0

Sample Output

-1
4

解析

(注:全文中的 v 均爲 u 的兒子節點)
很明顯是一道關於無向圖的割邊問題。
如何確定割邊?
我們可以借鑑割點中的 Tarjan 算法,利用 Low 與 Dfn 進行判斷。

Low 與 Dfn

Dfn[u]:用一種通俗的話來說,就是 u 在 dfs 中第幾個搜索到的。
Low[u]:不經過搜索樹中的邊可以到達的最早被搜索的節點。

對於 Low 舉個例子:

在這裏插入圖片描述

搜素順序 1 2 3 4
Dfn 1 2 3 4
Low 1 1 1 1

那麼該怎麼更新呢?
這裏有兩種情況:

  1. 如果 v 已經在搜索樹上了(也就是 u 的祖先),那麼 Low[u] = min(Dfn[v],Low[u]);
  2. 如果 v 沒有在當前搜索樹上(也就是還沒有搜到過 v 節點),那麼 Low[u] = min(Low[u],Low[v]);(當然你要先搜索了 v 節點,才能獲得 Low[v] 的值)

割邊

那割邊與 Low 和 Dfn 有什麼關係呢?Low[v] > dfn[u] 又表示什麼呢?
其實 Low[v] > dfn[u] 就表示(u,v)這條邊是割邊
如果Low[v] <= dfn[u] 就說明 v 可以不通過 (u,v)這條邊到達 u 的祖先節點,那麼此時(u,v)這條邊存不存都不影響圖的連通性,所以就不是割點。
反之亦然。
那 Low[v] >= dfn[u] 也正確嗎?
假設現在正在判斷(2,3)這條邊
假如割掉這條邊會發生什麼?
在這裏插入圖片描述
這個圖仍然聯通!
所以割邊並不等於割點!


void Tarjan(int x,int fano){
    Dfn[x] = Low[x] = ++ num;
    for (reg int i = 0;i < G[x].size(); ++ i){
        int v = G[x][i].to,vano = G[x][i].v;
        if ( ! Dfn[v]){
            Tarjan(v,vano);
            Low[x] = min(Low[x],Low[v]);
            if (Dfn[x] < Low[v])
                ans = min(ans,G[x][i].dis);
        }
        else
            Low[x] = min(Low[x],Dfn[v]);
    }
}

判重

注意:這道題有很多重邊。比如這組數據

2 3
1 2 1
1 2 2
2 1 3

(答案:-1)
從1 到 2 有三條路,所以任意切掉一條路,仍然聯通。
(一組水數據)
但當這個圖既存在重邊,又存在割邊,用上面的代碼可以仍然可以嗎?

3 3
1 2 1
2 1 2
2 3 3

它輸出了 -1 !而正確輸出應該是 3!爲什麼?
我們來看看 Low 與 Dfn 的值

編號 1 2 3
Dfn 1 2 3
Low 1 1 2

在這個算法下,ans 是永遠不會更新的。
Low[3] 可以等於 2 嗎?
根據定義,Low 一定是通過不存在與搜索樹上的邊能到達的 Dfn 最小的節點,而 3 被搜到正是因爲(2,3)這條搜索樹上的邊。
所以,我們需要將每條邊都標記一下,然後在判斷 Low[v] 第二種情況時加一個判斷語句 if (fano != vano)就可以讓 Low 不會錯誤更新了。

fano:當前點和父親點的連邊編號。
vano:當前點和兒子點的連邊編號


void Tarjan(int x,int fano){
    Dfn[x] = Low[x] = ++ num;
    for (reg int i = 0;i < G[x].size(); ++ i){
        int v = G[x][i].to,vano = G[x][i].v;
        if ( ! Dfn[v]){
            Tarjan(v,vano);
            Low[x] = min(Low[x],Low[v]);
            if (Dfn[x] < Low[v])
                ans = min(ans,G[x][i].dis);
        }
        else if (Dfn[x] > Dfn[v] && vano != fano)
            Low[x] = min(Low[x],Dfn[v]);
    }
}

這麼一講,其實我們可以直接在 dfs 時直接傳入(u,fa),並將判斷語句改爲 if (v != fa)
仍然可以判斷重邊。因此,知恩圖報的編譯器們,作爲回報,定會讓你樣例全崩。
這麼寫,我們就回到了割點,是對重邊視而不見的一種表現。就以第一組樣例爲例,你只會判斷搜索樹中的那麼一條邊。當曹操睜眼一瞧,才驀然驚醒:原來還有這麼多條路可以走,周瑜炸了這條,我還可以走那條。
所以,我們如果判邊的話,就不存在這種情況了

特判

一道模板題,打上剛纔那段代碼,就以可以過掉千千萬萬的數據,可惜,我們總是離 AC 差三個特判

  • ans = 0x3f3f3f3f,意味着這是一個強聯通圖,輸出 - 1
  • ans = 0,橋上沒兵,但我們仍需派一人去炸橋。輸出 1
  • num < n,更定圖本就不連通,可以直接輸出 0

AC加載中(代碼)

#include <cstdio>
#include <cstring>
#include <vector>
#define M 1005
#define INF 0x3f3f3f3f
#define reg register
using namespace std;

struct node{
    int to,dis,v;
    node(){}
    node(int To,int Dis,int V){
        to = To;
        dis = Dis;
        v = V;
    }
};
int n,m,num,ans;
vector < node > G[M];
int Dfn[M],Low[M];

void Tarjan(int x,int fano){
    Dfn[x] = Low[x] = ++ num;
    for (reg int i = 0;i < G[x].size(); ++ i){
        int v = G[x][i].to,vano = G[x][i].v;
        if ( ! Dfn[v]){
            Tarjan(v,vano);
            Low[x] = min(Low[x],Low[v]);
            if (Dfn[x] < Low[v])
                ans = min(ans,G[x][i].dis);
        }
        else if (vano != fano)
            Low[x] = min(Low[x],Dfn[v]);
    }
}

int main(){
    while ( ~ scanf("%d%d",&n,&m) && n != 0){
        int tot = 0;
        for (reg int i = 1;i <= m; ++ i){
            int a,b,c;
            scanf("%d%d%d",&a,&b,&c);
            G[a].push_back(node(b,c, ++ tot));
            G[b].push_back(node(a,c,tot));
        }
        ans = INF;
        Tarjan(1,0);
        if (ans == 1061109567)
            ans = -1;
        if (ans == 0)
            ans = 1;
        if (num != n)
            ans = 0;
        printf("%d\n",ans);
        for (reg int i = 1;i <= n; ++ i)
            G[i].clear();
        memset(Dfn,0,sizeof(Dfn));
        num = 0;
    }
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章