大佬的结论
无向图
双连通分量
1 边的双连通分量 e-dcc 极大的不包含桥的连通块
2 点的双连通分量 v-dcc 极大的不包含割点的连通块
删除后不连通 的 两个定义
桥
o-o 桥o-o
| | ↓ | |
o-o - o-o
1 如何找桥? x
/
y
x和y之间是桥 <=> dfn[x] < low[y] y无论如何往上走不到x //模板中反向边是不会遍历的
+y能到的最高的点low[y] = dfn[y]
2 如何找所有边的双连通分量?
2.1 将所有桥删掉
2.2 stack
dfn[x] == low[x]
<=> x无论如何走不到x的上面
<=> 从x开始的子树可以用一个栈存
每个割点至少属于两个连通分量
树里的每条边都是桥
o
/ \
o o
/\ /\
o o o o
1 边双连通分量
tarjan回顾
dfn[x] dfs序时间戳
low[x] x能达到的时间戳最小的点
无向图不存在横插边
o
/ \
o o
/ / \
y ← x o
→
x能往左的话 由于无向边 所以y也能往右,
那么在之前dfs的时候就把x先于x的父节点加进来了
*/
/*
新建一条道路 使得每两个草场之间都有一个分离的路径
给定一个无向连通图,问最少加几条边,可以将其变为一个边双连通分量
结论:一个边的双连通分量 <=> 任何两个点之间至少存在两个不相交路径
充分性: 对于每两个点都有互相分离的路径的话,则必然为强连通分量
反证 假设有桥(非双连通) x,y必然经过中间的桥 则只x→y的路径必在桥上相交
o-x 桥y-o
| | ↓ | |
o-o - o-o
必要性:图是一个边双连通分量 <=> 不包含桥
则一定对任意两点x,y
x,y之间至少存在两条互相分离(不相交)的路径
反证:假设存在两条相交路径
那么x→y中间必然有桥
// o-o-o-o-o 蓝色路径 (从x出发到y经过边数最少的路径)
// - - - - 绿色路径
// x y
对双连通分量做完缩点后 只剩桥和点
o
/ \
o o
/\ /\
o o o o
/ \
o o
o
/ \
o o
/\ /\
o o-o o
/ \ | |
o o__| |
|_________|
可以发现对左右两个叶子节点连通后,根节点连向左右叶子节点的边就可以删去了
同理 再把第2个和第4个叶子节点连通后,根节点连向第2个和第4个叶子节点的边也可以删去
第3个叶子节点随便连
给叶子节点按对称性加上边后就没有桥 <=> 变成边的双连通分量
这里cnt= 5 加了ans=(cnt+1)/2=3条
ans >= 下界[cnt/2]取整 == [(cnt+1)/2]取整
其中 cnt为缩完点后度数==1的点的个数
作者:仅存老实人
链接:https://www.acwing.com/solution/content/20697/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
1.求边的双连通分量
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 5010, M = 20010;
int n, m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
int id[N], dcc_cnt;
bool is_bridge[M];//每条边是不是桥
int d[N];//度数
void add(int a, int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
void tarjan(int u, int from)
{
dfn[u] = low[u] = ++ timestamp;
stk[ ++ top] = u;
for (int i = h[u]; i!=-1; i = ne[i])
{
int j = e[i];
if (!dfn[j])//j未遍历过,也可以防止反向边
{
tarjan(j, i);//dfs(j)
low[u] = min(low[u], low[j]);//用j更新u
if (dfn[u] < low[j])//j到不了u
// 则x-y的边为桥,
//正向边is_bridge[i] 反向边is_bridge[i ^ 1]都是桥
is_bridge[i] = is_bridge[i ^ 1] = true;//i是idx,在建边时,0与1相连,2与3相连,构成正边和反边,只要正边是桥,那么反边也肯定是桥
// 这里i==idx 如果idx==奇数 则反向边=idx-1 = idx^1
// 如果idx==偶数 则反向边=idx+1 = idx^1
}
// j遍历过 且i不是反向边(即i不是指向u的父节点的边)
// 因为我们不能用u的父节点的时间戳更新u
else if (i != (from ^ 1))//如果不是反向边时
low[u] = min(low[u], dfn[j]);
}
//双连通分量起点u /
// u
// /
// ne1
// ne2
if (dfn[u] == low[u])
{
++ dcc_cnt;
int y;
do {
y = stk[top -- ];
id[y] = dcc_cnt;
} while (y != u);
}
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
while (m -- )
{
int a, b;
cin >> a >> b;
add(a, b), add(b, a);
}
tarjan(1, -1);//防止搜反向边 用一个from
for (int i = 0; i < idx; i ++ )//要注意一定要遍历反向边,如果图是多点连接1个点时,每个点入度的值是1,不遍历反向边时入度是0
//如果边i是桥 在其所连的出边的点j所在强连通分量的度+1
// 桥两边的双连通分量各+1
if (is_bridge[i])
d[id[e[i]]] ++ ;
int cnt = 0;
for (int i = 1; i <= dcc_cnt; i ++ )
if (d[i] == 1)//多少个度数为1的节点(强连通分量)
cnt ++ ;//需要加的边的数量
cout << (cnt + 1) / 2 << endl;
return 0;
}
作者:仅存老实人
链接:https://www.acwing.com/solution/content/20697/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
2.求点的双连通分量1
3 割点
割点
o-o o-o
| \↓/ |
o-o-o-o-o
3.1求割点
x
|
y
要求low[y] > dfn[x] y能到达的最早的节点>x的时间戳
否则low[y] ≤ dfn[x]时,即y能到比xdfs序小的点,则存在环
→o
| ↓
| x
| |
o←y
low[y]≤dfn[x]的情况下
1 如果x不是根节点 那么x是割点
2 x是根节点 至少有两个子节点yi
且有low[yi]≥dfn[x]
如果只有一个子结点 去完根节点x
x
| → y1 子节点部分还是连通的
y1
如果有两个子结点 去完根节点x
x
/ \ → y1 y2 之间不连通
y1 y2
3.2 求点双连通分量
o x
|
o y
stk[]
if(dfn(x)<=low(y))
{
cnt++
if(x非根||cnt>1)x是割点
stk.pop(j) while j!=y将栈中元素弹出至y为止
且x也属于该 点双连通分量
}
样例1
0 - 1 0 - 1
\ / → 1个(不管删除哪个点都是连通的)
2
样例2
0 - 1 0
2 - 3 2 - 3 2个(删除1后仍有0 和2-3两个连通块)
样例3
0 - 1 删1还剩两个块 0 and 2
2 删2还剩一个块 0-1
1 统计连通块个数cnt
2 枚举从哪个块j中删
2.1 从块j中删除哪个点i
2.2 删除点i后块j分成s部分(在样例3中删2后s=0)
总共分成的部分 = s(i新的子块的个数)+cnt(删前总的连通块数)-1(删前子块的个数)
= 当前块的部分(s)+剩余连通块的数量(cnt-1)
= 1+1-1 (样例1:删2后 s=1 cnt=1 -1 =1
= 1+2-1 (样例2:删1后 s=1 cnt=2 -1 =2
= max(1+2-1,0+2-1)(样例3:删1后 s=1 cnt=2 -1=2
删2后 s=0 cnt=2 -1=1(点2所在的块删除2后没有点了 s=0)
dfn(x)<=low(y)
x删掉后y单独出来--多一个单独子树
如果x非根节点 还要多+1
/
x
/ \
问题转化为依次删除每个割点
求全局最大值
作者:仅存老实人
链接:https://www.acwing.com/solution/content/20702/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 10010, M = 30010;
int n, m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int root, ans;
void add(int a, int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++ ;
}
void tarjan(int u)
{
dfn[u] = low[u] = ++ timestamp;
int cnt = 0;//当前块内 已经可以分出来的子树的个数
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (!dfn[j])//没有遍历过j
{
tarjan(j);
low[u] = min(low[u], low[j]);//用j更新u
if (low[j] >= dfn[u]) cnt ++ ;//j为割点 则多一个连通块
}
else low[u] = min(low[u], dfn[j]);
}
if (u != root) cnt ++ ;//如果是一个环的话,可以发现此条件不成立,cnt=0;
//如果不是根节点
/*
/
x 删掉x后 除子节点yi外
/ \ 还要要加上父节点部分+1
o o
*/
ans = max(ans, cnt);
}
int main()
{
while (cin >> n >> m, n || m)
{
memset(dfn, 0, sizeof dfn);
memset(h, -1, sizeof h);
idx = timestamp = 0;
while (m -- )
{
int a, b;
cin >> a >> b;
add(a, b), add(b, a);
}
ans = 0;//把每个点删完后 最多能分为几块
int cnt = 0;//连通块数量
for (root = 0; root < n; root ++ )
if (!dfn[root])//如果点root没搜索过 连通块数+1
{
cnt ++ ;
tarjan(root);
}
cout << ans + cnt - 1 << endl;
}
return 0;
}
作者:仅存老实人
链接:https://www.acwing.com/solution/content/20702/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
3.求点的双连通分量2
给一个不一定连通的无向图
问最少在几个点设置出口
使得任意一个出口坏掉后,其他所有点都可以和某个出口连通
首先考虑
1 出口数量>=2 如果只有一个出口 那这个出口坏了就没有可以用的出口了
2 不同连通块之间相互独立(最终方案数 = 各连通块方案数乘积)
对一个连通块
2.1 无割点 <=> 不管我删掉哪个点 图剩余部分都是连通的、且剩余好出口>=1
<=> 度数==0
<=> 孤立的点
那么我们必须设置2个出口就可以满足
连通块中点个数=cnt
方案数=C[cnt][2] = cnt*(cnt-1)/2
2.2 有割点 <=> 点双连通分量 缩点(每个割点都属于2个点双连通分量)
2.2.1 先将所有割点单独出来作为一个点
2.2.2 从每个点双连通分量(V-DCC)向其所包含的每个割点连一条边
o o
/ \ / \
o---.---.---o
\ /
o
缩完点后 .-割点 o-连通块
o-.-o-.-o 同时缩完后的o中包含之前有连的
2.2.3 看V-DCC度数
2.2.3.1 如果V-DCC==1 意味着它只包含一个割点(上图中左和右)
则如果这个割点是出口且坏掉 这个V-DCC就无法连到其他出口了
🔺所以V-DCC==1 时 必须在V-DCC内(非割点)放置一个出口
2.2.3.2 如果V-DCC>1
就不需要设置出口
证明:
1 如果割点坏了
缩完点后,🔺此时所有度数==1的点相当于一个叶子节点-所有度数为1的叶子节点放一个出口
因此每个叶子节点必然会有一个出口
.(割点)
×/ \×
o o(连通块)
2 如果度数为1的右边的V-DCC里的某个点坏了
由于是V-DCC 删去坏点后仍然是一个V-DCC
.(割点)
/ \
o @(连通块中坏了一个点)
但此时不影响这个V-DCC到割点 并通过割点到左边V-DCC中的出口
3 如果度数为2的V-DCC里的某个点坏了
由于度数==2 <=> 必然连接两个割点
o(连通块)
/ \
. .(割点)
每个割点必然有一个出口
所以可以从上面的o到下面的两个割点到其他V-DCC找到出口
总结:
1 无割点 放2个出口 方案数 *= C[cnt][2] = cnt*(cnt-1)/2
2 有割点 V-DCC==1 放1个出口 方案数 *= C[cnt-1][1] = cnt-1 (不包含割点)
3 有割点 V-DCC>=2 放0个出口 方案数 *= 1
作者:仅存老实人
链接:https://www.acwing.com/solution/content/24931/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
#include<cstring>
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
typedef unsigned long long ULL;
const int N = 1010, M = 1010;
int n, m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
int dcc_cnt;
vector<int> dcc[N];
bool cut[N];
int root;
void add(int a,int b)
{
e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}
void tarjan(int u)
{
low[u] = dfn[u] = ++timestamp;
stk[++top] = u;
// 1 u是孤立点-自称一个dcc
if(u==root && h[u]==-1)//u是根节点且没有邻边
{
dcc_cnt++;
dcc[dcc_cnt].push_back(u);
return;
}
// 2 u不孤立
int cnt = 0;
for(int i = h[u];~i;i=ne[i])
{
int j = e[i];
if(!dfn[j])
{
tarjan(j);
low[u] = min(low[u],low[j]);
// 看j是不是能连到比u还高的地方
if(dfn[u]<=low[j])//j最高比u高度低 说明j是u一个新的分支(如果把u删掉 多一个j连通块)
{
cnt++;
// 判断u是否割点 如果不是根节点-只要有一个分支他就是割点 || 如果是根节点 需要有两个分支才是割点
// root /
// / \ 非root(自带上面一个边,所以只要一个下分支)
// /
if(u!=root||cnt>1)cut[u] = true;
++dcc_cnt;
int y;
do{
y = stk[top--];
dcc[dcc_cnt].push_back(y);//存储的是这个割点所连的点(树中该点的子节点)
}while(y!=j);//注意弹出栈不是弹到u为止 而是弹到j为止(u仍保留在stk中)
// 🔺 开新分支 == u一定和新分支j组成一个dcc 也和旧连通块组成dcc
// 那么当前最高点u还要被用在更高的包含u的旧连通块
// 所以如果这个时候出栈了 回溯到比u高的点的时候 u就加不进旧连通块里
dcc[dcc_cnt].push_back(u);
}
}
else low[u] = min(low[u],dfn[j]);
}
}
int main()
{
int T = 1;
while(cin >> m,m)
{
for(int i=1;i<=dcc_cnt;i++)dcc[i].clear();
idx = n = timestamp = top = dcc_cnt = 0;
memset(h,-1,sizeof h);
memset(dfn,0,sizeof dfn);
memset(cut,0,sizeof cut);
while(m--)
{
int a,b;
cin >> a >> b;
n = max(n,b),n = max(n,a);//第二个n=漏了
add(a,b),add(b,a);
}
for (root = 1; root <= n; root ++ )
if (!dfn[root])
tarjan(root);
int res = 0;
ULL 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]])//判断i的所连的点(包括自己)是否是割点
cnt++;
}
// 无割点
if(cnt == 0)
{
if(dcc[i].size()>1)res+=2,num*=dcc[i].size()*(dcc[i].size()-1)/2;
else res++;
}
else if(cnt==1)res++,num*=dcc[i].size()-1;
}
printf("Case %d: %d %llu\n", T ++, res, num);
}
return 0;
}
作者:仅存老实人
链接:https://www.acwing.com/solution/content/24931/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。