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

概念:

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

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

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

點雙連通和邊雙連通

  • 連通的概念:在無向圖中,全部點能互相到達
  • 連通份量:互相聯通的子圖
  • 點雙連通:刪掉一個點以後,圖仍聯通
  • 邊雙連通:刪掉一條邊以後,圖仍聯通

 


 

概述

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

在一個無向圖中,點雙連通的極大子圖稱爲點雙連通份量(簡稱雙連通份量,Biconnected Component,BCC)數據結構

 

性質

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

 

算法

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

(說實話我以爲存點不比存邊難理解和實現啊……下面會解釋)優化

 

理解

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

以下圖:spa

我猜測多是由於割點的定義,此圖中兩個點均不爲割點,因此此圖也屬於BCC?.net

總之作題時注意題面要求,若要求的不含此種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),但會有不少無用操做。我的以爲存點也是個不錯的選擇。

 

模板

#include<cstdio>
#include<cctype>
#include<vector>
using namespace std;
struct edge
{
    int to,pre;
}edges[1000001];
int head[1000001],dfn[1000001],dfs_clock,tot;
int num;//BCC數量 
int stack[1000001],top;//
vector<int>bcc[1000001];
int tarjan(int u,int fa)
{
    int lowu=dfn[u]=++dfs_clock;
    for(int i=head[u];i;i=edges[i].pre)
        if(!dfn[edges[i].to])
        {
            stack[++top]=edges[i].to;//搜索到的點入棧 
            int lowv=tarjan(edges[i].to,u);
            lowu=min(lowu,lowv);
            if(lowv>=dfn[u])//是割點或根 
            {
                num++;
                while(stack[top]!=edges[i].to)//將點出棧直到目標點 
                    bcc[num].push_back(stack[top--]);
                bcc[num].push_back(stack[top--]);//目標點出棧 
                bcc[num].push_back(u);//不要忘了將當前點存入bcc 
            }
        }
        else if(edges[i].to!=fa)
            lowu=min(lowu,dfn[edges[i].to]);
    return lowu;
}
void add(int x,int y)//鄰接表存邊 
{
    edges[++tot].to=y;
    edges[tot].pre=head[x];
    head[x]=tot;
}
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        add(x,y),add(y,x);
    }
    for(int i=1;i<=n;i++)//遍歷n個點tarjan 
        if(!dfn[i])
        {
            stack[top=1]=i;
            tarjan(i,i);
        }
    for(int i=1;i<=num;i++)
    {
        printf("BCC#%d: ",i);
        for(int j=0;j<bcc[i].size();j++)
            printf("%d ",bcc[i][j]);
        printf("\n");
    }
    return 0;
}

 


 

 

簡介

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

相關閱讀:割點和橋

定義

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

在一張連通的無向圖中,對於兩個點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 的代碼以下:

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

 

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、邊、點雙連通份量模板代碼要注意的地方

邊雙連通份量

1.每一個節點的全部兒子遍歷後纔開始計算份量大小,請與點雙連通相區分;

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

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

上述幾點以下:

void DFS(int i,int fd)//fd是父邊 
{
    low[i]=dfn[i]=++dfs_clock;
    vis[i]=1;
    stk[++top]=i;//棧存節點 
    for(int p=last[i];p;p=E[p].pre)
    {
        int j=E[p].to,id=E[p].id;
        if(vis[j])
        {
            if(dfn[j]<dfn[i]&&fd!=id) low[i]=min(low[i],dfn[j]);
            continue;
        }
        DFS1(j,id);
        low[i]=min(low[i],low[j]); 
    }
    
    //全部兒子遍歷完再求 
    if(low[i]==dfn[i])
    {
        cc++;
        int x;
        while(1)
        {
            x=stk[top--];
            belong[x]=cc;
            size[cc]++;
            if(x==i) break;//注意是等於i才跳出,也就是i只能屬於一個邊連通份量 
        }
        maxcc=max(maxcc,size[cc]);
    }
}

 

點雙連通份量

 

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

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

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

上述幾點以下:

