title: 【算法提高课】图论:无向图的双连通分量
katex: true
tags:
- Acwing
- hard
- 图论
categories: 算法提高课
概念
- 桥: 在一个图中,如果断开该边,两边会不连通,该边称为桥
- 边双连通分量(e-DCC): 不含桥的联通区域。不管删掉那条边,图还是联通的。极大的不包含桥的连通块。
- 边双连通(oiwiki): 在一张连通的无向图中,对于两个点 u u u 和 v v v,如果无论删去哪条边(只能删去一条)都不能使它们不连通,我们就说 u u u 和 v v v 边双连通。
- 割点: 如果删掉这个点所相连的边,整个图会变得不连通,该点称为割点。(删掉这个点)
- 点双连通分量(v-DCC): 极大的不包含割点的双连通分量
- 点双连通(oiwiki): 在一张连通的无向图中,对于两个点 u u u 和 v v v,如果无论删去哪个点(只能删去一个,且不能删 u u u 和 v v v 自己)都不能使它们不连通,我们就说 u u u 和 v v v 点双连通。
- 每个割点,至少属于两个联通分量
模板
- tarjan找桥: 将图缩成一个边只有桥的树
dfn[N]
:第一次 dfs 到的时间low[N]
:能到达的最早时间st[N]
:该边是否为桥from
:该点是从哪条边过来的- 关于^:由于存边时候存的是双向边,且正向边和反向边是连续的,从下标 2 开始存,那么一条条无向边是这样存的:{2,3},{4,5}…。而 ^ 有如下性质:如果是一个偶数 ^ 1,那么答案是偶数+1.如果是一个奇数 ^ 1,那么答案是奇数-1。第一次调用 tarjan from 传入一个负数即可。所以一条边 ^1 表示其反向边。
- 算法流程:
如果low[j]>dfn[u]
表示该点连接的下面那个点不能通过其他边连接到当前点,故该边为桥
void tarjan(int u,int from){
dfn[u]=low[u]=++timestamp;
stk.push(u);
for(int i=head[u];i;i=nxt[i]){
int j=to[i];
if(!dfn[j]){
tarjan(j,i);
low[u]=min(low[u],low[j]);
if(low[j]>dfn[u])
st[i]=st[i^1]=true;
}
else if(i!=(from^1)) low[u]=min(low[u],dfn[j]);
}
if(dfn[u]==low[u]){
int t;
dcc_cnt++;
do{
t=stk.top();
stk.pop();
id[t]=dcc_cnt;
}while(t!=u);
}
}
- tarjan找割点和缩成一个个含割点的双连通分量
vector<int>dcc[N]
:存双连通分量的点cut[N]
:是否为割点- 注意存分量的方式和之前都不同,逻辑和上面差不多,多一个等号
void tarjan(int u){
dfn[u]=low[u]=++timestamp;
stk.push(u);
if(u==root&&!head[u]){
dcc[++dcc_cnt].push_back(u);
return;
}
int cnt=0;
for(int i=head[u];i;i=nxt[i]){
int j=to[i];
if(!dfn[j]){
tarjan(j);
low[u]=min(low[u],low[j]);
if(dfn[u]<=low[j]){
cnt++;
if(u!=root||cnt>1) cut[u]=true;
++dcc_cnt;
int y;
do{
y=stk.top();
stk.pop();
dcc[dcc_cnt].push_back(y);
}while(y!=j);
dcc[dcc_cnt].push_back(u);
}
}
else low[u]=min(low[u],dfn[j]);
}
}
Acwing.395 冗余路径
- 题意: 给定 F ( 5000 ) F(5000) F(5000) 个点和 R ( 10000 ) R(10000) R(10000) 条无向边,问至少要加多少条边才能使两个点之间有至少两条路可以互通
- 思路: 使用 tarjan 将图所成一棵树,其中所有边都为桥,统计每个点的入度,如果只有
1
1
1 ,那么说明只能由一条边到达,
cnt++
即可 - C o d e Code Code
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int head[N],idx=1,to[N],nxt[N];
int dcc_cnt,timestamp,dfn[N],low[N],id[N];
stack<int>stk;
int n,m,d[N];
bool st[N];
void add(int u,int v){
to[++idx]=v,nxt[idx]=head[u],head[u]=idx;
}
void tarjan(int u,int from){
dfn[u]=low[u]=++timestamp;
stk.push(u);
for(int i=head[u];i;i=nxt[i]){
int j=to[i];
if(!dfn[j]){
tarjan(j,i);
low[u]=min(low[u],low[j]);
if(low[j]>dfn[u])
st[i]=st[i^1]=true;
}
else if(i!=(from^1)) low[u]=min(low[u],dfn[j]);
}
if(dfn[u]==low[u]){
int t;
dcc_cnt++;
do{
t=stk.top();
stk.pop();
id[t]=dcc_cnt;
}while(t!=u);
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
int a,b;
cin>>a>>b;
add(a,b),add(b,a);
}
tarjan(1,10000);
for(int i=2;i<=idx;i++)
if(st[i])
d[id[to[i]]]++;
int cnt=0;
for(int i=1;i<=dcc_cnt;i++){
if(d[i]==1)
cnt++;
}
cout<<(cnt+1)/2;
return 0;
}
Acwing.1183 电力
- 题意: 给定一个由 n n n 个点 m m m 条边构成的无向图,请你求出该图删除一个点之后,连通块最多有多少。
- 思路: 可能有多个连通块,故需要统计一开始的连通块个数,然后用 tarjan 求出每个联通块里面对于每个割点的连通块个数,主要要特判是否为 root,如果不是 root 则计数加一
- C o d e Code Code
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int head[N],to[N],idx,nxt[N];
int timestamp,dfn[N],low[N];
int n,m,root,ans;
void add(int u,int v){
to[++idx]=v,nxt[idx]=head[u],head[u]=idx;
}
void tarjan(int u){
dfn[u]=low[u]=++timestamp;
int cnt=0;
for(int i=head[u];i;i=nxt[i]){
int j=to[i];
if(!dfn[j]){
tarjan(j);
low[u]=min(low[u],low[j]);
if(dfn[u]<=low[j]) cnt++;
}
else low[u]=min(low[u],dfn[j]);
}
if(u!=root) cnt++;
ans=max(ans,cnt);
}
int main(){
while(cin>>n>>m,n||m){
memset(head,0,sizeof head);
memset(dfn,0,sizeof dfn);
idx=0;
timestamp=0;
ans=0;
for(int i=1;i<=m;i++){
int a,b;
cin>>a>>b;
add(a,b),add(b,a);
}
int cnt=0;
for(root=0;root<n;root++){
if(!dfn[root]){
cnt++;
tarjan(root);
}
}
cout<<ans+cnt-1<<endl;
}
}
Acwing.396 矿场搭建
- 题意: 给定一个无向图(矿场),现要设立一些安全点,使得图中任意某个点被删去时,其他点都能到达一个安全点,求最少需要设置几个安全点,以及不同最少安全点的方案总数
- 思路: 先使用 tarjan 把图缩成一个个双连通分量和一个个割点。
- 首先对于整个图,最少需要两个安全点,因为只有一个安全点可能被删去
- 再分别看每个连通块:①如果无割点,那么两个安全点随便设置,
cnt+=2
,方案数 C 缩点后该连通块的点数 2 C_{缩点后该连通块的点数}^{2} C缩点后该连通块的点数2。②如果有割点的话,那么由于割点的特性:删掉一个割点就会分为很多块,那么对于每一个双连通分量,如果其度数为1,那么就需要在这里放一个安全点,如果大于一,说明可以通过其他路走掉,无需放置安全点。
- C o d e Code Code
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int head[N],to[N],nxt[N],idx;
int dcc_cnt,dfn[N],low[N],timestamp;
bool cut[N];
vector<int>dcc[N];
stack<int>stk;
int root,n,m,T;
void add(int u,int v){
to[++idx]=v,nxt[idx]=head[u],head[u]=idx;
}
void tarjan(int u){
dfn[u]=low[u]=++timestamp;
stk.push(u);
if(u==root&&!head[u]){
dcc[++dcc_cnt].push_back(u);
return;
}
int cnt=0;
for(int i=head[u];i;i=nxt[i]){
int j=to[i];
if(!dfn[j]){
tarjan(j);
low[u]=min(low[u],low[j]);
if(dfn[u]<=low[j]){
cnt++;
if(u!=root||cnt>1) cut[u]=true;
++dcc_cnt;
int y;
do{
y=stk.top();
stk.pop();
dcc[dcc_cnt].push_back(y);
}while(y!=j);
dcc[dcc_cnt].push_back(u);
}
}
else low[u]=min(low[u],dfn[j]);
}
}
int main(){
while(cin>>m,m){
for(int i=1;i<=dcc_cnt;i++) dcc[i].clear();
dcc_cnt=0,idx=0,timestamp=0,n=0;
while(stk.size()) stk.pop();
memset(head,0,sizeof head);
memset(dfn,0,sizeof dfn);
memset(cut,0,sizeof cut);
while(m--){
int a,b;
cin>>a>>b;
n=max(a,n);
n=max(b,n);
add(a,b),add(b,a);
}
for(root=1;root<n;root++)
if(!dfn[root])
tarjan(root);
int res=0;
unsigned long long num=1;
for (int i = 1; i <= dcc_cnt; i ++ )
{
int cnt = 0;
for (int j = 0; j < dcc[i].size(); j ++ )
if (cut[dcc[i][j]])
cnt ++ ;
if (cnt == 0)
{
if (dcc[i].size() > 1) res += 2, num *= dcc[i].size() * (dcc[i].size() - 1) / 2;
else res ++ ;
}
else res ++, num *= dcc[i].size() - 1;
}
printf("Case %d: %d %llu\n", ++T , res, num);
}
return 0;
}