雙連通分量(點-雙連通分量&邊-雙連通分量)


雙連通分量(biconnected component, 簡稱bcc)

概念:

雙連通分量有點雙連通分量和邊雙連通分量兩種。若一個無向圖中的去掉任意一個節點(一條邊)都不會改變此圖的連通性,即不存在割點(橋),則稱作點(邊)雙連通圖。

一個無向圖中的每一個極大點(邊)雙連通子圖稱作此無向圖的點(邊)雙連通分量。求雙連通分量可用Tarjan算法。--百度百科

Tip:先學一下tarjan算法以及求割點割邊的算法之后,再看會比較好理解一些。

點雙連通和邊雙連通

  • 連通的概念:在無向圖中,所有點能互相到達
  • 連通分量:互相聯通的子圖
  • 點雙連通:刪掉一個點之后,圖仍聯通
  • 邊雙連通:刪掉一條邊之后,圖仍聯通

 


 

概述

在一個無向圖中,若任意兩點間至少存在兩條“點不重復”的路徑,則說這個圖是點雙連通的(簡稱雙連通,biconnected)

在一個無向圖中,點雙連通的極大子圖稱為點雙連通分量(簡稱雙連通分量,Biconnected Component,BCC)

 

性質

  1. 任意兩點間至少存在兩條點不重復的路徑等價於圖中刪去任意一個點都不會改變圖的連通性,即BCC中無割點
  2. 若BCC間有公共點,則公共點為原圖的割點
  3. 無向連通圖中割點一定屬於至少兩個BCC,非割點只屬於一個BCC

 

算法

 在Tarjan過程中維護一個棧,每次Tarjan到一個結點就將該結點入棧,回溯時若目標結點low值不小於當前結點dfn值就出棧直到目標結點(目標結點也出棧),將出棧結點和當前結點存入BCC

(說實話我覺得存點不比存邊難理解和實現啊……下面會解釋)

 

理解

首先申明一下,在我找到的BCC資料中,在算法實現中均將兩個點和一條邊構成的圖稱為BCC,此文章也沿用此的規定

如下圖:

我猜想可能是因為割點的定義,此圖中兩個點均不為割點,所以此圖也屬於BCC?

總之做題時注意題面要求,若要求的不含此種BCC則判斷每個BCC的大小即可

 

無向連通圖中割點一定屬於至少兩個BCC,非割點只屬於一個BCC

有了上面的規定我們也不難理解這一條了:割點就算相鄰也會屬於至少兩個BCC;BCC間的交點都是割點,所以非割點只屬於一個BCC

 

到一個結點就將該結點入棧

為什么用棧存儲呢?因為DFS是由上到下的,而分離BCC是自下而上的,需要后進先出的數據結構——棧

 

回溯時若目標結點low值不小於當前結點dfn值就出棧直到目標結點(目標結點也出棧),將出棧結點和當前結點存入BCC

對於每個BCC,它在DFS樹中最先被發現的點一定是割點或DFS樹的樹根

證明:割點是BCC間的交點,故割點在BCC的邊緣,且BCC間通過割點連接,所以BCC在DFS樹中最先被發現的點是割點;特殊情況是對於開始DFS的點屬於的BCC,其最先被發現的點就是DFS樹的樹根

上面的結論等價於每個BCC都在其最先被發現的點(一個割點或樹根)的子樹中

 這樣每發現一個BCC(low[v]>=dfn[u]),就將該子樹出棧,並將該子樹和當前結點(割點或樹根)加入BCC中。上面的操作與此描述等價

(我就是因為這個條件“將子樹出棧”沒理解寫錯了結果調了一晚上poj2942)

 

綜上,存點是不是很好理解?存邊雖然不會涉及重復問題(割點屬於至少兩個BCC),但會有很多無用操作。個人覺得存點也是個不錯的選擇。

 

