无向图的双连通分量
一些定义和性质:
对于一张连通的无向图,如果删除图中的某条边之后原先的图不能成为连通图,那么这条边就被称为 桥
极大的不包含桥的连通块,被称为 边双连通分量(e-dcc)
对于边双连通分量,不管删除哪条边,图依旧是连通的。
类似于桥,如果删除图中的某个点与和他连的边之后原先的图不能成为连通图,那么这个点就被称为
割点
极大的不包含割点的连通块,被称为 点双连通分量(v-dcc)
对于一个连通块 u u u ,如果不存在另一个连通块 v v v ,使得 v v v 包含 u u u 中所有的元素并且 v v v 含有 u u u 中没有的元素,称 u u u 是极大的。
每一个割点至少属于两个连通分量。
对于一棵树,所有边都是桥
每一个点双连通分量至少包含一个割点。
两个割点之间的边不一定是桥。
一个桥连接的两个端点不一定是割点。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5ZEYljky-1637498558202)(C:\Users\ADguy\AppData\Roaming\Typora\typora-user-images\1637471162436.png)]
点双连通分量不一定是边双连通分量。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c53Z6rEI-1637498558203)(C:\Users\ADguy\AppData\Roaming\Typora\typora-user-images\1637471242860.png)]
边双连通分量不一定是点双连通分量。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EzlGbgDv-1637498558205)(C:\Users\ADguy\AppData\Roaming\Typora\typora-user-images\1637471282559.png)]
一言蔽之,割点和桥 没联系。
边双连通分量
理论实现
求解边双连通分量,类似于有向图的强连通分量问题,再次引入时间戳 的概念。
定义 d f n ( x ) dfn(x) dfn(x) 为 d f s dfs dfs 序时第一次访问到 x x x 节点的时间点
定义 l o w ( x ) low(x) low(x) 为以 x x x 节点为根节点的连通分量最早能到达的时间点
无向图不存在横叉边,因为在无向图中,横叉边必然在搜索前一个点是遍历完成,此时应该是前向边。
-
如何判断桥?
判断 y y y 能否走到 x x x 的祖先节点!!!如果不能,则为桥。
$dfn(x)<low(y) $
-
如何找到所有边双连通分量?
利用栈维护,如果 d f n ( x ) = = l o w ( x ) dfn(x)==low(x) dfn(x)==low(x) ,表示 x x x 无法返回他的祖先节点,说明 x x x 的父节点与 x x x 的边即为桥,还在栈当中的节点即为边双连通分量。
写的时候因为是无向边,我们要新加一个参数 p r e pre pre ( p r e pre pre 是边不是点)防止走回头路,因为如果走回头路的话必然找不到桥了。
要新开一个数组 b r i d g e [ M A X N ] bridge[MAXN] bridge[MAXN] 来记录每条边是否是桥。如果一条边是桥,那么他的反向边也是桥。
模板例题
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H62pZy32-1637498558207)(C:\Users\ADguy\AppData\Roaming\Typora\typora-user-images\1637478440698.png)]
给出一个无向的连通图,问最少加几条边能使整张图变成边双连通分量。
**结论:**对于一个无向连通图,将图中所有的双连通分量缩点后,定义那些度数为
1
1
1 的点个数为
c
n
t
cnt
cnt 。则最少需要添加
(
c
n
t
+
1
)
/
2
(cnt+1)/2
(cnt+1)/2 条边,就能使原图变为双连通分量。不会证明
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define MAXN 5005
#define MAXM 200005
typedef pair<int,int> pii;
#define INF 0x3f3f3f3f
int n,m;
int num,dfn[MAXN],low[MAXN],dcc,indgr[MAXN],key[MAXN],vis[MAXN];
stack <int> st;
int head[MAXN];int tot;
struct EDGE
{
int to,next;
}edge[MAXM];
void add_edge(int from,int to)
{
edge[++tot].to=to;edge[tot].next=head[from];head[from]=tot;
}
int bridge[MAXM];//判断某条边是不是桥
int get(int x)//根据边的编号求它的反向边
{
if(x&1) return x+1;
return x-1;
}
void tarjan(int x,int pre)
{
dfn[x]=low[x]=++num;
st.push(x);vis[x]=1;
for(int i=head[x];i;i=edge[i].next)
{
int y=edge[i].to;
if(!dfn[y])
{
tarjan(y,i);//是从i这一条边遍历过来的
low[x]=min(low[x],low[y]);
if(dfn[x]<low[y])//y无论如何都回不到y的祖先节点,那么这条边和其反向边为桥
{
bridge[i]=bridge[get(i)]=1;
}
}
else if(i!=get(pre))//这条边不是走回头路的,且y之前已经记录过它的时间戳
{
low[x]=min(low[x],dfn[y]);
}
}
if(dfn[x]==low[x])
{
dcc++;
int now=-1;
while(now!=x)
{
now=st.top();st.pop();
vis[now]=0;
key[now]=dcc;
}
}
}
void solve()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int u,v;
cin>>u>>v;
add_edge(u,v),add_edge(v,u);
}
tarjan(1,0);
for(int i=1;i<=tot;i++)
if(bridge[i])
indgr[key[edge[i].to]]++;
int ans=0;
for(int i=1;i<=dcc;i++)
if(indgr[i]==1)
ans++;
cout<<(ans+1)/2<<endl;
}
int main()
{
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
solve();
return 0;
}
之前济南热身赛有一道曹操炸桥的题。题意很明显,要炸的桥就是边双连通分量意义下的桥。我们只需找到边权最小的桥把它炸掉即可。
点双连通分量
理论实现
依旧引入 时间戳 的概念
- 如何求割点?
还是类似,由 x x x 连向 y y y ,去掉 x x x 和与之相连的边后, y y y 不能返回 x x x 的祖先节点。但如果 x x x 是整个图的根节点,那么还得继续讨论
- x x x 不是根节点, d f n [ x ] ≤ l o w [ y ] dfn[x] \leq low[y] dfn[x]≤low[y]
- x x x 是根节点,满足 x x x 至少有两个子节点 y 1 , y 2 y_1,y_2 y1,y2 ,使得$low[y_1] \geq dfn[x] $ and $ low[y_2] \geq dfn[x]$
- 如何维护所有点双连通分量
还是需要一个栈。
当发现 d f n [ x ] ≤ l o w [ y ] dfn[x] \leq low[y] dfn[x]≤low[y] 时,说明 y y y 无法返回 x x x 的祖先节点,说明原图中点双连通分量的个数 v c c vcc vcc 加一。如果 x x x 不是根节点或者 c n t > 1 cnt>1 cnt>1 ,说明 x x x 是割点。此时就将栈中元素弹出直至弹出 y y y 。并且 x x x 也属于该点双连通分量。
特判一下一个孤立点的情况,因为这也算一个点双连通分量。
模板例题
第一道例题,只要判断割点就行,无需维护边双连通分量。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SAQUGS9q-1637498558208)(C:\Users\ADguy\AppData\Roaming\Typora\typora-user-images\1637482810818.png)]
统计连通块个数 $cnt $
枚举从各个连通块中删除某个点 ,原先的一个连通块变成了 s s s 个连通块
答案即为 m a x ( c n t + s − 1 ) max(cnt +s-1) max(cnt+s−1)
关键在于找删除某个点后新生成的连通块个数
- 删除的点不是割点,不用管
- 删除的点是割点,且是根节点,删除后分裂的个数即为 d f n [ x ] ≤ l o w [ y ] dfn[x] \leq low[y] dfn[x]≤low[y] 的个数
- 如果不是根节点,分裂的个数为 d f n [ x ] ≤ l o w [ y ] dfn[x] \leq low[y] dfn[x]≤low[y] 的个数加一。因为是从根节点过来的,但遍历的时候不会去根节点。
问题转化为依次删除每个割点,求全局最大值。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define MAXN 10005
#define MAXM 30005
int n,m,num,cnt,ans,root;
int low[MAXN],dfn[MAXN];
int head[MAXN];int tot;
struct EDGE
{
int to,next;
}edge[MAXM];
void add_edge(int from,int to)
{
edge[++tot].to=to;edge[tot].next=head[from];head[from]=tot;
}
void init()
{
memset(dfn,0,sizeof(dfn));
memset(head,-1,sizeof(head));
tot=0,num=0,ans=0,cnt=0;
}
void tarjan(int x)
{
dfn[x]=low[x]=++num;
int cnt=0;//删除该点,原先的一个连通块变成cnt个连通块
for(int i=head[x];i!=-1;i=edge[i].next)
{
int y=edge[i].to;
if(!dfn[y])
{
tarjan(y);
low[x]=min(low[x],low[y]);
if(low[y]>=dfn[x]) cnt++;//x是割点,并且对于y,删除x后y不能与其祖先节点相连,多一个连通块
}
else low[x]=min(low[x],dfn[y]);
}
if(x!=root) cnt++;//x不是根节点的话祖先也得分出来一块
ans=max(ans,cnt);
}
void solve()
{
init();
for(int i=1;i<=m;i++)
{
int u,v;
cin>>u>>v;
add_edge(u,v),add_edge(v,u);
}
for(int i=0;i<n;i++)
{
if(!dfn[i])
{
root=i;
cnt++;//连通块数量加一
tarjan(i);//以i为根节点去遍历
}
}
cout<<ans+cnt-1<<endl;
}
int main()
{
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
while(cin>>n>>m)
{
if(n==0&&m==0) break;
solve();
}
return 0;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XP7DBnrA-1637498558209)(C:\Users\ADguy\AppData\Roaming\Typora\typora-user-images\1637491971780.png)]
首先需要明确一个特判:所有出口数量 ≥ \geq ≥ 2
由于图不一定连通,可以遍历每个连通块,算出每个连通块内各自的方案。将他们乘起来后得到的值就是最后的答案。
如果一个连通块内无割点,那么任意选两个点作为出口即可。方案数量为 C c n t 2 C_{cnt}^2 Ccnt2
如果一个连通块内有割点,考虑缩点。
对于一个割点,他至少属于两个点双连通分量,很几把扯淡。所以写起来贼几把烦。
缩点步骤:
- 每个割点单独作为一个点
- 每个点双连通分量向它所包含的那个割点连条边
这样操作完之后,每个点双连通分量的度数就是其所连接的割点的个数。
对于缩完点之后的图,可以把割点看成连接各个点双连通分量的出口。
对于一个点双连通分量里面来说,如果这个点双连通分量只连接了一个出口,即度数为 1 1 1 ,并且这个唯一的出口没了,那么这个点双连通分量里面必须确保有一个出口。方案数为 s i z e − 1 size-1 size−1 , s i z e size size 表示这个点双连通分量里点的个数,减一是因为这个出口不能是割点。
如果缩完点后的一个点双连通分量的度数大于 1 1 1 ,那么无论哪个出口塌了都可以逃出去,不用设置方案。
缩完点后的图必然是一棵树。度数为 1 1 1 的节点意味着是 叶子节点 ,因此加入树根没了,也就是割点,树的每棵子树必然可以逃到叶子节点避难。如果是其他点倒了,那就更没事了,因为可以逃到别的地方去避难或者自救。因此验证了方案的可靠性。
理一下思路:
- 求所有点双连通分量
- 缩点
- 统计度数(割点)
- 如果没有割点,方案 KaTeX parse error: Undefined control sequence: \C at position 1: \̲C̲_{size}^2,出口数量加 2 2 2 (特判孤立点的情况,方案贡献为 0 0 0 ,出口数量加 1 1 1);如果割点数量为1,方案 s i z e − 1 size-1 size−1,出口数量加 1 1 1 ;如果割点数量大于1,对方案没有贡献,对出口数量没有贡献。
#include<bits/stdc++.h>
using namespace std;
#define ull unsigned long long
#define MAXN 1005
#define MAXM 1005
int n,m,T;
int dfn[MAXN],num,low[MAXN],dcc,iscut[MAXN],root;
stack <int> st;
vector <int> dccc[MAXN];
int head[MAXN];int tot;
struct EDGE
{
int to,next;
}edge[MAXM];
void add_edge(int from,int to)
{
edge[++tot].to=to;edge[tot].next=head[from];head[from]=tot;
}
void init()
{
for(int i=1;i<=dcc;i++) dccc[i].clear();
memset(head,0,sizeof(head));
tot=num=dcc=n=0;
memset(dfn,0,sizeof(dfn));
memset(iscut,0,sizeof(iscut));
}
void tarjan(int x)
{
dfn[x]=low[x]=++num;
st.push(x);
if(x==root&&head[x]==0)//特判孤立点
{
dcc++;
dccc[dcc].emplace_back(x);
return;
}
int cnt=0;//统计裂开的块的的个数
for(int i=head[x];i;i=edge[i].next)
{
int y=edge[i].to;
if(!dfn[y])
{
tarjan(y);
low[x]=min(low[x],low[y]);
if(dfn[x]<=low[y])//y无法到达x的祖先
{
cnt++;
if(x!=root||cnt>1)
iscut[x]=1;//x是割点
dcc++;
int now=-1;
while(now!=y)
{
now=st.top();st.pop();
dccc[dcc].emplace_back(now);
}
dccc[dcc].emplace_back(x);//把x也要push进去
}
}
else
low[x]=min(low[x],dfn[y]);
}
}
void solve()
{
init();
for(int i=1;i<=m;i++)
{
int a,b;
cin>>a>>b;
n=max({a,b,n});
add_edge(a,b),add_edge(b,a);
}
for(root =1;root <=n;root++)
{
if(!dfn[root])
tarjan(root);
}
int mx=0;
ull ans=1;
for(int i=1;i<=dcc;i++)
{
int cnt=0;//统计一个点双连通分量内的割点个数
for(int j=0;j<dccc[i].size();j++)
{
if(iscut[dccc[i][j]]==1)//如果是割点
cnt++;
}
if(cnt==0)
{
if(dccc[i].size()>1)
mx+=2,ans*=dccc[i].size()*(dccc[i].size()-1)/2;
else
mx++;
}
else if(cnt==1)
mx++,ans*=(dccc[i].size()-1);
}
cout<<"Case "<<T<<": "<<mx<<" "<<ans<<endl;
}
int main()
{
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
T=0;
while(cin>>m)
{
if(!m) break;
T++;
solve();
}
return 0;
}
size()>1)
mx+=2,ans*=dccc[i].size()(dccc[i].size()-1)/2;
else
mx++;
}
else if(cnt==1)
mx++,ans=(dccc[i].size()-1);
}
cout<<"Case “<<T<<”: “<<mx<<” "<<ans<<endl;
}
int main()
{
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
T=0;
while(cin>>m)
{
if(!m) break;
T++;
solve();
}
return 0;
}