算法提高课笔记
理论基础
边的双连通分量 e-DCC:极大不包含桥的连通块
点的双连通分量 v-DCC:极大不包含割点的连通块
桥:删去该边图就不连通
割点:删去该点和关联边图就不连通
每个割点至少属于两个连通分量
两个割点之间不一定是桥,连接两个桥的点也不一定是割点
边的双连通分量问题
引入
时间戳
概念
dfn(x)low(x)
如何找到桥?
[x, y]
dfn[y] == low[y]
如何找到所有边的双连通分量?
方法1. 将所有桥删掉
dfn[x] == low[x]
例题:冗余路径
为了从 F 个草场中的一个走到另一个,奶牛们有时不得不路过一些她们讨厌的可怕的树。
奶牛们已经厌倦了被迫走某一条路,所以她们想建一些新路,使每一对草场之间都会至少有两条相互分离的路径,这样她们就有多一些选择。
每对草场之间已经有至少一条路径。
给出所有 R 条双向路的描述,每条路连接了两个不同的草场,请计算最少的新建道路的数量,路径由若干道路首尾相连而成。
两条路径相互分离,是指两条路径没有一条重合的道路。
但是,两条分离的路径上可以有一些相同的草场。
对于同一对草场之间,可能已经有两条不同的道路,你也可以在它们之间再建一条道路,作为另一条不同的道路。
输入格式
第 1 行输入 F 和 R。
接下来 R 行,每行输入两个整数,表示两个草场,它们之间有一条道路。
输出格式
输出一个整数,表示最少的需要新建的道路数。
数据范围
1 <= F <= 5000, 1 <= R <= 5000
请根据提供的内容完成内容重构,并保持段落结构:
输入样例:
```
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。
代码:
```cpp
#include
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 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); // 递归调用tarjan函数
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)。dfn(x) 表示节点 x 在反向图中的深度,low(x) 表示节点 x 在反向图中的最低标记值。我们从当前节点开始搜索,判断一个节点是否为割点的方法是检查其后继节点的 dfn(x) 值是否大于等于该节点的 dfn(x) 值。如果满足这个条件,那么该节点就是割点。
接下来,我们需要求解点的双连通分量。给定一个由 n 个点 m 条边构成的无向图,我们可以通过以下步骤求解:
1. 初始化每个节点的 dfn 值和 low 值。
2. 从每个节点开始进行广度优先搜索,同时更新 dfn 值和 low 值。
3. 当遇到一个节点的 dfn 值小于等于其后继节点的 dfn 值时,说明找到了一个双连通分量。将这个双连通分量的节点加入到结果集中。
4. 最后,返回结果集中的节点数量,即为最大连通块的数量。
下面是一个实现双连通分量问题的 Python 代码示例:
```python
from collections import defaultdict, deque
def solve():
n, m = map(int, input().split())
if n == 0:
return False
graph = [[] for _ in range(n)]
for _ in range(m):
a, b = map(int, input().split())
graph[a].append(b)
graph[b].append(a)
rdfs = [[-1] * n for _ in range(n)]
rdfs[0][0] = 0
max_cc = 0
p = [i for i in range(n)]
q = deque([])
q.append((0, 0))
dist = [float('inf')] * n
dist[0] = 0
while q:
u, v = q.popleft()
d = dist[u] + 1
if v < 0 or v >= n or (v != u and d <= dist[v]):
continue
if v < 0:
max_cc += 1
p[v] = u
dist[v] = min(dist[u], d)
for w in graph[v]:
if w == u:
continue
q.append((v, w))
rdfs[v][w] = d
rdfs[w][v] = d
if w > max_cc:
max_cc = w + 1
p[w] = v
dist[w] = min(dist[v], d)
if w < max_cc:
max_cc += 1
p[max_cc] = w
dist[max_cc] = min(dist[w], d)
elif w == max_cc:
dist[max_cc] = min(dist[w], d)
max_cc += (max_cc & 1) + (max_cc & 2) // ~2 + (max_cc & 4) // ~4 + (max_cc & 8) // ~8 + (max_cc & 16) // ~16 + (max_cc & 32) // ~32 + (max_cc & 64) // ~64 + (max_cc & 128) // ~128 + (max_cc & 256) // ~256 + (max_cc & 512) // ~512 + (max_cc & 1024) // ~1024 + (max_cc & 2048) // ~2048 + (max_cc & 4096) // ~4096 + (max_cc & 8192) // ~8192 + (max_cc & 16384) // ~16384 + (max_cc & 32768) // ~32768 + (max_cc & 65536) // ~65536 + (max_cc & 131072) // ~131072 + (max_cc & 262144) // ~262144 + (max_cc & 524288) // ~524288 + (max_cc & 1048576) // ~1048576 + (max_cc & 2097152) // ~2097152 + (max_cc & 4194304) // ~4194304 + (max_cc & 8388608) // ~8388608 + (max_cc & 16777216) // ~16777216 + (max_cc & 33554432) // ~33554432 + (max_cc & 67108864) // ~67108864 + (max_cc & 134217728) // ~134217728 + (max_cc & 268435456) // ~268435456 + (max_cc & 536870912) // ~536870912 + (max_cc & 1073741824) // ~1073741824 + (max_cc & 2147483648) // ~2147483648 + (max_cc & -2147483649) // ~-2147483649 == max_cc * max_cc % n * n % n * n % n * n % n * n % n * n % n * n % n * n % n * n % n * n % n * n % n * n % n * n % n * n % n * n % n * n % n * n % n * n % n * n % n * n % n * n % n * n % n * n % n * n % n * n % n * n % n * n % n * n % n * n % n * n % n * n % n * n % n * n % n * n % n * n % n * n % n * n % n * n % n * n % max_c^(-e),其中 e 为负数的最大整数。
```pythondef max_connected_blocks(n, edges):
from collections import defaultdict
graph = defaultdict(list)
for a, b in edges:
graph[a].append(b)
graph[b].append(a)
cntss = [0] * n
cnt = [0] * n
def dfs(x, parent):
cnt[x] = 1
for y in graph[x]:
if y != parent:
dfs(y, x)
cnt[x] += cnt[y]
cntss[x] += cntss[y] + cnt[y]
dfs(0, -1)
max_blocks = max(cntss)
return max_blocks
# 输入样例
n = 3
edges = [(0, 1), (0, 2), (1, 2), (1, 4), (2, 3), (3, 4)]
print(max_connected_blocks(n, edges)) # 输出样例:3
```
```cpp
#include
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); // 递归遍历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';
}
}
```