/*
有向图
连通分量:对于分量中任意两点u,v 必然可以从u走到v 且从v走到u
强连通分量:极大连通分量
有向图 → 有向无环图(DAG)
缩点(将所有连通分量缩成一个点)
缩点举例:
o→o→o→o
↑ ↓
o→o→o→o
中间的环缩成一个点
o o
↘ ↗
o
↗ ↘
o o
应用:
求最短/最长路 递推
求强连通分量:dfs
1 树枝边(x,y)
o
/ \
o o
/ / \
o o o
2 前向边(x,y)
o
/ \
o x
/ ↙ \
o y o
3 后向边
o
/ \
o y
/ ↗ \
o x o
4横插边(往已经搜过的路径上的点继续深搜)
因为我们是从左往右搜的 所以一般是x左边分支上的点
o
/ \
o o
/ / \
y ← x o
如果往x右边边分支上的点搜 则属于树枝边
强连通分量:
情况1:
x存在后向边指向祖先结点y 直接构成环
o
/ \
o y
/ ↗/\
o x o
情况2:
x存在横插边指向的点y有指向x和y的公共祖先节点及以上的点的边
再通过根节点往下走到x间接构成环
o
↗ / \
↗ o o
↗ / / \
y ← x o
Tarjan 算法求强连通分量
引入 时间戳(按dfs 回溯的顺序标记)
1
↗ / \
↗ 2 4
↗ / / \
3 ← 5 6
标记上时间后:
dfn[u]dfs遍历到u的时间(如上图中的数字)
low[u]从u开始走所能遍历到的最小时间戳(上图中1,2,3,4,5都是一个环/强连通分量中的
即dfn[1]=low[1]=low[2]=low[3]=low[4]=low[5])
--即u如果在强连通分量,其所指向的层数最高的点
u是其所在的强连通分量的最高点 (上图中dfn[1]=low[1] dfn[6]=low[6])
<=>
dfn[u] == low[u]
树枝边(x,y) 中dfn[y]>dfn[x] low[u]>dfn[u]
前向边(x,y) 中dfn[y]>dfn[x] low[u]>dfn[u]
后向边(x,y) 中dfn[x]>dfn[y] 后向边的终点dfn[u] == low[u]
横插边(x,y) 中dfn[x]>dfn[y]
缩点
for i=1;i<=n;i++
for i的所有邻点j
if i和j不在同一scc中:
加一条新边id[i]→id[j]
缩点操作后变成有向无环图
就能做topo排序了(此时连通分量编号id[]递减的顺序就是topo序了)
因为我们++scc_cnt是在dfs完节点i的子节点j后才判断low[u]==dfn[u]后才加的
那么子节点j如果是强连通分量 scc_idx[j]一定小于scc_idx[i]
本题
当一个强连通的出度为0,则该强连通分量中的所有点都被其他强连通分量的牛欢迎
但假如存在两及以上个出度=0的牛(强连通分量) 则必然有一头牛(强连通分量)不被所有牛欢迎
见下图最右边两个强连通分量
o→o→o
↑
o→o
*/
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 10010, M = 50010;
int n, m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
bool in_stk[N];
int id[N], scc_cnt, Size[N];//每个强连通分的节点个数
int dout[N];
void add(int a, int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
void tarjan(int u)
{
//u的时间戳
dfn[u] = low[u] = ++timestamp;
//把当前点加到栈中 当前点在栈中
stk[++top] = u,in_stk[u] = true;
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i];
if(!dfn[j])//j点未被遍历过
{
tarjan(j);//继续dfs 遍历j
//j也许存在反向边到达比u还高的层,所以用j能到的最小dfn序(最高点)更新u能达到的(最小dfn序)最高点
low[u] = min(low[u],low[j]);
}
//j点在栈中 说明还没出栈 是dfs序比当前点u小的
//则其 1要么是横插边(左边分支的点)
// o
// / \
// j ← u
// 2要么是u的祖宗节点
// j
// ↗/
// u
// 两种情况u的dfs序都比j大 所以用dfn[j]更新low[u]
else if(in_stk[j])
{
low[u] = min(low[u],dfn[j]);//直接用j的时间戳更新u
}
//栈代表当前未被搜完的强连通分量的所有点
}
// ⭐
// 解释一下为什么tarjan完是逆dfs序
// 假设这里是最高的根节点fa
// 上面几行中 fa的儿子节点j都已经在它们的递归中走完了下面9行代码
// 其中就包括 ++scc_cnt
// 即递归回溯到高层节点的时候 子节点的scc都求完了
// 节点越高 scc_id越大
// 在我们后面想求链路dp的时候又得从更高层往下
// 所以得for(int i=scc_cnt(根节点所在的scc);i;i--)开始
// 所以当遍历完u的所有能到的点后 发现u最高能到的点是自己
// 1 则u为强连通分量中的最高点,则以u为起点往下把该强连通分量所有节点都找出来
// 2 要么它就没有环,就是一个正常的往下的点
if(dfn[u]==low[u])
{
int y;
++scc_cnt;//强连通分量总数+1
do
{
y = stk[top--];//取栈顶元素y
in_stk[y] = false;//则y不再在栈中
id[y] = scc_cnt;
Size[scc_cnt] ++;//第scc_cnt个连通块点数+1
}while(y!=u);
//1 因为栈中越高的元素的dfs序越大,那么我们只需要把dfs序比u大的这些pop到u
//即因为最终会从下至上回到u 所以当y==u
//则说明点u所在的所有强连通分量都标记了id
// → u
// / /
// / ne1
// ← ne2
// 因为ne2会在u能到的dfs序里最大的,也就是此时的栈顶
// 那么我们就逐一pop出ne2和ne1
//2 要么它就是一个没有环的点 则该点单点成一个连通分量
}
}
int main()
{
cin >> n >> m;
memset(h,-1,sizeof h);
while(m--)
{
int a,b;
cin >> a >> b;
add(a,b);
}
for (int i = 1; i <= n; i ++ )
if (!dfn[i])
tarjan(i);
//统计新图中点的出度
for (int i = 1;i <= n; i ++ )
for (int j = h[i];j!=-1; j = ne[j])
{
int k = e[j];
int a = id[i], b = id[k];//a,b不为一个连通分量
if (a != b) dout[a] ++ ;//a出度+1 dout[a] += i→k
}
int zeros = 0, sum = 0;//sum 存的所有出度为0的强连通分量的点的数量
for (int i = 1; i <= scc_cnt; i ++ )
if (!dout[i])//如果第i个强连通分量出度==0
{
zeros ++ ;
sum += Size[i];//则加上第i个强连通分量的点的个数
if (zeros > 1)//如果有k>1个出度为0的 则会存在k-1头牛不被所有牛欢迎
{
sum = 0;
break;
}
}
cout << sum;
return 0;
}
为啥要判断一下else if(in_stk[j])呢?为什么会存在不在栈中的情况呢?
后续还有点没有被搜到,应该是因为这个
因为有可能:比如当节点是一个出度为0的点,自己就是一个连通分量,此时dfn[x] == low[x],然后此时就出栈了,其他点遍历的时候如果走到它时,根据算法它已经出栈了
大佬
in_stk[y] = false;//则y不再在栈中
为什么这句话去掉也能ac?
正在思考这个栈的具体运作过程,作者只解释了在栈中的情况,我还不理解不在栈中是什么情况
出栈的原因是:对于u已经走不下去了,所以会判一下是不是最高点,如果是就将所在连通分量的所有点出栈; 再次遍历到不存在栈中的点时是因为有了横插边(i,j),这时候不在栈中是否意味着:
j点已经被其所在连通分量夺走(出栈),且该连通分量的最高点不能走到当前点i,所以i必定不是属于该连通分量的点,所以要判一下是否在栈中,不在的话就不能用dfn[j]更新low[u]???
还没有很懂这个算法,胡诌一番
数据太水的问题
太无助了,求帮忙,越改越像题解,就是老是输出零,应该和题解差不多,有一些和别人不一样的习惯,应该可以看的
优秀的题解,值得我学习~~~我也写了一下题解,对y总的讲解进行了图示记录。
https://www.acwing.com/solution/content/42606/
希望对大家有所帮助!
为什么树枝边(x,y) 中dfn[y]>dfn[x] low[u]>dfn[u]
前向边(x,y) 中dfn[y]>dfn[x] low[u]>dfn[u]的low[u]会大于dfn[u]?
为什么a,b不在一个连通块里面,a就要向b连一条横叉边?
点在栈里,为什么用low[j]更新low[u]也是可以的
因为 dfn[u] = low[u] = ++ts;
Orz
tarjin算法写得太详细了!!大爱
low[u] = min(low[u],dfn[j]);//直接用j的时间戳更新u
为什么改为 low[u] = min(low[u],low[j])也可以过
dfn[u] = low[u] = ++ts;
因该把sum+=size[i]改为==size[i]比较好理解
sum=size[i] 的确更好
讲的真好捏
请问大佬这个部分
“树枝边(x,y) 中dfn[y]>dfn[x] low[u]>dfn[u]
前向边(x,y) 中dfn[y]>dfn[x] low[u]>dfn[u]
后向边(x,y) 中dfn[x]>dfn[y] 后向边的终点dfn[u] == low[u]
横插边(x,y) 中dfn[x]>dfn[y]
”
是如何推出来的呢,为何树枝边和前向边那个部分可以得出 low[u] > dfn[u]呢
说实话,这个题解没举例子,学懂了之后复习还可以,对于初学者是完全看不懂,建议看这篇,,中间给了例子,认真看一遍肯定懂,我当时看这个没懂就看的这个题解https://blog.csdn.net/tjcwt2011/article/details/107156631
同一个点的dfn[u]因该不会小于low[u]吧
请问有大佬知道为什么
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i];
if(!dfn[j])
{
tarjan(j);
low[u] = min(low[u],low[j]);
}
else if(in_stk[j])
{
low[u] = min(low[u],dfn[j]);
}
}
里面
else if(in_stk[j])
{
low[u] = min(low[u],dfn[j]);
}
变成
low[u] = min(low[u],dfn[j]);
也能过吗?画了30min了
你这不没变吗?
我是说去掉判断【流汗】
那不就相当于j在不在栈中我都去用可能更小的dfn[j]更新low[u]了吗,如果不在栈中j肯定是一个比u的dfs序更大的节点啊,更新等于没更新,所以这个判断的作用只是把无意义的更新去掉了而已
明白啦,谢谢
tql
写的不是很详细
有个地方想不通,最后求出度为零的强连通分量内部点个数,y总说强连通分量内部的点都是可以互相到达的那么应该从终点(唯一一个出度为零的点)出发也可以达到强连通分量内部的其他点但是这和终点本身的定义就相互违背了不是吗
是将强连通分量缩成一个点,然后那个点即为终点
明白了,谢谢啦
讲的好好懂 谢谢大佬
写的很好,(我没看懂呃呃呃我太菜了)
写的通俗易懂,必须点赞收藏!