模板

 1 #include<cstdio>
 2 #include<cctype>
 3 #include<vector>
 4 using namespace std;
 5 struct edge 6 { 7 int to,pre; 8 }edges[1000001]; 9 int head[1000001],dfn[1000001],dfs_clock,tot; 10 int num;//BCC數量 11 int stack[1000001],top;//棧 12 vector<int>bcc[1000001]; 13 int tarjan(int u,int fa) 14 { 15 int lowu=dfn[u]=++dfs_clock; 16 for(int i=head[u];i;i=edges[i].pre) 17 if(!dfn[edges[i].to]) 18  { 19 stack[++top]=edges[i].to;//搜索到的點入棧 20 int lowv=tarjan(edges[i].to,u); 21 lowu=min(lowu,lowv); 22 if(lowv>=dfn[u])//是割點或根 23  { 24 num++; 25 while(stack[top]!=edges[i].to)//將點出棧直到目標點 26 bcc[num].push_back(stack[top--]); 27 bcc[num].push_back(stack[top--]);//目標點出棧 28 bcc[num].push_back(u);//不要忘了將當前點存入bcc 29  } 30  } 31 else if(edges[i].to!=fa) 32 lowu=min(lowu,dfn[edges[i].to]); 33 return lowu; 34 } 35 void add(int x,int y)//鄰接表存邊 36 { 37 edges[++tot].to=y; 38 edges[tot].pre=head[x]; 39 head[x]=tot; 40 } 41 int main() 42 { 43 int n,m; 44 scanf("%d%d",&n,&m); 45 for(int i=1;i<=m;i++) 46  { 47 int x,y; 48 scanf("%d%d",&x,&y); 49  add(x,y),add(y,x); 50  } 51 for(int i=1;i<=n;i++)//遍歷n個點tarjan 52 if(!dfn[i]) 53  { 54 stack[top=1]=i; 55  tarjan(i,i); 56  } 57 for(int i=1;i<=num;i++) 58  { 59 printf("BCC#%d: ",i); 60 for(int j=0;j<bcc[i].size();j++) 61 printf("%d ",bcc[i][j]); 62 printf("\n"); 63  } 64 return 0; 65 }

form:https://www.cnblogs.com/LiHaozhe/p/9527136.html

 


 

 

簡介

在閱讀下列內容之前,請務必了解圖論基礎部分。

相關閱讀:割點和橋

定義

割點和橋更嚴謹的定義參見圖論基礎

在一張連通的無向圖中,對於兩個點u和v,如果無論刪去哪條邊(只能刪去一條)都不能使它們不連通,我們就說u和v邊雙連通 。

在一張連通的無向圖中,對於兩個點u和v,如果無論刪去哪個點(只能刪去一個,且不能刪 和 自己)都不能使它們不連通,我們就說u和v點雙連通 。

邊雙連通具有傳遞性,即,若x,y邊雙連通, y,z邊雙連通,則x,z邊雙連通。

點雙連通  具有傳遞性,反例如下圖, A,B點雙連通, B,C點雙連通,而 A,C點雙連通。

bcc-counterexample.png

DFS

對於一張連通的無向圖,我們可以從任意一點開始 DFS,得到原圖的一棵生成樹(以開始 DFS 的那個點為根),這棵生成樹上的邊稱作 樹邊 ,不在生成樹上的邊稱作 非樹邊 。

由於 DFS 的性質,我們可以保證所有非樹邊連接的兩個點在生成樹上都滿足其中一個是另一個的祖先。

DFS 的代碼如下:

1 void DFS(int p) {
2   visited[p] = true;
3   for (int to : edge[p])
4     if (!visited[to]) DFS(to);
5 }

 

DFS 找橋並判斷邊雙連通

首先,對原圖進行 DFS。

bcc-1.png

如上圖所示,黑色與綠色邊為樹邊,紅色邊為非樹邊。每一條非樹邊連接的兩個點都對應了樹上的一條簡單路徑,我們說這條非樹邊 覆蓋 了這條樹上路徑上所有的邊。綠色的樹邊 至少 被一條非樹邊覆蓋,黑色的樹邊不被 任何 非樹邊覆蓋。

我們如何判斷一條邊是不是橋呢?顯然,非樹邊和綠色的樹邊一定不是橋,黑色的樹邊一定是橋。

如何用算法去實現以上過程呢?首先有一個比較暴力的做法,對於每一條非樹邊,都逐個地將它覆蓋的每一條樹邊置成綠色,這樣的時間復雜度為O(nm) 

怎么優化呢?可以用差分。對於每一條非樹邊,在其樹上深度較小的點處打上 -1 標記,在其樹上深度較大的點處打上 +1 標記。然后O(n)求出每個點的子樹內部的標記之和。對於一個點u,其子樹內部的標記之和等於覆蓋了u和u的父親之間的樹邊的非樹邊數量。若這個值非0,則u和u的父親之間的樹邊不是橋,否則是橋。

用以上的方法O(n+m)求出每條邊分別是否是橋后,兩個點是邊雙連通的,當且僅當它們的樹上路徑中  包含橋。

DFS 找割點並判斷點雙連通

bcc-2.png

如上圖所示,黑色邊為樹邊,紅色邊為非樹邊。每一條非樹邊連接的兩個點都對應了樹上的一條簡單路徑。

考慮一張新圖,新圖中的每一個點對應原圖中的每一條樹邊(在上圖中用藍色點表示)。對於原圖中的每一條非樹邊,將這條非樹邊對應的樹上簡單路徑中的所有邊在新圖中對應的藍點連成一個連通塊(這在上圖中也用藍色的邊體現出來了)。