void DFS(int i,int fd)//fd是父邊 
{
    low[i]=dfn[i]=++dfs_clock;
    stk[++top]=i;//棧存節點 
    int chd=0;//統計兒子數 
    
    for(int p=last[i];p;p=E[p].pre)
    {
        
        int j=E[p].to,id=E[p].id;
        if(dfn[j])
        {
            if(dfn[j]<dfn[i]&&id!=fd) low[i]=min(low[i],dfn[j]);
            continue;
        }
        
        
        chd++;
        DFS(j,id);
        low[i]=min(low[i],low[j]);
        
        
        if(low[j]>=dfn[i])//遍歷完一個兒子就看是否有連通份量 
        {
            cut[i]=1;//初步判斷i是割頂(還不必定,要看最後的條件) 
            bcc_cnt++;
            bcc[bcc_cnt].push_back(i);//只是把i給存進去,而不存i屬於哪一個份量,由於i是割頂,可能也屬於別的份量 
            int x;
            while(1)
            {
                x=stk[top--];
                bcc[bcc_cnt].push_back(x); 
                if(x==j) break;//注意到j結束 
            }
        }
        
    }
    
    
    if(fd==0&&chd==1) cut[i]=0;//這個結論應該都知道 
}

 

 

【強連通份量】

1、定義

有向圖上的環,不囉嗦,與上面兩種相似,至少爲一個點;

 

2、模板代碼注意的地方

1.每一個點全部兒子遍歷完纔開始求份量;(相似邊雙連通份量)

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

void DFS(int i)
{
    low[i]=dfn[i]=++dfs_clock;
    stk[++top]=i;
    for(int p=last[i];p;p=E[p].pre)
    {
        int j=E[p].v;
        if(dfn[j])
        {
            if(!belong[j]) low[i]=min(low[i],dfn[j]);
            continue;
        }
        
        DFS(j);
        low[i]=min(low[i],low[j]); 
    }
    
    if(dfn[i]==low[i])
    {
        scc++;
        while(1)
        {
            int x=stk[top--];
            belong[x]=scc;
            size[scc]++;
            if(x==i) break;
        }
    }
}

 


 

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

雙連通份量

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.拿到題目要先搞清楚給的是連通圖仍是非連通圖。

 

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)上路徑上的橋,均可以減小,路徑上的點均可以縮成一個點,即合併成一個份量。

 

對於縮點的處理:

  方法一:對於一個份量,能夠設置一個點爲實點,其他的點爲虛點。實點即表明着這個份量的全部信息,虛點雖然屬於這個份量的點,可是卻對他視而不見。咱們要作的,就是在這個份量裏選擇一個點,去表明整個份量。

  方法二:一樣地,咱們也須要爲每個份量選出一個表明,以表示這個份量。與方法一的「視而不見」不一樣的是,方法二對每個點都設置了一個歸屬集合,即表示這個點屬於哪個集合。因爲在處理的過程當中,一個集合可能又會被另外一個集合所包含,因此咱們能夠利用並查集的路徑壓縮,很快地找到一個點的最終所屬集合。

方法一:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <vector>
#include <queue>
#include <stack>
#include <map>
#include <string>
#include <set>
#define ms(a,b) memset((a),(b),sizeof((a)))
using namespace std;
typedef long long LL;
const double EPS = 1e-8;
const int INF = 2e9;
const LL LNF = 2e18;
const int MAXN = 1e5+10;

struct Edge
{
    int to, next;
}edge[MAXN*8];
int tot, head[MAXN];

int index, dfn[MAXN], low[MAXN];
int isbridge[MAXN], sum_bridge;
int fa[MAXN], depth[MAXN];

void addedge(int u, int v)
{
    edge[tot].to = v;
    edge[tot].next = head[u];
    head[u] = tot++;
}

void Tarjan(int u, int pre)
{
    dfn[u] = low[u] = ++index;
    depth[u] = depth[pre] + 1;  //記錄深度
    fa[u] = pre;        //記錄父親結點
    for(int i = head[u]; i!=-1; i = edge[i].next)
    {
        int v = edge[i].to;
        if(v==pre) continue;
        if(!dfn[v])
        {
            Tarjan(v, u);
            low[u] = min(low[u], low[v]);
            if(low[v]>dfn[u])   //isbridge[v]表示在樹中,以v爲兒子結點的邊是否爲橋
                isbridge[v] = 1, sum_bridge++;
        }
        else
            low[u] = min(low[u], dfn[v]);
    }
}

void LCA(int u, int v)
{
    if(depth[u]<depth[v]) swap(u, v);
    while(depth[u]>depth[v])    //深度大的先往上爬。遇到橋,就把它刪去。
    {
        if(isbridge[u]) sum_bridge--, isbridge[u] = 0;
        u = fa[u];
    }
    while(u!=v) //當深度同樣時,一塊兒爬。遇到橋,就把它刪去。
    {
        if(isbridge[u]) sum_bridge--, isbridge[u] = 0;
        u = fa[u];
        if(isbridge[v]) sum_bridge--, isbridge[v] = 0;
        v = fa[v];
    }
}

