今天做题发现图论的有向图强连通分量的tarjan算法不会,于是就照着这个博客https://www.byvoid.com/blog/scc-tarjan/学习了一下,感觉非常棒
题目是说等价证明,a,b,c,d互相证明,最少增加的推导数目,a能推出b,b能推出c,c能推出d,d能推出a,最少是4次。首先找出强连通分量,然后把每个强连通分量缩成一个点,得到一个DAG。接下来,设有a个节点(别忘了,这里的每个节点相对于原图的一个强连通分量)的入度为0,b个节点的出度为0,则max{a,b}就是答案。注意特殊情况:当原图已经强连通时,答案是0,而不是1.
定义lowlink(u)为节点u搜索的次序编号(时间戳),pre(u)为u或u的子树能够追溯到的最早的栈中节点的次序号。由定义可以得出,
当lowlink(u)=pre(u)时,以u为根的搜索子树上所有节点是一个强连通分量
#include <iostream>
#include <vector>
#include <stack>
#include <stdio.h>
#include <string.h>
#define maxn 50005
using namespace std;
vector<int>G[maxn];
int pre[maxn],lowlink[maxn],sccno[maxn],dfs_clock,scc_cnt;
//scc_cnt为GCC计数器,sccno[i]为i所在的SCC编号
stack<int> S;
void dfs(int u)
{
pre[u]=lowlink[u]=++dfs_clock;//这里是用来标记这个点处理过,下次dfs的时候就可以跳过这个点了
S.push(u);//每搜索到一个点,就压入栈中
for(int i=0; i<G[u].size(); i++)//遍历与u相连的点
{
int v=G[u][i];
if(!pre[v])//如果这个点没有遍历过(不在栈中),那么就对这个点深搜
{
dfs(v);
lowlink[u]=min(lowlink[u],lowlink[v]);
}
else if(!sccno[v])//(在栈中)
{
lowlink[u]=min(lowlink[u],pre[v]);
}
}
if(lowlink[u]==pre[u])//发现一个根
{
scc_cnt++;
while(1)
{
int x=S.top();//此点以上的栈里面的点全出栈成为一个强连通分量
S.pop();
sccno[x]=scc_cnt;
if(x==u)
break;
}
}
}
void find_scc(int n)
{
dfs_clock=scc_cnt=0;
memset(sccno,0,sizeof(sccno));
memset(pre,0,sizeof(pre));
for(int i=0; i<n; i++)
if(!pre[i])
dfs(i);
}
//以上是模板
int in0[maxn],out0[maxn];
int main()
{
int n,m;
while(~scanf("%d%d",&n,&m))
{
for(int i=0; i<n; i++)
G[i].clear();
for(int i=0; i<m; i++)
{
int u,v;
scanf("%d%d",&u,&v);
u--;
v--;
G[u].push_back(v);
}
find_scc(n);
for(int i=1; i<=scc_cnt; i++)
in0[i]=out0[i]=1;//把每个强连通分量的缩点附成1
for(int u=0; u<n; u++)
for(int i=0; i<G[u].size(); i++)
{
int v=G[u][i];
if(sccno[u]!=sccno[v])//如果这两个不是一堆,那么他们中间肯定有连线,那么这两对的入度出度就不为0,因为我们要求的就是为0的,所以这块要处理一下
in0[sccno[v]]=out0[sccno[u]]=0;//这里处理成0为了和是1的相对照
}
int a=0,b=0;
for(int i=1; i<=scc_cnt; i++)
{
if(in0[i])//为1的时候表示需要去抵消为0的情况,
a++;
if(out0[i])
b++;
}
int ans=max(a,b);//最大抵消即为需要加的最少的边
if(scc_cnt==1)
ans=0;
printf("%d\n",ans);
}
return 0;
}