這樣,一個點不是橋,當且僅當與其相連的所有邊在新圖中對應的藍點都屬於同一個連通塊。兩個點點雙連通,當且僅當它們在原圖的樹上路徑中的所有邊在新圖中對應的藍點都屬於同一個連通塊。

藍點間的連通關系可以用與求邊雙連通時用到的差分類似的方法維護,時間復雜度 

 

 

 


 

【雙連通分量】

一、邊雙連通分量定義

在分量內的任意兩個點總可以找到兩條邊不相同的路徑互相到達。總而言之就是一個圈,正着走反着走都可以相互到達,至少只有一個點。

二、點雙連通分量的定義

參照上面,唯一的不同:任意兩個點可以找到一條點不同的路徑互相到達。也是一個圈,正反走都可以,至少為一個點。

三、邊、點雙連通分量模板代碼要注意的地方

邊雙連通分量:

1.每個節點的所有兒子遍歷后才開始計算分量大小,請與點雙連通相區分;

2.割頂只能屬於一個分量,請與割邊區分;(容易搞混)

3.要注意j是否是i的父節點;

上述幾點如下:

 1 void DFS(int i,int fd)//fd是父邊 
 2 {
 3     low[i]=dfn[i]=++dfs_clock;
 4     vis[i]=1;
 5     stk[++top]=i;//棧存節點 
 6     for(int p=last[i];p;p=E[p].pre)
 7     {
 8         int j=E[p].to,id=E[p].id;
 9         if(vis[j])
10         {
11             if(dfn[j]<dfn[i]&&fd!=id) low[i]=min(low[i],dfn[j]);
12             continue;
13         }
14         DFS1(j,id);
15         low[i]=min(low[i],low[j]); 
16     }
17     
18     //所有兒子遍歷完再求 
19     if(low[i]==dfn[i])
20     {
21         cc++;
22         int x;
23         while(1)
24         {
25             x=stk[top--];
26             belong[x]=cc;
27             size[cc]++;
28             if(x==i) break;//注意是等於i才跳出,也就是i只能屬於一個邊連通分量 
29         }
30         maxcc=max(maxcc,size[cc]);
31     }
32 }

 

點雙連通分量:

 

1.每遍歷一個兒子就計算是否有點連通分量;

2.割頂可以屬於多個連通分量,請注意與割邊區分;

3.當i為根節點時,至少要有兩個兒子才能是割點;

上述幾點如下:

 1 void DFS(int i,int fd)//fd是父邊 
 2 {
 3     low[i]=dfn[i]=++dfs_clock;
 4     stk[++top]=i;//棧存節點 
 5     int chd=0;//統計兒子數 
 6     
 7     for(int p=last[i];p;p=E[p].pre)
 8     {
 9         
10         int j=E[p].to,id=E[p].id;
11         if(dfn[j])
12         {
13             if(dfn[j]<dfn[i]&&id!=fd) low[i]=min(low[i],dfn[j]);
14             continue;
15         }
16         
17         
18         chd++;
19         DFS(j,id);
20         low[i]=min(low[i],low[j]);
21         
22         
23         if(low[j]>=dfn[i])//遍歷完一個兒子就看是否有連通分量 
24         {
25             cut[i]=1;//初步判斷i是割頂(還不一定,要看最后的條件) 
26             bcc_cnt++;
27             bcc[bcc_cnt].push_back(i);//只是把i給存進去,而不存i屬於哪個分量,因為i是割頂,可能也屬於別的分量 
28             int x;
29             while(1)
30             {
31                 x=stk[top--];
32                 bcc[bcc_cnt].push_back(x); 
33                 if(x==j) break;//注意到j結束 
34             }
35         }
36         
37     }
38     
39     
40     if(fd==0&&chd==1) cut[i]=0;//這個結論應該都知道 
41 }

 

 

【強連通分量】

一、定義

有向圖上的環,不啰嗦,與上面兩種類似,至少為一個點;

 

二、模板代碼注意的地方

1.每個點所有兒子遍歷完才開始求分量;(類似邊雙連通分量)

2.每個點只能屬於一個分量;

 1 void DFS(int i)
 2 {
 3     low[i]=dfn[i]=++dfs_clock;
 4     stk[++top]=i;
 5     for(int p=last[i];p;p=E[p].pre)
 6     {
 7         int j=E[p].v;
 8         if(dfn[j])
 9         {
10             if(!belong[j]) low[i]=min(low[i],dfn[j]);
11             continue;
12         }
13         
14         DFS(j);
15         low[i]=min(low[i],low[j]); 
16     }
17     
18     if(dfn[i]==low[i])
19     {
20         scc++;
21         while(1)
22         {
23             int x=stk[top--];
24             belong[x]=scc;
25             size[scc]++;
26             if(x==i) break;
27         }
28     }
29 }

 


 

