图论基础算法之割边割点,没想到为了调整各种细节还花了我不少时间
无向图中的tarjan
算法说明
图论算法没有图很难说明,在此只做最简单的叙述就好
首先对于一张图进行DFS,由DFS过程引入三个概念:
- 时间截:节点进行DFS的时间顺序编号
- 搜索树:深搜路径的生成树
- 追溯值:一个节点不通过父节点能够到达其他节点的最早时间截
分别用dfs[n]dfs[n]dfs[n]、low[n]low[n]low[n]表示时间截和追溯值
很容易可以想象出,如果:
-
存在一个节点的子节点满足:
dfs[pos]<=low[child]dfs[pos]<=low[child]dfs[pos]<=low[child]
则说明有至少一个由子节点构成的连通块除了通过搜索树,没有其他路径回到树的根节点
所以该点删除后必然产生一个独立的连通块,此时该点为割点tips:当pos为根节点时,需要存在两个child节点满足条件才可判断根节点为割点
-
存在一条边满足:
dfs[pos]<low[child]dfs[pos]<low[child]dfs[pos]<low[child]
则说明通过该边的子节点的子搜索树中没有其他能通往根节点的路径
删掉该边后产生新的连通块,所以此边为割边
代码实现
原理很好理解,但是在代码实现和做题过程中,我遇到了不少的问题,接下来对犯过的错误和需要注意的点进行记录
luogu模板题
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <queue>
#include <cmath>
#include <cstring>
#include <utility>
using namespace std;
struct Edge
{
int from;
int to;
int next;
} ed[200005];
int head[20005];
int low[20005];
int dfs[20005];
bool isAns[20005];
int num;
int n, m;
int org = 1;
int sum[20005];
int ans = 0;
void add(int f, int t)
{
ed[++num].from = f;
ed[num].to = t;
ed[num].next = head[f];
head[f] = num;
}
int times = 0;
bool flag_1 = 0;
void tarjan(int pos)
{
dfs[pos] = low[pos] = ++times;
for (int i = head[pos]; i; i = ed[i].next)
{
int y = ed[i].to;
if (dfs[y])
{
low[pos] = min(low[pos], dfs[y]);
continue; //当搜索树到达边界后,不能继续搜索下去,也不能
//对叶节点进行统计
}
else
{
tarjan(y);
low[pos] = min(low[pos], low[y]);
}
if (low[y] >= dfs[pos])
{
if (pos == org)
{
if (flag_1 == 0)
{
flag_1 = 1;
}
else if (flag_1 == 1 && isAns[pos] == 0)//已经确认的点不能再统计一次答案
//否则会重复增加ans的数量
{
isAns[pos] = 1;
ans++;
}
}
else if (isAns[pos] == 0)
{
isAns[pos] = 1;
ans++;
}
}
}
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= m; i++)
{
int u, v;
cin >> u >> v;
add(u, v);
add(v, u);
}
for (int i = 1; i <= n; i++)//题目中的图不一定联通,对于每个点都要检查
{
if (dfs[i] == 0)
{
flag_1 = 0;
times = 0;
org = i;//每次确定了连通块后,需要修改根节点和flag
tarjan(i);
}
}
cout << ans << endl;
for (int i = 1; i <= n; i++)
{
if (isAns[i])
{
cout << i << ' ';
}
}
}
注释中记录的四个关键点确实让我浪费了不少时间调试找错
双连通分量
当一个图中不存在割边或割点时,我们称其为边双连通图或点双连通图
满足条件的子图我们称之为分量
边双连通分量求法
只需要求出所有割边,删去割边后剩下的就是边双连通分量
e-DCC缩点
把所有e-DCC作为节点,割边作为边,可以产生一棵树
点双连通分量的求法
维护一个栈,每遇到一个新的节点就入栈
最后判断节点是否为割点时,如果是割点,则出栈直到该节点出栈,所有出栈节点构成点双连通分量
然后可以进行缩点:
v-DDC缩点
割点可能同时被多个点双连通分量包括,所以v-DDC缩点的策略是把割点作为一个节点,然后每个点双连通分量作为一个节点和割点相连
有向图中的Tarjan
有向图中选取一个根节点可以到达其他所有点,,我们称之为流图
在有向图中讨论割边割点是一个很别扭的事情,我鲜有看见相关的文章
相对应的,在有向图中更常见到的是强连通分量——任意两个点可以自由通达的最大强连通子图(分量包括的含义就是最大连通的子图)
有向图中的Tarjan类似于无向图,也是一个最小追溯值的一个计算
dfs的过程中维护一个栈
计算出最小追溯值后,如果low[pos]==dfn[pos]low[pos]==dfn[pos]low[pos]==dfn[pos],则一直出栈直到pos出栈为止,所有出栈的节点构成一个环,即为一个强连通分量
Luogu P3387 缩点
#include <iostream>
#include <cstdio>
#include <queue>
#include <stack>
using namespace std;
struct Edge
{
int from;
int to;
};
deque<Edge> head[10001];
int degree[10001];
deque<Edge> head_new[10001];
int node_w_new[10001];
int map[10001];
int cnt = 0;
void add(int f, int t)
{
head[f].push_back(Edge{f, t});
}
stack<int> node;
int node_w[10001];
int dfn[10005];
int low[10005];
bool inTheStack[10001];
int count = 0;
void tarjan(int p)
{
count++;
node.push(p);
dfn[p] = count;
low[p] = count;
inTheStack[p] = 1;
for (const auto &el : head[p])
{
int y = el.to;
if (dfn[y] && inTheStack[y])//y must be in the stack
{
low[p] = min(low[p], dfn[y]);
}
else if (!dfn[y])
{
tarjan(y);
low[p] = min(low[p], low[y]);
}
}
if (low[p] == dfn[p])
{
++cnt;
while (node.top() != p)
{
int temp = node.top();
node.pop();
inTheStack[temp] = 0;
node_w_new[cnt] += node_w[temp];//add up the weight
map[temp] = cnt;//map to its belonging
}
int temp = node.top();
node.pop();
inTheStack[temp] = 0;
node_w_new[cnt] += node_w[temp];
map[temp] = cnt;
}
}
int cal(int p)
{
int ans = 0;
for (const auto &el : head_new[p])
{
ans = max(ans, cal(el.to));
}
return ans + node_w_new[p];
}
int main()
{
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
map[i] = i;
}
for (int i = 1; i <= n; i++)
{
cin >> node_w[i];
}
int f, t;
for (int i = 1; i <= m; i++)
{
cin >> f >> t;
add(f, t);
}
for (int i = 1; i <= n; i++)
{
if (!dfn[i])
tarjan(i);
}
// cout << inTheStack[2]<< endl;
for (int i = 1; i <= n; i++)
{
for (const auto &el : head[i])
{
if (map[el.from] == map[el.to])
{
continue;
}
head_new[map[el.from]].push_back(Edge{map[el.from], map[el.to]});
//this is such an amazing way to deal with each edge
degree[map[el.to]]++;
}
}
int ans = 0;
for (int i = 1; i <= cnt; i++)
{
if (!degree[i])
{
ans = max(ans, cal(i));
}
}
cout << ans << endl;
}
- 将每个节点在缩点后的新图中建立映射关系,然后就可以直接更新每一条边在新图中的对应链接,非常巧妙的方式
740



被折叠的 条评论
为什么被折叠?