void init()
{
    tot = 0;
    memset(head, -1, sizeof(head));

    index = 0;
    memset(dfn, 0, sizeof(dfn));
    memset(low, 0, sizeof(low));
    memset(isbridge, 0, sizeof(isbridge));

    sum_bridge = 0;
}

int main()
{
    int n, m, kase = 0;
    while(scanf("%d%d", &n, &m) && (n||m) )
    {
        init();
        for(int i = 1; i<=m; i++)
        {
            int u, v;
            scanf("%d%d", &u, &v);
            addedge(u, v);
            addedge(v, u);
        }

        depth[1] = 0;
        Tarjan(1, 1);
        int q, a, b;
        scanf("%d", &q);
        printf("Case %d:\n", ++kase);
        while(q--)
        {
            scanf("%d%d", &a, &b);
            LCA(a, b);
            printf("%d\n", sum_bridge);
        }
        printf("\n");
    }
}
View Code

方法二:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <vector>
#include <queue>
#include <stack>
#include <map>
#include <string>
#include <set>
#define ms(a,b) memset((a),(b),sizeof((a)))
using namespace std;
typedef long long LL;
const double EPS = 1e-8;
const int INF = 2e9;
const LL LNF = 2e18;
const int MAXN = 1e6+10;

struct Edge
{
    int to, next;
}edge[MAXN], edge0[MAXN];   //edge爲初始圖, edge0爲重建圖
int tot, head[MAXN], tot0, head0[MAXN];

int index, dfn[MAXN], low[MAXN];
int top, Stack[MAXN], instack[MAXN];
int belong[MAXN];
int fa[MAXN], depth[MAXN];  //fa用於重建圖時記錄當前節點的父親節點,depth記錄當前節點的深度
int sum_bridge;

//找到x最終所屬的結合
int find(int x) { return belong[x]==x?x:belong[x]=find(belong[x]); }

void addedge(int u, int v, Edge edge[], int head[], int &tot)
{
    edge[tot].to = v;
    edge[tot].next = head[u];
    head[u] = tot++;
}

void Tarjan(int u, int pre)
{
    dfn[u] = low[u] = ++index;
    Stack[top++] = u;
    instack[u] = true;
    for(int i = head[u]; i!=-1; i = edge[i].next)
    {
        int v = edge[i].to;
        if(v==pre) continue;
        if(!dfn[v])
        {
            Tarjan(v, u);
            low[u] = min(low[u], low[v]);
            if(low[v]>dfn[u]) sum_bridge++;
        }
        else if(instack[v])
            low[u] = min(low[u], dfn[v]);
    }

    if(dfn[u]==low[u])
    {
        int v;
        do
        {
            v = Stack[--top];
            instack[v] = false;
            belong[v] = u;  //把集合的編號設爲聯通份量的第一個點
        }while(v!=u);
    }
}

void build(int u, int pre)
{
    fa[u] = pre;    //記錄父親節點
    depth[u] = depth[pre] + 1;  //記錄深度
    for(int i  = head0[u]; i!=-1; i=edge0[i].next)
        if(edge0[i].to!=pre)    //防止往回走
            build(edge0[i].to, u);
}


int LCA(int u, int v)   //左一步右一步地找LCA
{
    if(u==v) return u;  //由於兩個結點必定有LCA, 因此必定有u==v的時候

    //可能爬一步就爬了幾個深度,由於中間的結點已經往上縮點了
    if(depth[u]<depth[v]) swap(u, v);   //深度大的往上爬
    sum_bridge--;
    int lca = LCA(find(fa[u]), v);
    return belong[u] = lca;     //找到了LCA,在沿路返回的時候把當前節點的所屬集合置爲LCA的所屬集合
}

void init()
{
    tot = tot0 = 0;
    memset(head, -1, sizeof(head));
    memset(head0, -1, sizeof(head0));

    index = top = 0;
    memset(dfn, 0, sizeof(dfn));
    memset(low, 0, sizeof(low));
    memset(instack, 0, sizeof(instack));

    sum_bridge = 0;
}