【強連通分量和雙連通分量常見的模型和問法】

雙連通分量

1.給出的圖是非連通圖,如:

a.有一些點,一些邊,加最少的邊,要使得整個圖變成雙聯通圖。

大致方法:求出所有分量,把每個分量看成一個點,統計每個點的度,有一個度為一則cnt加1,答案為(cnt+1)/2;

b.有一些點,一些邊,問最少多少個點單着。

大致方法:求出所有的分量即可,但要注意不同的題可能有特殊要求(如圓桌騎士要求奇圈,要用到二分圖判定)

c.各種變式問題

 

2.給出的圖是連通圖,如:

a.給定一個起點一個終點,求各種問題是否能實現。

大致方法:求出所有分量,並把每個分量當成點,於是問題得到化簡;

b.給一個圖,然后有大量的離線回答。

大致方法:求出所有分量,再求出上下子樹的信息;

c.各種變式問題;

 

強連通分量

1.給出的是非連通圖,如:

a.有一些點,一些有向邊,求至少加多少邊使任意兩個點可相互到達

大致方法:求出所有的分量,縮點,分別求出出度入度為0的點的數量,取多的為答案;

b.有一些點,一些有向邊,求在這個圖上走一條路最多可以經過多少個點

大致方法:求出所有的分量,縮點,形成一個或多個DAG圖,然后做DAG上的dp

c.有一些點,一些有向邊,給出一些特殊點,求終點是特殊點的最長的一條路

大致方法:求出所有分量,並標記哪些分量有特殊點,然后也是DAG的dp

 

2.給出的是連通圖,比較少,有也比較簡單

總結:

1.遇到非連通圖幾乎可以肯定是要求連通分量,不論是無向還是有向圖;(可以節約大量思考時間)

2.凡是對邊、點的操作,在同一個分量內任意一個點效果相同的,幾乎都是縮點解決問題;再粗暴點,幾乎求了連通分量都要縮點;

3.一定要考慮特殊情況,如整個圖是一個連通分量等(考慮到了就有10-20分);

4.對於雙連通分量要分析是邊還是點雙連通分量;

5.拿到題目要先搞清楚給的是連通圖還是非連通圖。

原文:https://blog.csdn.net/WWWengine/article/details/80616779 

 


 

 

POJ3694 Network

https://vjudge.net/problem/POJ-3694

problem

A network administrator manages a large network. The network consists of N computers and M links between pairs of computers. Any pair of computers are connected directly or indirectly by successive links, so data can be transformed between any two computers. The administrator finds that some links are vital to the network, because failure of any one of them can cause that data can't be transformed between some computers. He call such a link a bridge. He is planning to add some new links one by one to eliminate all bridges.

You are to help the administrator by reporting the number of bridges in the network after each new link is added.

Input

The input consists of multiple test cases. Each test case starts with a line containing two integers N(1 ≤ N ≤ 100,000) and M(N - 1 ≤ M ≤ 200,000).
Each of the following M lines contains two integers A and B ( 1≤ A ≠ B ≤ N), which indicates a link between computer A and B. Computers are numbered from 1 to N. It is guaranteed that any two computers are connected in the initial network.
The next line contains a single integer Q ( 1 ≤ Q ≤ 1,000), which is the number of new links the administrator plans to add to the network one by one.
The i-th line of the following Q lines contains two integer A and B (1 ≤ A ≠ B ≤ N), which is the i-th added new link connecting computer A and B.

The last test case is followed by a line containing two zeros.

Output

For each test case, print a line containing the test case number( beginning with 1) and Q lines, the i-th of which contains a integer indicating the number of bridges in the network after the first i new links are added. Print a blank line after the output for each test case.

Sample Input

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

Sample Output

Case 1:
1
0

Case 2:
2
0

大致翻譯:給你N個點M條邊的無向圖,並且有Q次加邊,問每次加邊之后圖中的橋的數量。

顯然,如果加入的邊的兩個端點在同一個邊雙內,那么橋的數量不變。所以我們先用Tarjan對原圖進行邊雙連通分量縮點得到一棵樹。

接着,對於兩個端點不在一個邊雙的情況,顯然橋的數量減少量等於兩個端點的樹上距離。我們求出樹上距離,然后把兩端點之間的邊標記起來,即邊長由原來的1改成0。每次求樹上距離時就先一個個地往上爬,順便還可以標記邊。時間復雜度為O(M+QN),可以通過本題,但顯然不優。

既然邊長變成0了,我們以后都沒必要再管這些邊了,所以我們可以用縮樹的辦法,用並查集把兩個端點之間的點合並到一個集合中去,然后下次爬到這兩個端點處時直接跳到LCA的位置就好了。

 

