本文出自我的掘金博客, 欢迎大家访问传送门
Tarjan算法
在割点割边以及强连通分量中的应用以及缩点技巧割点: 无向连通图中,去掉一个顶点及和它相邻的所有边,图中的连通分量数增加,则该顶点称为割点。
割边: 无向联通图中,去掉一条边,图中的连通分量数增加,则这条边,称为割边。
想象一下如果用暴力法, 你会如何求解割点或者割边的数目呢?
最容易想到的当然是对于每一个点, 去掉它后对整个图dfs
一遍, 看看连通分量是否增加, 若增加, 则这个去掉的点是割点, 对于割边也是一样的思路, 所以对于暴力法, 你需要`dfs``好多遍.
Tarjan算法
, 你只需要`dfs’一遍即可.这个算法需要用到好几个辅助数组, 下面我来详细介绍它们的作用
int dfn[MAXN];//用来记录一个顶点第一次被访问时的时间戳
int low[MAXN];//用来记录一个顶点不经过它的父亲顶点最高能访问到它的祖先节点中的最小时间戳, 通俗易懂的来说, 就是与结点i连接的所有点中dfn[]值最小的一个。
int cut[MAXN];//用来记录该点是否是割点, 因为一个割点可能多次被记录
//这是链式前向星, 用来存储边的一个数据结构
int head[MAXN], cnt;
struct Edge {
int to;
int nxt;
} e[MAXM];
dfs
遍历, 但由于图可能不是连通图, 故外面要套一个for
, 这可以理解吧for (int i = 1; i <= n; i++) {
if (!dfn[i]) {
tarjan(i, i);
}
}
这段代码中我没有另外使用一个
vis
数组记录节点是否被访问过, 因为一个节点若被访问过, 那么dfn
数组肯定不为0, 故可以通过dfn
数组的值来判断该节点是否已经被访问过
tarjan
函数第二个参数传入的是父亲节点, 根节点是个例外, 父亲节点就当做它本身, 便于后面的特判
tarjan
函数对于一个根节点, 判断它是不是割点很简单, 如果它有两个或者两个以上的子树, 那么去掉根节点这几颗子树就不连通了, 故这时可以判定根节点是割点.
对于一个非根节点U, 相对要麻烦一点.
接下来说一说这几个数组的值得更新操作
dfn
数组, 只会在它第一次访问的时候赋值, 其值等于访问时的时间戳, 而且后续其值永远不会改变low
数组, 它在第一次访问一个顶点时也会赋值, 也是第一次访问时的时间戳, 但后续操作它的值可能会改变, 即假设当前顶点为u,则默认low[u]=dfn[u]
,即最早只能回溯到自身。有一条边(u, v),如果v未访问过,继续DFS,DFS完之后,low[u]=min(low[u]
, low[v]);如果v访问过(且u不是v的父亲),就不需要继续DFS了,一定有dfn[v],low[u]=min(low[u], dfn[v])。
说说对于根节点的特判
tarjan
函数传入了两个参数, 挡在主函数中第一次调用时, 是tarjan(i, i)
, 我说过, 第二个参数代表的是父亲节点, 但i
的父亲节点怎么会是它本身呢? 嘿嘿, 这就是一个用来特判根节点的技巧啦, 如果对于一个顶点u
, 有u == fa
, 那么u
就是根节点, 下面上函数代码, 您可以慢慢体会, 有详细的注释//fa代表父亲节点
void tarjan (int u, int fa) {
dfn[u] = low[u] = ++id; //id代表时间戳
int child = 0; //child代表子树数目, 只有u是根节点时, 这个变量才会起作用哟
for (int i = head[u]; i; i = e[i].nxt) { //链式前向星的遍历操作
int to = e[i].to;
if (!dfn[to]) {
//如果顶点to没有访问过, 那么继续dfs
tarjan(to, fa); //传入当前节点以及父节点作为参数
low[u] = min(low[u], low[to]); //回溯的时候更新low数组的值
if (low[to] >= dfn[u] && u != fa) { //注意这里特判了不是根节点
cut[u] = 1;//标记为割点
}
if (u == fa) { //特判是根节点
child++; //子树数目加1
}
}
low[u] = min(low[u], dfn[to]); //这里的更新操作不要漏掉了
}
if (child >= 2 && u == fa) { //若根节点的子树数目大于或等于2
cut[u] == 1; //则根节点也是割点
}
}
cut
数组, 更新割点的数目for (int i = 1; i <= n; i++) {
if (cut[i]) {
ans++;
}
}
cout << ans << endl;
return 0;
tarjan算法
来寻找割边割点的数目, 我们还可以用它来寻找强连通分量, 这时相对于割点割边会复杂许多, 希望您能耐心看完根据离散数学中的定义
1,强连通:在一个有向图中,如果两个点可以互相到达,就称为这两个点强连通。
2,强连通图:在一个有向图中,如果任意两个点强连通,就把这个图称为强连通图。
3,强连通分量:在一个非强连通图中的最大强连通子图,称为强连通分量。
关于强连通, 您只需要知道这么多就够了.
这一题需要用到的数据结构比上一题要多, 我来详细介绍一下
int low[MAXN]; //同上一题
int dfn[MAXN]; //同上一题
int stack_[MAXN]; //栈
int exist[MAXN]; //判断第i个元素是否在栈中
int color[MAXN]; //用于对不同的连通分量染色的数组
//链式前向星
struct Cow {
int to;
int nxt;
} cow[MAXM];
//注:下面这两个数组是针对我给的模板题而言的
int num[MAXN]; //用于记录每个连通分量有多少格元素的数组
int outDgree[MAXN]; //用于记录出度的数组
强连通分量模板题, 希望您能好好做一下这一题帮助您理解这个算法
我觉得这一段伪代码很好的总结了其算法流程 出处
tarjan(u)
{
DFN[u]=Low[u]=++Index // 为节点u设定次序编号和Low初值
Stack.push(u) // 将节点u压入栈中
for each (u, v) in E // 枚举每一条边
if (v is not visted) // 如果节点v未被访问过
tarjan(v) // 继续向下找
Low[u] = min(Low[u], Low[v])
else if (v in S) // 如果节点v还在栈内
Low[u] = min(Low[u], DFN[v])
if (DFN[u] == Low[u]) // 如果节点u是强连通分量的根
repeat
v = S.pop // 将v退栈,为该强连通分量中一个顶点
print v
until (u== v)
}
dfn
和low
, 如果一个点u无路可走了, 那么若dfn[u] == low[u]
, 就弹出栈顶到u
的元素, 这些元素是属于一个强连通分量内的. 用语言很难描述清楚, 来模拟一下吧.下方流程出处
对于如下一幅图
寻找它的强连通分量
上述过程用代码描述是这样的
void dfs(int x) {
dfn[x] = low[x] = ++tot; //都初始化为x
stack_[++top] = x; //点x入栈
exist[x] = 1; //表示点x在栈中
for (int i = head[x]; i; i = cow[i].nxt) {
if (!dfn[cow[i].to]) {
//如果与它相连的这个点还没有被遍历
dfs(cow[i].to);
low[x] = min(low[x], low[cow[i].to]);
} else if (exist[cow[i].to]) {
//如果与它相连的这个点在栈中, 表示它们在同一个连通分量中
low[x] = min(low[x], low[cow[i].to]);
}
}//end for
if (low[x] == dfn[x]) {
//如果节点x是强连通分量的根
id++; //每个连通分量的标号
do {
color[stack_[top]] = id;
num[id]++;
exist[stack_[top]] = 0;
} while (x != stack_[top--]);
}
}
这段流程相当的清楚. 现在您应该已经懂得了如何求得强连通分量了.
所以有了上述的思想, 奶牛这一题就迎刃而解了, 当我们把每一个连通分量都进完缩点时,我们只需要在剩下的这张图中找明星就行了,如果这张图里的每一个点都是强连通,那么显然这张图里的牛都是明星。否则我们可以找到出度为0的牛,这样可以保证其他所有的牛都喜欢它,但是如果有2个以上的出度为0的牛,那么很显然这张图里是不存在明星的。
在我们已经染色完成并用缩点法转化后, 对于一个缩点的出度只用看颜色i的顶点的出边是否是另一种颜色, 是的话颜色i的强连通分量出度+1
for (int i = 1; i <= n; i++) {
for (int j = head[i]; j; j = cow[j].nxt) {
if (color[i] != color[cow[j].to]) {
outDgree[color[i]]++;
}
}
}
首先, 判断是否有明星, 若只有一个缩点的出度为0, 那么该缩点就是明星.
若有超过一个缩点的出度不为0, 那么是没有明星的.
若一个缩点为明星, 那么代表这个缩点所代表的强连通分量内的奶牛全部是明星
for (int i = 1; i <= id; i++) {
if (outDgree[i] == 0) {
if (ans) {
cout << 0;
return 0;
}
ans = i;
}
}
cout << num[ans];
return 0;
然后就AC
啦
本文参考博客:
zhy_1412186887 的博客
有向图强连通分量的Tarjan算法
Tarjan算法:求解图的割点与桥(割边)