算法提高课笔记

理论基础

边的双连通分量 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 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); // 递归调用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';

}

}

```