算法提高课笔记
理论基础
- 边的双连通分量 e-DCC:极大不包含桥的连通块
- 点的双连通分量 v-DCC:极大不包含割点的连通块
桥:删去该边图就不连通
割点:删去该点和关联边图就不连通
每个割点至少属于两个连通分量
两个割点之间不一定是桥,连接两个桥的点也不一定是割点
边的双连通分量问题
引入时间戳概念
记录dfn(x)
:遍历到每个点的时间
low(x)
:每个连通分量的最高点
如何找到桥?
图中一条边[x, y]
,如果y可以走到更上面,就不是桥,如果y最多只能走到自己,那就是桥
因此,一条边是桥等价于dfn[y] == low[y]
如何找到所有边的双连通分量?
方法1. 将所有桥删掉
方法2. 用一个栈存,如果找到一个x使得dfn[x] == low[x]
,那么x的子树中还在栈中的结点就是x的连通分量的结点
例题:冗余路径
为了从 F 个草场中的一个走到另一个,奶牛们有时不得不路过一些她们讨厌的可怕的树。
奶牛们已经厌倦了被迫走某一条路,所以她们想建一些新路,使每一对草场之间都会至少有两条相互分离的路径,这样她们就有多一些选择。
每对草场之间已经有至少一条路径。
给出所有 R 条双向路的描述,每条路连接了两个不同的草场,请计算最少的新建道路的数量,路径由若干道路首尾相连而成。
两条路径相互分离,是指两条路径没有一条重合的道路。
但是,两条分离的路径上可以有一些相同的草场。
对于同一对草场之间,可能已经有两条不同的道路,你也可以在它们之间再建一条道路,作为另一条不同的道路。
输入格式
第 1 行输入 F 和 R。
接下来 R 行,每行输入两个整数,表示两个草场,它们之间有一条道路。
输出格式
输出一个整数,表示最少的需要新建的道路数。
数据范围
1
≤
F
≤
5000
,
1≤F≤5000,
1≤F≤5000,
F
−
1
≤
R
≤
10000
F−1≤R≤10000
F−1≤R≤10000
输入样例
7 7
1 2
2 3
3 4
2 5
4 5
5 6
5 7
输出样例
2
题意
给出无向连通图,求问至少加多少条路径使得成为边的双连通分量
思路
边的双连通分量意味着不含桥(不具体证明了)
找到所有边的双连通分量之后,进行缩点,之后图就会变成一棵树
树中所有度数为1的点都需要加一条边(否则把原有边砍掉之后就不连通了)
所以需要加的边数至少是 cnt/2 上取整(叶子结点的个数)
像这样连,发现取cnt/2上取整是完全满足条件的 于是答案是 (cnt+1)/2
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 5010, M = 20010;
int n, m;
int h[N], ne[M], e[M], idx;
int dfn[N], low[N], timestamp;
stack<int> stk;
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) // from是父结点
{
dfn[u] = low[u] = ++ timestamp; // 先将dfn和low都初始化为时间戳
stk.push(u); // u加入栈中
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i]; // 取出u的所有邻点j
if (!dfn[j]) // 如果j还没被遍历
{
tarjan(j, i);
low[u] = min(low[u], low[j]); // 用low[j]更新low[u]
if (dfn[u] < low[j]) // 说明j到不了u 即当前这条边是一个桥
is_bridge[i] = is_bridge[i ^ 1] = true; // 正向边和反向边
}
else if (i != (from ^ 1)) // 不是反向边
low[u] = min(low[u], dfn[j]); // 更新low[u]
}
if (dfn[u] == low[u]) // 如果该点是所在强连通分量的最高点
{
++ dcc_cnt; // 更新连通块数量
int y;
do
{
y = stk.top(); // 取出栈顶元素
stk.pop();
id[y] = dcc_cnt; // 标记每个点所在的连通分量编号
} while (y != u); // 直到取到此连通分量的最高点为止
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
memset(h, -1, sizeof h);
cin >> n >> m;
while (m -- )
{
int a, b;
cin >> a >> b;
add(a, b), add(b, a);
}
tarjan(1, -1);
for (int i = 0; i <= idx; i ++ )
if (is_bridge[i]) d[id[e[i]]] ++ ; // 记录每个结点的度
int cnt = 0;
for (int i = 1; i <= dcc_cnt; i ++ )
if (d[i] == 1) cnt ++ ;
cout << (cnt + 1) / 2 << '\n';
}
点的双连通分量问题
同样需要记录时间戳
dfn(x)
:遍历到每个点的时间
low(x)
:每个连通分量的最高点
如何判断割点?
我们从当前开始搜,也就是从x搜到y,判断x是否为割点只要看y是否能搜到x上面
如果y能搜到x上方,说明x一定不是割点
所以要求 low(y) >= dfn(x)
- 如果x不是根结点,那么x必然是割点
- 如果x是根结点,还要分两种情况讨论:
- 如果x只有一个子树,那么删去x对原来的连通性没有影响,x不是割点
- 如果x有两个及以上的子树,那么删去x会影响图的连通性,x是割点。该条件可以转换成
low(y) >= dfn(x)
如何求点的双连通分量?
例题:电力
给定一个由 n 个点 m 条边构成的无向图,请你求出该图删除一个点之后,连通块最多有多少。
输入格式
输入包含多组数据。
每组数据第一行包含两个整数 n,m。
接下来 m 行,每行包含两个整数 a,b,表示 a,b 两点之间有边连接。
数据保证无重边。
点的编号从 0 到 n−1。
读入以一行 0 0 结束。
输出格式
每组数据输出一个结果,占一行,表示连通块的最大数量。
数据范围
1
≤
n
≤
10000
,
1≤n≤10000,
1≤n≤10000,
0
≤
m
≤
15000
,
0≤m≤15000,
0≤m≤15000,
0
≤
a
,
b
<
n
0≤a,b<n
0≤a,b<n
输入样例
3 3
0 1
0 2
2 1
4 2
0 1
2 3
3 1
1 0
0 0
输出样例
1
2
2
题意
给出一张图,删掉一个结点,问连通块最多有多少
思路
- 首先统计连通块个数
cnt
- 枚举哪个连通块中删,确定了连通块之后再枚举删除哪个点,分别求一下删除该店之后可以把当前连通块分成几部分
- 假设可以分为
s
部分,那么总共连通块个数就是s + cnt - 1
现在问题就变成了s
怎么算呢?
我们要在这个地方删去x点,如果x不是根结点,那么删完x有三部分,就像这样:
如果x是根结点,那么删完x由两部分,就像这样:
/.;
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 10010, M = 30010;
int n, m;
int h[N], ne[M], e[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; // 先将dfn和low都初始化为时间戳
int cnt = 0; // 记录删去当前点后当前连通块能变成多少个连通块
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i]; // 取出u的所有邻点j
if (!dfn[j]) // 如果j还没被遍历
{
tarjan(j);
low[u] = min(low[u], low[j]); // 用low[j]更新low[u]
if (low[j] >= dfn[u]) // 说明j到不了u以上的点 即删去u后连通块数量加1
cnt ++ ;
}
else low[u] = min(low[u], dfn[j]); // 更新low[u]
}
if (u != root) cnt ++ ; // u不是根结点 删去u原连通块分成三部分(也就是在原来两部分的基础上再加1)
ans = max(ans, cnt);
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
while (cin >> n >> m, n || m)
{
memset(h, -1, sizeof h);
memset(dfn, 0, sizeof dfn);
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])
{
cnt ++ ; // 总连通块数量加1
tarjan(root); // 遍历当前连通块
}
cout << ans + cnt - 1 << '\n';
}
}