點雙連通和邊雙連通
- 連通的概念:在無向圖中,所有點能互相到達
- 連通分量:互相聯通的子圖
- 點雙連通:刪掉一個點之後,圖仍聯通
- 邊雙連通:刪掉一條邊之後,圖仍聯通
tarjan 算法:
該算法是R.Tarjan發明的。對圖深度優先搜索, dfn[i]爲第i個結點在搜索樹中的深度,low[i]爲第i個結點的子樹的所有兒子連接到的最上面的結點層數。根據定義,則有:
Low(u)=Min
{
dfn(u),
dfn(v) ,(u,v)爲後向邊(返祖邊) 等價於 DFS(v)<DFS(u)且v不爲u的父親節點
Low(v) ,(u,v)爲樹枝邊(父子邊)
}
一個頂點u是割點,當且僅當滿足(1)或(2)
(1) u爲樹根,且u有多於一個子樹。
(2) u不爲樹根,且滿足存在(u,v)爲樹枝邊(或稱父子邊,即u爲v在搜索樹中的父親),使得DFS(u)<=Low(v)。
一條無向邊(u,v)是橋,當且僅當(u,v)爲樹枝邊,且滿足DFS(u)
求雙連通分量
- 對於點雙連通分量,實際上在求割點的過程中就能順便把每個點雙連通分量求出。建立一個棧,存儲當前雙連通分量,在搜索圖時,每找到一條樹枝邊或後向邊(非橫叉邊),就把這條邊加入棧中。如果遇到某時滿足DFS(u)<=Low(v),說明u是一個割點,同時把邊從棧頂一個個取出,直到遇到了邊(u,v),取出的這些邊與其關聯的點,組成一個點雙連通分支。割點可以屬於多個點雙連通分量,其餘點和每條邊只屬於且屬於一個點雙連通分量支。
(這種還沒有實現過,不過我認爲顯然如果把割點標記出來,跑dfs也能求出點雙連通分支,雖然代碼量會上升,不過還挺好打的,下面例題二類似) - 對於邊雙連通分量,求法更爲簡單。只需在求出所有的橋以後,把橋邊刪除,原圖變成了多個連通塊,則每個連通塊就是一個邊雙連通分量。橋不屬於任何一個邊雙連通分量,其餘的邊和每個頂點都屬於且只屬於一個邊雙連通分量。
構造雙連通圖
- 一個有橋的連通圖,如何把它通過加邊變成邊雙連通圖?方法爲首先求出所有的橋,然後刪除這些橋邊,剩下的每個連通塊都是一個雙連通子圖。把每個雙連通子圖收縮爲一個頂點,再把橋邊加回來,最後的這個圖一定是一棵樹,邊連通度爲1
- 統計出樹中度爲1的節點的個數,即爲葉節點的個數,記爲leaf。則至少在樹上添加(leaf+1)/2條邊,就能使樹達到邊二連通,所以至少添加的邊數就是(leaf+1)/2。具體方法爲,首先把兩個最近公共祖先最遠的兩個葉節點之間連接一條邊,這樣可以把這兩個點到祖先的路徑上所有點收縮到一起,因爲一個形成的環一定是雙連通的。然後再找兩個最近公共祖先最遠的兩個葉節點,這樣一對一對找完,恰好是(leaf+1)/2次,把所有點收縮到了一起。
模板
int root, cnt;
int vis[maxn], dfn[maxn], low[maxn];
bool cut[maxn];
//vector<pair<int, int>>bridge;
void dfs(int u, int fa)
{
int son=0;
vis[u]=1;
dfn[u]=low[u]=++cnt;
for (int i=0; i<G[u].size(); i++)
{
int v=G[u][i];
if (v==fa) continue;
if (vis[v]==1) low[u]=min(low[u], dfn[v]); //返祖邊
if (vis[v]==0)
{
dfs(v, u);
son++;
low[u]=min(low[u], low[v]);
if ( (u==root && son>1) || (u!=root && low[v]>=dfn[u]))
{
cut[u]=true;
//if(low[v] > dfn[u]) bridge.push_back({u, v}); //(u, v) 是橋
}
}
}
vis[u]=2;
}
void tarjan_init()
{
memset(vis, 0, sizeof(vis));
memset(cut, 0, sizeof(cut));
cnt=0; root=1;
//bridge.clear();
}
例題
POJ1144【基礎】
題目大意:
給出一個無向圖,求出有多少個割點。
輸入:
有若干組測試數據。每一組測試數據的第一行有一個整數 n,表示有 n
(1<=n<100)個點,n=0 時測試數據結束。接下來有若干行,每一行第一個整
數 u 表示這一行描述的是以 u 爲起點的邊,接下來有若干個整數 vi 表示有一條
邊 u-vi,u=0 時表示這一組測試數據結束。
輸出:
對於每一組測試數據,輸出一個整數,即有多少個割點。
模板題,沒太多要說的
#include<iostream>
#include<cstdio>
#include<vector>
#include<cstdlib>
#include<cstring>
using namespace std;
typedef long long LL;
const int maxn=110;
vector<int> G[maxn], a;
char s[1000];
void add(int u, int v)
{
G[u].push_back(v);
G[v].push_back(u);
}
void deal()
{
a.clear();
int lens=strlen(s), now=0;
for (int i=0; i<lens; i++)
if (s[i]==' ')
{
a.push_back(now);
now=0;
}else now=now*10+(s[i]-'0');
a.push_back(now);
}
//tarjan
int root, cnt;
int vis[maxn], dfn[maxn], low[maxn];
bool cut[maxn];
//vector<pair<int, int>>bridge;
void dfs(int u, int fa)
{
int son=0;
vis[u]=1;
dfn[u]=low[u]=++cnt;
for (int i=0; i<G[u].size(); i++)
{
int v=G[u][i];
if (v==fa) continue;
if (vis[v]==1) low[u]=min(low[u], dfn[v]);
if (vis[v]==0)
{
dfs(v, u);
son++;
low[u]=min(low[u], low[v]);
if ( ((u==root) && son>1) || (u!=root && low[v]>=dfn[u]))
{
cut[u]=true;
//if(low[v] > dfn[u]) bridge.push_back({u, v}); //(u, v) 是橋
}
}
}
vis[u]=2;
}
void tarjan_init()
{
memset(vis, 0, sizeof(vis));
memset(cut, 0, sizeof(cut));
cnt=0; root=1;
//bridge.clear();
}
int main()
{
int n;
while (scanf("%d\n", &n)!=EOF)
{
if (n==0) break;
for (int i=1; i<=n; i++) G[i].clear();
int u, v;
while (1)
{
scanf("%[^\n]s", s); getchar();
deal();
for (int i=1; i<a.size(); i++) add(a[0], a[i]);
if (a[0]==0) break;
}
tarjan_init();
dfs(1, -1);
int ans=0;
for (int i=1; i<=n; i++)
if (cut[i]) ans++;
printf("%d\n", ans);
}
return 0;
}
POJ1523
題目大意:
給出一個無向圖,求出其割點的數量,並且求出去掉每一個割點後原圖分成多
少個連通分量。
輸入:
有若干組測試數據。每一組測試數據有若干行,每一行有兩個整數表示無向圖
中的一條邊,或者一個 0 表示這一組測試數據的結束。所有測試數據最後以一
個 0 作爲結束。無向圖中的點的數量不超過 1000 個,假定一個圖裏面的點從 1
開始編號,並且是連續遞增的。
輸出:
對於每一組測試數據,如果有割點,按如下格式輸出:
Network #測試數據組序號
SPF node 割點 1 編號 leaves x1 subnets
SPF node 割點 2 編號 leaves x2 subnets
……
其中 xi 表示在圖中去掉某一個割點後產生的連通分量數量。
如果沒有割點,輸出:
Network #測試數據組序號
No SPF nodes
兩組輸出之間需要有一個空行進行分割。
題解:
先求割點,然後枚舉每一個割點裸 dfs 求連通分量數量。
#include<iostream>
#include<cstdio>
#include<vector>
#include<cstdlib>
#include<cstring>
using namespace std;
typedef long long LL;
const int maxn=1010;
vector<int> G[maxn+10];
void add(int u, int v)
{
G[u].push_back(v);
G[v].push_back(u);
}
//tarjan
int root, cnt;
int vis[maxn], dfn[maxn], low[maxn];
bool cut[maxn];
//vector<pair<int, int>>bridge;
void dfs(int u, int fa)
{
int son=0;
vis[u]=1;
dfn[u]=low[u]=++cnt;
for (int i=0; i<G[u].size(); i++)
{
int v=G[u][i];
if (v==fa) continue;
if (vis[v]==1) low[u]=min(low[u], dfn[v]);
if (vis[v]==0)
{
dfs(v, u);
son++;
low[u]=min(low[u], low[v]);
if ( ((u==root) && son>1) || (u!=root && low[v]>=dfn[u]))
{
cut[u]=true;
//if(low[v] > dfn[u]) bridge.push_back({u, v}); //(u, v) 是橋
}
}
}
vis[u]=2;
}
void tarjan_init()
{
memset(vis, 0, sizeof(vis));
memset(cut, 0, sizeof(cut));
cnt=0; root=1;
//bridge.clear();
}
int n;
int belong[maxn], cnt2;
void dfs2(int u, int fa, int x)
{
if (belong[u]!=-1) return;
belong[u]=cnt2;
for (int i=0; i<(int)G[u].size(); i++)
{
int v=G[u][i];
if (v==fa || v==x) continue;
dfs2(v, u, x);
}
}
void deal(int x)
{
memset(belong, -1, sizeof(belong));
cnt2=0;
for (int i=1; i<=n; i++)
if (i!=x && belong[i]==-1) ++cnt2, dfs2(i, -1, x);
}
int main()
{
int a, b, kase=0;
while (scanf("%d", &a)!=EOF)
{
if (a==0) break;
kase++;
printf("Network #%d\n", kase);
for (int i=1; i<=1000; i++) G[i].clear();
scanf("%d", &b);
n=max(a, b);
add(a, b);
int u, v;
while (scanf("%d", &u)!=EOF)
{
if (u==0) break;
scanf("%d", &v);
add(u, v);
n=max(n, u); n=max(n, v);
}
tarjan_init();
dfs(1, -1);
bool ans=false;
for (int i=1; i<=n; i++) if (cut[i]) ans=true;
if (ans==false)
printf(" No SPF nodes\n");
else
for (int i=1; i<=n; i++)
{
deal(i);
if (cut[i]) printf(" SPF node %d leaves %d subnets\n", i, cnt2);
}
printf("\n");
}
return 0;
}
POJ3694
題目大意:
給出一個無向圖,有 N 個點和 M 條邊( 1<=N<=100000, N-1<=M<=200000)求出
有多少條割邊(橋),以及每加入一條新的邊以後還剩下多少條割邊。假定所
有的點從 1 到 N 進行編號,一開始的時候全圖是連通的。
輸入:
有若干組測試數據。每一組測試數據的第一行有兩個整數 N 和 M,當 N=M=0 時
輸入數據結束。接下來有 M 行,每一行有兩個整數 u, v,表示在 u, v 之間有
一條邊。接下來有一行,包含一個整數 Q,表示將加入多少條邊。接下來有 Q
行,每一行按上述相同方式描述一條邊。
輸出:
對於每一組測試數據,先輸出一行“ Case %測試數據組編號%:”,然後對每一
條新加入的邊,輸出一行,包含一個整數,即加入了這一條邊以後還有多少條
割邊。
問題分析:
如下圖,如若加了綠邊,帶來的影響是從綠邊(u,v)往上的節點,直到lca(u,v)都不再是割點
用並查集處理即可,這裏的lca不用寫O(log n)的版本,O(n)的即可
#include<iostream>
#include<cstdio>
#include<vector>
#include<cstdlib>
#include<cstring>
using namespace std;
typedef long long LL;
const int maxn=100100;
vector<int> G[maxn+10];
void add(int u, int v)
{
G[u].push_back(v);
G[v].push_back(u);
}
//tarjan
int root, cnt, ans;
int vis[maxn], dfn[maxn], low[maxn];
bool bridge[maxn];
int parent[maxn];
void dfs(int u, int fa)
{
int son=0;
vis[u]=1;
dfn[u]=low[u]=++cnt;
for (int i=0; i<(int)G[u].size(); i++)
{
int v=G[u][i];
if (v==fa) continue;
if (vis[v]==1) low[u]=min(low[u], dfn[v]);
if (vis[v]==0)
{
dfs(v, u);
parent[v]=u;
son++;
low[u]=min(low[u], low[v]);
if ( (u==root && son>1) || (u!=root && low[v]>=dfn[u]) )
{
ans++;
bridge[v]=1;
}
}
}
vis[u]=2;
}
void tarjan_init()
{
memset(vis, 0, sizeof(vis));
memset(bridge, 0, sizeof(bridge));
cnt=0; root=1; ans=0;
}
void lca(int u, int v)
{
if (dfn[v]<dfn[u]) swap(u, v);
while (dfn[v]>dfn[u])
{
if(bridge[v]) {
ans--;
bridge[v] = 0;
}
v=parent[v];
}
while(u!= v)
{
if (bridge[u]) {
ans--;
bridge[u]=0;
}
u=parent[u]; v=parent[v];
}
}
int main()
{
int n, m, kase=0;
while (scanf("%d%d", &n, &m)!=EOF)
{
if (n==0 && m==0) break;
printf("Case %d:\n", ++kase);
for (int i=1; i<=m; i++)
{
int u, v;
scanf("%d%d", &u, &v);
add(u, v);
}
tarjan_init();
dfs(1, -1);
int Q;
scanf("%d", &Q);
while (Q--)
{
int u, v;
scanf("%d%d", &u, &v);
lca(u, v);
printf("%d\n", ans);
}
printf("\n");
}
return 0;
}