題解:

1.利用Tarjan算法,求出每個邊雙聯通分量,並且記錄每個點屬於哪一個分量。

2.將每一個邊雙聯通分量縮成一個點,最終得到一棵樹。而我們想要得到一棵有根樹,怎么辦?其實在執行Tarjan算法的時候,就已經形成了一個有根樹。所以我們只需要在Tarjan算法的基礎上,再記錄每一個點的父節點以及深度就可以了。

3.每次詢問的時候,如果兩個點在同一個分量中,那么他們的相連不會減少橋的個數。如果兩個點在不同的分量中,那么u->LCA(u,v)和v->LCA(u,v)上路徑上的橋,都可以減少,路徑上的點都可以縮成一個點,即合並成一個分量。

 

對於縮點的處理:

  方法一:對於一個分量,可以設置一個點為實點,其余的點為虛點。實點即代表着這個分量的所有信息,虛點雖然屬於這個分量的點,但是卻對他視而不見。我們要做的,就是在這個分量里選擇一個點,去代表整個分量。

  方法二:同樣地,我們也需要為每一個分量選出一個代表,以表示這個分量。與方法一的“視而不見”不同的是,方法二對每一個點都設置了一個歸屬集合,即表示這個點屬於哪一個集合。由於在處理的過程中,一個集合可能又會被另一個集合所包含,所以我們可以利用並查集的路徑壓縮,很快地找到一個點的最終所屬集合。

方法一:

  1 #include <iostream>
  2 #include <cstdio>
  3 #include <cstring>
  4 #include <cmath>
  5 #include <algorithm>
  6 #include <vector>
  7 #include <queue>
  8 #include <stack>
  9 #include <map>
 10 #include <string>
 11 #include <set>
 12 #define ms(a,b) memset((a),(b),sizeof((a)))
 13 using namespace std;
 14 typedef long long LL;
 15 const double EPS = 1e-8;
 16 const int INF = 2e9;
 17 const LL LNF = 2e18;
 18 const int MAXN = 1e5+10;
 19 
 20 struct Edge
 21 {
 22     int to, next;
 23 }edge[MAXN*8];
 24 int tot, head[MAXN];
 25 
 26 int index, dfn[MAXN], low[MAXN];
 27 int isbridge[MAXN], sum_bridge;
 28 int fa[MAXN], depth[MAXN];
 29 
 30 void addedge(int u, int v)
 31 {
 32     edge[tot].to = v;
 33     edge[tot].next = head[u];
 34     head[u] = tot++;
 35 }
 36 
 37 void Tarjan(int u, int pre)
 38 {
 39     dfn[u] = low[u] = ++index;
 40     depth[u] = depth[pre] + 1;  //記錄深度
 41     fa[u] = pre;        //記錄父親結點
 42     for(int i = head[u]; i!=-1; i = edge[i].next)
 43     {
 44         int v = edge[i].to;
 45         if(v==pre) continue;
 46         if(!dfn[v])
 47         {
 48             Tarjan(v, u);
 49             low[u] = min(low[u], low[v]);
 50             if(low[v]>dfn[u])   //isbridge[v]表示在樹中,以v為兒子結點的邊是否為橋
 51                 isbridge[v] = 1, sum_bridge++;
 52         }
 53         else
 54             low[u] = min(low[u], dfn[v]);
 55     }
 56 }
 57 
 58 void LCA(int u, int v)
 59 {
 60     if(depth[u]<depth[v]) swap(u, v);
 61     while(depth[u]>depth[v])    //深度大的先往上爬。遇到橋,就把它刪去。
 62     {
 63         if(isbridge[u]) sum_bridge--, isbridge[u] = 0;
 64         u = fa[u];
 65     }
 66     while(u!=v) //當深度一樣時,一起爬。遇到橋,就把它刪去。
 67     {
 68         if(isbridge[u]) sum_bridge--, isbridge[u] = 0;
 69         u = fa[u];
 70         if(isbridge[v]) sum_bridge--, isbridge[v] = 0;
 71         v = fa[v];
 72     }
 73 }
 74 
 75 void init()
 76 {
 77     tot = 0;
 78     memset(head, -1, sizeof(head));
 79 
 80     index = 0;
 81     memset(dfn, 0, sizeof(dfn));
 82     memset(low, 0, sizeof(low));
 83     memset(isbridge, 0, sizeof(isbridge));
 84 
 85     sum_bridge = 0;
 86 }
 87 
 88 int main()
 89 {
 90     int n, m, kase = 0;
 91     while(scanf("%d%d", &n, &m) && (n||m) )
 92     {
 93         init();
 94         for(int i = 1; i<=m; i++)
 95         {
 96             int u, v;
 97             scanf("%d%d", &u, &v);
 98             addedge(u, v);
 99             addedge(v, u);
100         }
101 
102         depth[1] = 0;
103         Tarjan(1, 1);
104         int q, a, b;
105         scanf("%d", &q);
106         printf("Case %d:\n", ++kase);
107         while(q--)
108         {
109             scanf("%d%d", &a, &b);
110             LCA(a, b);
111             printf("%d\n", sum_bridge);
112         }
113         printf("\n");
114     }
115 }
View Code