int main()
{
    int n, m, kase = 0;
    while(scanf("%d%d", &n, &m) && (n||m) )
    {
        init();
        for(int i = 1; i<=m; i++)
        {
            int u, v;
            scanf("%d%d", &u, &v);
            addedge(u, v, edge, head, tot);
            addedge(v, u, edge, head, tot);
        }

        Tarjan(1, 1);
        for(int u = 1; u<=n; u++)   //重建建圖
        for(int i = head[u]; i!=-1; i = edge[i].next)
        {
            int tmpu = find(u);
            int tmpv = find(edge[i].to);
            if(tmpu!=tmpv)
                addedge(tmpu, tmpv, edge0, head0, tot0);
        }

        depth[find(1)] = 0;
        build(find(1), find(1));    //把無根樹轉爲有根樹

        int q, a, b;
        scanf("%d", &q);
        printf("Case %d:\n", ++kase);
        while(q--)
        {
            scanf("%d%d", &a, &b);
            LCA(find(a), find(b));
            printf("%d\n", sum_bridge);
        }
        printf("\n");
    }
}
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上的邊就是題目要的答案了!!!!

#include <stdio.h>
#include <string.h>
#include <algorithm>
#include <stack>
using namespace std;
#define N 100010
#define M 400010

struct edge{
    int v;
    int next;
}Edge[M];//邊的集合

int node[N];//頂點集合
int DFN[N];//節點u搜索的序號(時間戳)
int LOW[N];//u或u的子樹可以追溯到的最先的棧中節點的序號(時間戳)
int fa[N];//上一個節點 
int pre[N];//並查集父親節點 
int n,m;//n:點的個數;m:邊的條數
int cnt_edge;//邊的計數器
int Index;//序號(時間戳)
int ans;//橋的個數 


void init()//初始化,注意不要把n初始爲0 
{
    cnt_edge=0;
    Index=0;
    ans=0;
    memset(Edge,0,sizeof(Edge));
    memset(node,-1,sizeof(node));
    memset(DFN,0,sizeof(DFN));
    memset(LOW,0,sizeof(LOW));
    memset(fa,0,sizeof(fa));
    memset(pre,0,sizeof(pre));
    for(int i=1;i<=n;i++)
    {
        pre[i]=i;
    }
}

int Find(int x)
{
//    while(n!=pre[n])//寫成這樣會出錯
//    {
//        n=pre[n];
//    }
//    return n;
    return pre[x] == x? pre[x]: (pre[x] = Find(pre[x]));
}

int Union(int u,int v)
{
    int uu,vv;
    uu=Find(u);
    vv=Find(v);
    if(vv==uu)
        return 0;
    pre[uu]=vv;
    return 1;
}

void add_edge(int u,int v)//鄰接表存儲
{
    Edge[cnt_edge].next=node[u];
    Edge[cnt_edge].v=v;
    node[u]=cnt_edge++;
}

void tarjan(int u)
{
    DFN[u]=LOW[u]=Index++;
    for(int i=node[u];i!=-1;i=Edge[i].next)
    {
        int v=Edge[i].v;
        if(v==fa[u]) //這個要寫前面 
            continue;
        if(!DFN[v])//若是點v沒被訪問
        {
            fa[v]=u;
            tarjan(v);
            LOW[u]=min(LOW[u],LOW[v]);
            if(LOW[v]>DFN[u])
            {
                ans++;
            }
            else Union(v,u);
        }
        else //if(v!=fa[u]) //若是點v已經被訪問過
            LOW[u]=min(LOW[u],DFN[v]);  
    }
}

void LCA(int u,int v)
{
    if(DFN[v]<DFN[u])
        swap(u,v);
    while(DFN[v]>DFN[u])
    {
        if(Union(v,fa[v]))
            ans--;
        v=fa[v];
    }
    while(v!=u)
    {
        if(Union(u,fa[u]))
            ans--;
        u=fa[u];
    }
}

int main()
{
    //freopen("sample.txt","r",stdin);
    int tot=0;
    while(~scanf("%d %d",&n,&m)&&(m+n))
    {
        init();
        while(m--)
        {
            int u,v;
            scanf("%d %d",&u,&v);
            add_edge(u,v);
            add_edge(v,u);
        }
        fa[1]=1;
        for(int i=1;i<=n;i++)
        {
            if(!DFN[i])
            {
                tarjan(i);
            }
        }
        int q;
        scanf("%d",&q);
        printf("Case %d:\n",++tot);
        while(q--)
        {
            int u,v;
            scanf("%d %d",&u,&v);
            LCA(u,v);
            printf("%d\n",ans);
            
        }
        printf("\n");
    }
    return  0;
}
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。

相關文章
相關標籤/搜索