定义
在一张连通的无向图中,对于两个点u和v,如果无论删去哪条边(只能删去一条)都不能使得它们不连通,则称u和v边双连通。
在一张连通的无向图中,对于两个点u和v,如果无论删去哪个点(只能删去一个,且不能删去u和v自己)都不能使得它们不连通,则称u和v点双连通。
边双连通具有传递性,即若x,y边双连通,y,z边双连通,则x,z边双连通。
点双连通不具有传递性。
割点:对于一个无向图,如果把一个点删除后这个图的极大连通分量数增加了,那么这个点就是这个图的割点。
割边:对于一个无向图,如果删掉一条边后图中的连通分量数增加了,则称这条边为桥或者割边。
注意:割点其实和割边并没有直接关系
例题
冗余路径
本题即是问:最少加几条边可以使得该无向图变成一个边双连通分量
缩点后该图就会成为一棵树,答案就是树中度为1的节点除2上取整。
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f
using namespace std;
typedef long long LL;
const int N = 5010, M = 20010;
int n, m;
int idx, head[N], e[M], ne[M];
int dfn[N], low[N], timestamp;
int stk[N], top;
bool bridge[N];
int dcc_cnt, id[N], degree[N];
void add(int u, int v)
{
e[idx] = v, ne[idx] = head[u], head[u] = idx ++;
}
void tarjan(int u, int from) //from:从哪条边过来
{
dfn[u] = low[u] = ++ timestamp;
stk[++ top] = u;
for(int i = head[u]; ~i; i = ne[i])
{
int v = e[i];
if(!dfn[v])
{
tarjan(v, i);
low[u] = min(low[u], low[v]);
if(dfn[u] < low[v]) bridge[i] = bridge[i ^ 1] = true;
//说明 v 无论如何也走不到 u 已经 u 以上的点,u--> v这条边就是一个桥
}
else if(i != (from ^ 1)) //不是反向边
low[u] = min(low[u], dfn[v]);
}
if(dfn[u] == low[u])
{
++ dcc_cnt;
int p;
do {
p = stk[top --];
id[p] = dcc_cnt;
} while(p != u);
}
}
int main()
{
#ifdef LOCAL
freopen("in.in", "r", stdin);
freopen("out.out", "w", stdout);
#endif
cin >> n >> m;
memset(head, -1, sizeof head);
while(m --)
{
int u, v;
cin >> u >> v;
add(u, v), add(v, u);
}
tarjan(1, -1);
for(int i = 0; i < idx; i ++)
if(bridge[i]) degree[id[e[i]]] ++;
int cnt = 0;
for(int i = 1; i <= dcc_cnt; i ++) cnt += (degree[i] == 1);
cout << (cnt + 1) / 2 << endl;
return 0;
}
电力
如果删除一个点以后联通块个数会增加,意味着该点一定是图中的一个割点,否则删除不是割点的点的话联通块个数并不会改变。
所以本题做法很明显,求出所有割点,枚举删除哪一个割点会得到最大值。
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f
using namespace std;
typedef long long LL;
const int N = 10010, M = 30010;
int n, m;
int idx, head[N], e[M], ne[M];
int dfn[N], low[N], timestamp;
int root; //记录每一个连通块的“根节点”
int ans; //记录答案最大值
void add(int u, int v)
{
e[idx] = v, ne[idx] = head[u], head[u] = idx ++;
}
void tarjan(int u)
{
dfn[u] = low[u] = ++ timestamp;
int cnt = 0; //如果u是割点的话,去掉该点u得到的连通块个数。(也相当于缩点后u的儿子个数)
for(int i = head[u]; ~i; i = ne[i])
{
int v = e[i];
if(!dfn[v])
{
tarjan(v);
low[u] = min(low[u], low[v]);
if(low[v] >= dfn[u]) cnt ++;
//v无论如何都到不了u的上面,那么去掉u之后,v所在连通块会与u上面的连通块分开,即u是割点
}
else low[u] = min(low[u], dfn[v]);
}
if(u != root) cnt ++; //u不是根,那么去掉u之后,u上面也会分离出来一个连通块
ans = max(ans, cnt);
}
int main()
{
#ifdef LOCAL
freopen("in.in", "r", stdin);
freopen("out.out", "w", stdout);
#endif
while(scanf("%d%d", &n, &m), n || m)
{
memset(dfn, 0, sizeof dfn);
memset(head, -1, sizeof head);
idx = timestamp = 0;
while(m --)
{
int u, v;
scanf("%d%d", &u, &v);
add(u, v), add(v, u);
}
ans = 0;
int cnt = 0; //原图连通块个数
for(root = 0; root < n; root ++)
if(!dfn[root])
{
cnt ++;
tarjan(root);
}
printf("%d\n", ans + cnt - 1);
}
return 0;
}
矿场搭建
显然:出口个数一定要大于等于2。
分情况看:
1、整个图没有割点
则不管删除哪一个点,整个图依然是连通的,所以只要有2个出口就可以,并且这2个出口的位置可以随便放,方案数是 C c n t 2 C^2_{cnt} Ccnt2 。(cnt是点的个数)
2、有割点
缩点后,整个图就变成了一棵树。
具体缩点方法是:
1)每个割点单独作为一个点
2)从每个V-DCC(点双连通分量)向其所包含的每个割点连一条边
如果V-DCC的度数为1,意味着它只有一个割点,如果该割点坏掉,该点双连通分量与其他的点就分开了,所以该点连通分量内部(非割点位置)必须要设置一个出口,方案数为cnt-1。(cnt是该点双连通分量的大小,-1是除去了割点)
如果V-DCC读数大于1,即它包含大于1个割点。那么不管哪一个点坏掉,该点连通分量总数可以连接到其他的点,所以这样的点双连通分量内部不需要设置出口。
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f
using namespace std;
typedef unsigned long long ULL;
const int N = 1010, M = 1010;
int n, m;
int idx, head[N], e[M], ne[M];
int dfn[N], low[N], timestamp;
int stk[N], top;
int dcc_cnt, root;//双连通分量个数
vector<int> dcc[N];//双连通分量内包含哪些点
bool cut[N];//是否是割点
void add(int u, int v)
{
e[idx] = v, ne[idx] = head[u], head[u] = idx ++;
}
void tarjan(int u)
{
dfn[u] = low[u] = ++ timestamp;
stk[++ top] = u;
//1、u是孤立点--自成一个v-dcc
if(u == root && head[u] == -1) //u是根且没有邻边
{
dcc_cnt ++;
dcc[dcc_cnt].push_back(u);
return ;
}
//2、u不是孤立点
int cnt = 0; //若u是割点,cnt就是其儿子节点的个数
for(int i = head[u]; ~i; i = ne[i])
{
int v = e[i];
if(!dfn[v])
{
tarjan(v);
low[u] = min(low[u], low[v]);
if(low[v] >= dfn[u]) //从v遍历不到u的上面,u可能是割点
{
cnt ++;
if(u != root || cnt > 1) cut[u] = true;
//如果u不是根或者u是根但是u的儿子个数大于1,那么u就是割点
++ dcc_cnt;
int y;
do {
y = stk[top --];
dcc[dcc_cnt].push_back(y);
} while(y != v);//注意:结束条件是y != v,u仍然保留在栈中
//因为u被同时包含在两个点双连通分量中,它还要用于更高的一个连通块
//如果这个时候吧u出栈,在回溯到比u更高的点的时候,u就无法被加入该块内
dcc[dcc_cnt].push_back(u); //最后把u放入
}
}
else low[u] = min(low[u], dfn[v]);
}
}
int main()
{
#ifdef LOCAL
freopen("in.in", "r", stdin);
freopen("out.out", "w", stdout);
#endif
int kase = 1;
while(cin >> m, m)
{
memset(head, -1, sizeof head);
memset(dfn, 0, sizeof dfn);
memset(cut, 0, sizeof cut);
for(int i = 1; i <= dcc_cnt; i ++) dcc[i].clear();
idx = timestamp = dcc_cnt = top = n = 0;
while(m --)
{
int u, v;
cin >> u >> v;
add(u, v), add(v, u);
n = max(n, max(u, v));
}
for(root = 1; root <= n; root ++)
if(!dfn[root]) tarjan(root);
int ans = 0; //最少出口数量
ULL nums = 1;//方案数
for(int i = 1; i <= dcc_cnt; i ++) //枚举每一个点双连通分量
{
int cnt = 0; //该点双连通分量包含的割点的个数
for(auto x: dcc[i]) cnt += cut[x];
if(!cnt)
{
if(dcc[i].size() > 1) ans += 2, nums *= dcc[i].size() * (dcc[i].size() - 1) / 2;
else ans ++;//特殊判断,孤立点
}
else if(cnt == 1) ans ++, nums *= dcc[i].size() - 1;
}
printf("Case %d: %d %llu\n", kase ++, ans, nums);
}
return 0;
}