方法二:

  1 #include <iostream>
  2 #include <cstdio>
  3 #include <cstring>
  4 #include <cmath>
  5 #include <algorithm>
  6 #include <vector>
  7 #include <queue>
  8 #include <stack>
  9 #include <map>
 10 #include <string>
 11 #include <set>
 12 #define ms(a,b) memset((a),(b),sizeof((a)))
 13 using namespace std;
 14 typedef long long LL;
 15 const double EPS = 1e-8;
 16 const int INF = 2e9;
 17 const LL LNF = 2e18;
 18 const int MAXN = 1e6+10;
 19 
 20 struct Edge
 21 {
 22     int to, next;
 23 }edge[MAXN], edge0[MAXN];   //edge為初始圖, edge0為重建圖
 24 int tot, head[MAXN], tot0, head0[MAXN];
 25 
 26 int index, dfn[MAXN], low[MAXN];
 27 int top, Stack[MAXN], instack[MAXN];
 28 int belong[MAXN];
 29 int fa[MAXN], depth[MAXN];  //fa用於重建圖時記錄當前節點的父親節點,depth記錄當前節點的深度
 30 int sum_bridge;
 31 
 32 //找到x最終所屬的結合
 33 int find(int x) { return belong[x]==x?x:belong[x]=find(belong[x]); }
 34 
 35 void addedge(int u, int v, Edge edge[], int head[], int &tot)
 36 {
 37     edge[tot].to = v;
 38     edge[tot].next = head[u];
 39     head[u] = tot++;
 40 }
 41 
 42 void Tarjan(int u, int pre)
 43 {
 44     dfn[u] = low[u] = ++index;
 45     Stack[top++] = u;
 46     instack[u] = true;
 47     for(int i = head[u]; i!=-1; i = edge[i].next)
 48     {
 49         int v = edge[i].to;
 50         if(v==pre) continue;
 51         if(!dfn[v])
 52         {
 53             Tarjan(v, u);
 54             low[u] = min(low[u], low[v]);
 55             if(low[v]>dfn[u]) sum_bridge++;
 56         }
 57         else if(instack[v])
 58             low[u] = min(low[u], dfn[v]);
 59     }
 60 
 61     if(dfn[u]==low[u])
 62     {
 63         int v;
 64         do
 65         {
 66             v = Stack[--top];
 67             instack[v] = false;
 68             belong[v] = u;  //把集合的編號設為聯通分量的第一個點
 69         }while(v!=u);
 70     }
 71 }
 72 
 73 void build(int u, int pre)
 74 {
 75     fa[u] = pre;    //記錄父親節點
 76     depth[u] = depth[pre] + 1;  //記錄深度
 77     for(int i  = head0[u]; i!=-1; i=edge0[i].next)
 78         if(edge0[i].to!=pre)    //防止往回走
 79             build(edge0[i].to, u);
 80 }
 81 
 82 
 83 int LCA(int u, int v)   //左一步右一步地找LCA
 84 {
 85     if(u==v) return u;  //因為兩個結點一定有LCA, 所以一定有u==v的時候
 86 
 87     //可能爬一步就爬了幾個深度,因為中間的結點已經往上縮點了
 88     if(depth[u]<depth[v]) swap(u, v);   //深度大的往上爬
 89     sum_bridge--;
 90     int lca = LCA(find(fa[u]), v);
 91     return belong[u] = lca;     //找到了LCA,在沿路返回的時候把當前節點的所屬集合置為LCA的所屬集合
 92 }
 93 
 94 void init()
 95 {
 96     tot = tot0 = 0;
 97     memset(head, -1, sizeof(head));
 98     memset(head0, -1, sizeof(head0));
 99 
100     index = top = 0;
101     memset(dfn, 0, sizeof(dfn));
102     memset(low, 0, sizeof(low));
103     memset(instack, 0, sizeof(instack));
104 
105     sum_bridge = 0;
106 }
107 
108 int main()
109 {
110     int n, m, kase = 0;
111     while(scanf("%d%d", &n, &m) && (n||m) )
112     {
113         init();
114         for(int i = 1; i<=m; i++)
115         {
116             int u, v;
117             scanf("%d%d", &u, &v);
118             addedge(u, v, edge, head, tot);
119             addedge(v, u, edge, head, tot);
120         }
121 
122         Tarjan(1, 1);
123         for(int u = 1; u<=n; u++)   //重建建圖
124         for(int i = head[u]; i!=-1; i = edge[i].next)
125         {
126             int tmpu = find(u);
127             int tmpv = find(edge[i].to);
128             if(tmpu!=tmpv)
129                 addedge(tmpu, tmpv, edge0, head0, tot0);
130         }
131 
132         depth[find(1)] = 0;
133         build(find(1), find(1));    //把無根樹轉為有根樹
134 
135         int q, a, b;
136         scanf("%d", &q);
137         printf("Case %d:\n", ++kase);
138         while(q--)
139         {
140             scanf("%d%d", &a, &b);
141             LCA(find(a), find(b));
142             printf("%d\n", sum_bridge);
143         }
144         printf("\n");
145     }
146 }
View Code

 

