題目
描述
在赤壁之戰中,曹操被諸葛亮和周瑜擊敗。但他不會放棄。曹操的軍隊仍然不善於水戰,所以他提出了另一個想法。他在長江建造了許多島嶼,在這些島嶼的基礎上,曹操的軍隊很容易攻擊周瑜的部隊。曹操還建造了連接島嶼的橋樑。如果所有島嶼都通過橋樑相連,那麼曹操的軍隊可以在這些島嶼中非常方便地部署。周瑜無法忍受,所以他想要摧毀一些曹操的橋樑,這樣一個或多個島嶼就會與其他島嶼分開。但周瑜只有一枚由諸葛亮留下的炸彈,所以他只能摧毀一座橋。周瑜必須派人攜帶炸彈來摧毀這座橋。橋上可能有守衛。轟炸隊的士兵數量不能低於橋樑的守衛數量,否則任務就會失敗。請弄清楚周瑜至少需要多少士兵。
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 |
那麼該怎麼更新呢?
這裏有兩種情況:
- 如果 v 已經在搜索樹上了(也就是 u 的祖先),那麼 Low[u] = min(Dfn[v],Low[u]);
- 如果 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;
}