題目大意:n個點的無向圖 初始化有m條邊

之后q次操作 每次表示在點a與點b間搭建一條邊 輸出對於q次操作 每次剩下的橋的條數

 

初始化可以用tarjan算法求出橋 對於不是割邊的兩個點 就可以算是在一個集合中 這樣用並查集就可以進行縮點

最后生成的就是一棵樹 樹邊就是圖中的所有橋 q次詢問中 每次加邊<u,v> 如果u和v在一個集合中 說明新的邊不會造成影響

如果u和v在兩個集合中 兩個集合間的邊在添加<u,v>后就會失去橋的性質 這樣通過LCA就可以遍歷所有兩個集合間的集合 在加上<u,v>這條邊后 這兩個集合間的集合其實就變成了一個環 也就是可以縮成一個點 在合並集合的過程中 就可以把消失的橋從總和中減去了


之前一直在想為什么要用LCA來做這道題,原來他們縮點之后會形成一棵樹,然后因為已經經過縮點了,所以這些樹上的邊都是橋(終於理解為什么他們說縮點之后的樹邊為橋了),那么如果加入的這條邊是屬於一個縮點的話(縮點里面的點算是一個集合)那么就對原圖中的橋沒有任何影響,但是如果加入的邊是屬於兩個縮點的話,那么就會形成一個環,那么任意刪除這個環里面的一條邊,這棵樹還是互通的。ORZ終於理解了,那么就可以利用LCA的特性去算出到底減少了多少條橋了,因為是最近公共祖先,那么新加入的這條邊的兩個點通過LCA找到對方肯定是走最短的路徑(在樹上走最小的邊)那么就可以得到結果了,總橋數減去走LCA上的邊就是題目要的答案了!!!!

 

  1 #include <stdio.h>
  2 #include <string.h>
  3 #include <algorithm>
  4 #include <stack>
  5 using namespace std;
  6 #define N 100010
  7 #define M 400010
  8 
  9 struct edge{
 10     int v;
 11     int next;
 12 }Edge[M];//邊的集合
 13 
 14 int node[N];//頂點集合
 15 int DFN[N];//節點u搜索的序號(時間戳)
 16 int LOW[N];//u或u的子樹能夠追溯到的最早的棧中節點的序號(時間戳)
 17 int fa[N];//上一個節點 
 18 int pre[N];//並查集父親節點 
 19 int n,m;//n:點的個數;m:邊的條數
 20 int cnt_edge;//邊的計數器
 21 int Index;//序號(時間戳)
 22 int ans;//橋的個數 
 23 
 24 
 25 void init()//初始化,注意不要把n初始為0 
 26 {
 27     cnt_edge=0;
 28     Index=0;
 29     ans=0;
 30     memset(Edge,0,sizeof(Edge));
 31     memset(node,-1,sizeof(node));
 32     memset(DFN,0,sizeof(DFN));
 33     memset(LOW,0,sizeof(LOW));
 34     memset(fa,0,sizeof(fa));
 35     memset(pre,0,sizeof(pre));
 36     for(int i=1;i<=n;i++)
 37     {
 38         pre[i]=i;
 39     }
 40 }
 41 
 42 int Find(int x)
 43 {
 44 //    while(n!=pre[n])//寫成這樣會出錯
 45 //    {
 46 //        n=pre[n];
 47 //    }
 48 //    return n;
 49     return pre[x] == x? pre[x]: (pre[x] = Find(pre[x]));
 50 }
 51 
 52 int Union(int u,int v)
 53 {
 54     int uu,vv;
 55     uu=Find(u);
 56     vv=Find(v);
 57     if(vv==uu)
 58         return 0;
 59     pre[uu]=vv;
 60     return 1;
 61 }
 62 
 63 void add_edge(int u,int v)//鄰接表存儲
 64 {
 65     Edge[cnt_edge].next=node[u];
 66     Edge[cnt_edge].v=v;
 67     node[u]=cnt_edge++;
 68 }
 69 
 70 void tarjan(int u)
 71 {
 72     DFN[u]=LOW[u]=Index++;
 73     for(int i=node[u];i!=-1;i=Edge[i].next)
 74     {
 75         int v=Edge[i].v;
 76         if(v==fa[u]) //這個要寫前面 
 77             continue;
 78         if(!DFN[v])//如果點v沒被訪問
 79         {
 80             fa[v]=u;
 81             tarjan(v);
 82             LOW[u]=min(LOW[u],LOW[v]);
 83             if(LOW[v]>DFN[u])
 84             {
 85                 ans++;
 86             }
 87             else Union(v,u);
 88         }
 89         else //if(v!=fa[u]) //如果點v已經被訪問過
 90             LOW[u]=min(LOW[u],DFN[v]);  
 91     }
 92 }
 93 
 94 void LCA(int u,int v)
 95 {
 96     if(DFN[v]<DFN[u])
 97         swap(u,v);
 98     while(DFN[v]>DFN[u])
 99     {
100         if(Union(v,fa[v]))
101             ans--;
102         v=fa[v];
103     }
104     while(v!=u)
105     {
106         if(Union(u,fa[u]))
107             ans--;
108         u=fa[u];
109     }
110 }
111 
112 int main()
113 {
114     //freopen("sample.txt","r",stdin);
115     int tot=0;
116     while(~scanf("%d %d",&n,&m)&&(m+n))
117     {
118         init();
119         while(m--)
120         {
121             int u,v;
122             scanf("%d %d",&u,&v);
123             add_edge(u,v);
124             add_edge(v,u);
125         }
126         fa[1]=1;
127         for(int i=1;i<=n;i++)
128         {
129             if(!DFN[i])
130             {
131                 tarjan(i);
132             }
133         }
134         int q;
135         scanf("%d",&q);
136         printf("Case %d:\n",++tot);
137         while(q--)
138         {
139             int u,v;
140             scanf("%d %d",&u,&v);
141             LCA(u,v);
142             printf("%d\n",ans);
143             
144         }
145         printf("\n");
146     }
147     return  0;
148 }
View Code

 

 


 

 

【POJ 3177】Redundant Paths(Tarjan求橋、邊雙連通分量)

Description

In order to get from one of the F (1 <= F <= 5,000) grazing fields (which are numbered 1..F) to another field, Bessie and the rest of the herd are forced to cross near the Tree of Rotten Apples. The cows are now tired of often being forced to take a particular path and want to build some new paths so that they will always have a choice of at least two separate routes between any pair of fields. They currently have at least one route between each pair of fields and want to have at least two. Of course, they can only travel on Official Paths when they move from one field to another. 

Given a description of the current set of R (F-1 <= R <= 10,000) paths that each connect exactly two different fields, determine the minimum number of new paths (each of which connects exactly two fields) that must be built so that there are at least two separate routes between any pair of fields. Routes are considered separate if they use none of the same paths, even if they visit the same intermediate field along the way. 

There might already be more than one paths between the same pair of fields, and you may also build a new path that connects the same fields as some other path.

Input

Line 1: Two space-separated integers: F and R 

Lines 2..R+1: Each line contains two space-separated integers which are the fields at the endpoints of some path.

Output

Line 1: A single integer that is the number of new paths that must be built.

Sample Input

7 7
1 2
2 3
3 4
2 5
4 5
5 6
5 7

Sample Output

 2

Hint

Explanation of the sample: 

Check some of the routes: 
1 – 2: 1 –> 2 and 1 –> 6 –> 5 –> 2 
1 – 4: 1 –> 2 –> 3 –> 4 and 1 –> 6 –> 5 –> 4 
3 – 7: 3 –> 4 –> 7 and 3 –> 2 –> 5 –> 7 
Every pair of fields is, in fact, connected by two routes. 

It's possible that adding some other path will also solve the problem (like one from 6 to 7). Adding two paths, however, is the minimum.

[題意][求一個無向圖中還需加入多少條邊能構成一個邊雙連通分量]
【題解】【看起來很厲害的樣子,其實還是先用Tarjan縮點,然后,枚舉每一條邊,看左右兩個端點縮點后是否在同一個點中,如果不在連邊,其實只要更新每點的度即可,最后統計度為1的點的個數ans,由求“加入多少條邊能構成一個邊雙連通分量”的方法可知,答案為(ans+1)/2。

 

 

 

 

 

 

 

 

 

 

 

 

 

 



 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM