Tarjan SCC
模板
以下部分内容转载自http://kmplayer.iteye.com/blog/604532
- 在有向图G中,如果两个顶点间至少存在一条路径,称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。非强连通图有向图的极大强连通子图,称为强连通分量(strongly connected component)。
- 下图中,子图{1,2,3,4}为一个强连通分量,因为顶点1,2,3,4两两可达。{5},{6}也分别是两个强连通分量。
- Tarjan算法是基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树。搜索时,把当前搜索树中未处理的节点加入一个堆栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。
定义几个关键数组:
int DFN[MAX]; //记录节点u第一次被访问时的步数
int LOW[MAX]; //记录与节点u和u的子树节点中最早的步数
接下来是对算法流程的演示。
从节点1开始DFS,把遍历到的节点加入栈中。搜索到节点u=6时,DFN[6]=LOW[6],找到了一个强连通分量。退栈到u=v为止,{6}为一个强连通分量。
返回节点5,发现DFN[5]=LOW[5],退栈后{5}为一个强连通分量。
返回节点3,继续搜索到节点4,把4加入堆栈。发现节点4向节点1有后向边,节点1还在栈中,所以LOW[4]=1。节点6已经出栈,(4,6)是横叉边,返回3,(3,4)为树枝边,所以LOW[3]=LOW[4]=1。
继续回到节点1,最后访问节点2。访问边(2,4),4还在栈中,所以LOW[2]=DFN[4]=5。返回1后,发现DFN[1]=LOW[1],把栈中节点全部取出,组成一个连通分量{1,3,4,2}。
至此,算法结束。经过该算法,求出了图中全部的三个强连通分量{1,3,4,2},{5},{6}。 - 分析:
运行Tarjan算法的过程中,每个顶点都被访问了一次,且只进出了一次堆栈,每条边也只被访问了一次,所以该算法的时间复杂度为 O(N+M) 。 - 代码:
struct E{int from,to,next;} e[MAXM+1];int ecnt,G[MAXN+1];
void addEdge(int u,int v){e[++ecnt]=(E){u,v,G[u]};G[u]=ecnt;}
int gcnt,belong[MAXN+1];//缩成的点数,这个点属于所点后的那个点
stack<int> st;bool inSt[MAXN+1];
int dfn[MAXN+1],low[MAXN+1];int vcnt;
void tarjan(int u)
{
int i;
dfn[u]=low[u]=++vcnt;
st.push(u);inSt[u]=true;
for(i=G[u];i;i=e[i].next)
{
int v=e[i].to;
if(!dfn[v])
{
tarjan(v);
low[u]=min(low[u],low[v]);
}
else
if(inSt[v])
low[u]=min(low[u],dfn[v]);
}
if(dfn[u]==low[u])
{
++gcnt;
do
{
u=st.top();st.pop();
inSt[u]=false;
belong[u]=gcnt;
}
while(dfn[u]!=low[u]);
}
}
缩点
缩点有两种方式:
- 假缩点:由于题目较为简单,缩点后不会访问图,可以只用统计入度出度的方式逻辑缩点,而不在物理上建图 例1 例2
- 真缩点:由于缩点之后还需要进行复杂的操作(如树上动规),重新建一个缩过点的图 例3
例题
例1.USACO5.3 Network_of_Schools
Tarjan的简单应用
由于所点后续操作十分简单,我们没必要再费内存建另一张缩过点后的图,只需统计缩过后的每一个点的入度与出度。
由于所有入度为零的学校必须得到真传才可以,故A问题的答案即为所有入度为零的点;对于B问题,不仅需要入度为零的点得到真传,还要保证所有点都能肩负起传给其他所有学校的历史重任,即所有点的出度都不能为零。
事实上,只要上述一个条件满足,则另一个条件一定满足(因为只要一个条件满足,那么这张缩过点的图也变成一张连通图了)。故B问题的答案即为入度为零的点数和出度为零的点数两者的最大值。
有一点需要注意,如果最后缩成一个点,需要进行特殊处理。
/*
ID:zhangch33
Task:schlnet
LANG:C++
*/
#include<iostream>
#include<cstdio>
#include<stack>
#define max(a,b) (a>b?a:b)
#define min(a,b) (a<b?a:b)
using namespace std;
const int MAXN=100,MAXM=10000;
int N;
struct E{int from,to,next;} e[MAXM+1];int ecnt,G[MAXN+1];
void addEdge(int u,int v){e[++ecnt]=(E){u,v,G[u]};G[u]=ecnt;}
int gcnt,belong[MAXN+1];//缩成的点数,这个点属于所点后的那个点
stack<int> st;bool inSt[MAXN+1];
int dfn[MAXN+1],low[MAXN+1];int vcnt;
void tarjan(int u)
{
int i;
dfn[u]=low[u]=++vcnt;
st.push(u);inSt[u]=true;
for(i=G[u];i;i=e[i].next)
{
int v=e[i].to;
if(!dfn[v])
{
tarjan(v);
low[u]=min(low[u],low[v]);
}
else
if(inSt[v])
low[u]=min(low[u],dfn[v]);
}
if(dfn[u]==low[u])
{
++gcnt;
do
{
u=st.top();st.pop();
inSt[u]=false;
belong[u]=gcnt;
}
while(dfn[u]!=low[u]);
}
}
int inDgr[MAXN+1],outDgr[MAXN+1];
int main()
{
freopen("schlnet.in","r",stdin);
freopen("schlnet.out","w",stdout);
int i;
scanf("%d",&N);
for(i=1;i<=N;i++)
{
int u=i,v;
scanf("%d",&v);
while(v)
{
addEdge(u,v);
scanf("%d",&v);
}
}
for(i=1;i<=N;i++)
if(!dfn[i]) tarjan(i);
for(i=1;i<=ecnt;i++)
if(belong[e[i].from]!=belong[e[i].to])
{
++inDgr[belong[e[i].to]];
++outDgr[belong[e[i].from]];
}
int ansA=0,ansB=0;
for(i=1;i<=gcnt;i++)
{
if(inDgr[i]) ++ansA;
if(outDgr[i]) ++ansB;
}
ansB=max(ansA,ansB);
if(gcnt==1) printf("1\n0\n");
else printf("%d\n%d\n",ansA,ansB);
return 0;
}
例2.HAOI2006 受欢迎的牛
这道题更水了,不过因为是本省省选真题,还是说一下:
很明显首先需要缩点,所完点后每一个强联通分量里的牛形成一个命运共同体,也就是说只要缩完后的点是受欢迎的,那么这里边所有的牛都是,我们想到要存一下每一个命运共同体里的牛的数量。
接下来,我们注意到当且仅当缩完后的点中有且只有一个点的出度为一时,这个点中的牛是受欢迎的(如果有两个这样的点,那么整张图就不存在受欢迎的牛),而答案就是这个点中牛的数目。
代码依然不长,在省选中实为送分题
#include<iostream>
#include<cstdio>
#include<stack>
using namespace std;
const int MAXN=1000,MAXM=5000;
int N,M;
struct E{int from,to,next;} e[MAXM+1];int ecnt,G[MAXN+1];
void addEdge(int u,int v){e[++ecnt]=(E){u,v,G[u]};G[u]=ecnt;}
stack<int> st;
bool inSt[MAXN+1];
int belong[MAXN+1],gcnt,memNum[MAXN+1];
int dfn[MAXN+1],low[MAXN+1];
int vcnt;
void tarjan(int u)
{
int i;
dfn[u]=low[u]=++vcnt;
st.push(u);inST[u]=true;
for(i=G[u];i;i=e[i].next)
{
int v=e[i].to;
if(!dfn[v])
{
tarjan(v);
low[u]=min(low[u],low[v]);
}
else
if(inst[v])
low[u]=min(low[u],dfn[v]);
}
if(dfn[u]==low[u])
{
++gcnt;
do
{
u=st.top();st.pop();
inSt[u]=false;
belong[u]=gcnt;
++memNum[gcnt];
}while(low[u]!=dfn[u]);
}
}
int outDgr[MAXN+1];
int main()
{
int i;
scanf("%d%d",&N,&M);
for(i=1;i<=m;i++)
{
int u,v;
scanf("%d%d",&u,&v);
addEdge(u,v);
}
for(i=1;i<=N;i++)
if(!dfn[i]) tarjan(i);
for(i=1;i<=M;i++)
if(belong[e[i].from]!=belong[e[i].to])
++outDgr[e[i].to];
int ans;
bool exist=false;
for(i=1;i<=gcnt;i++)
if(!outDgr[i])
if(exist)
{
exist=false;
break;
}else exist=true,ans=i;
if(exist) printf("%d\n",memNum[ans]);
else printf("0\n");
return 0;
}
例3.HAOI2010 软件安装
这道题难道是传说中的依赖背包?我也不清楚。不过很明显的是一定要先用tarjan缩点,然后再跑树上背包。这里有一个技巧,为了成功地跑树上背包,我们将一个超级源点连接到所有入度为零的点,把森林建成一棵树,这样答案就在源点上了。
#include<iostream>
#include<cstdio>
#include<stack>
using namespace std;
const int MAXN=100,MAXE=10000,MAXM=500;
int M,N;
struct E{int from,to,next;};
struct CFS//Chain Forward Star
{
E e[MAXE+1];
int ecnt,G[MAXN+1];
void addEdge(int u,int v){e[++ecnt]=(E){u,v,G[u]};G[u]=ecnt;}
} G1,G2;
int weight[MAXN+1],value[MAXN+1];
stack<int> st;bool inSt[MAXN+1];
int dfn[MAXN+1],low[MAXN+1],times;
int belong[MAXN+1];
struct S{int w,v;} scc[MAXN+1];int scnt;
void tarjan(int u)
{
int i;
dfn[u]=low[u]=++times;
inSt[u]=true;
for(i=G1.G[i];i;i=G1.e[i].next)
{
int v=G1.e[i].to;
if(!dfn[v])
{
tarjan(v);
low[u]=min(low[u],low[v]);
}else
if(inSt[v])
low[u]=min(low[u],dfn[v]);
}
if(low[u]==dfn[u])
{
++scnt;
do
{
u=st.top();st.pop();
inSt[u]=false;
belong[u]=scnt;
scc[scnt]={weight[u],value[u]};
}
while(low[u]!=dfn[u]);
}
}
bool inDgr[MAXN+1];
inline void rebuild()
{
int i;
for(i=1;i<=G1.ecnt;i++)
if(belong[G1.e[i].from]!=belong[G1.e[i].to])
{
G2.addEdge(belongG1.e[i].from],belong[G1.e[i].to]);
inDgr[belong[G1.e[i].to]]=true;
}
}
int dp[MAXN+1][MAXM+1];
void dfs(int u)
{
int i,j,k;
//dfs过程
for(i=G2.G[u];i;i=G2.e[i].next)
{
dfs(G2.e[i].to);
for(j=M-scc[u].w;j>=0;j--)
for(k=0;k<=j;k++)
dp[u][j]=max(dp[u][j],dp[u][k]+dp[G2.e[i].to][j-k]);
}
//更新本节点的值
for(i=M;i>=0;i--)
if(i>=scc[u].w)
dp[u][i]=dp[u][j-scc[u].w]+scc[u].v;
else dp[u][i]=0;
//
}
int main()
{
int i;
scanf("%d%d",&N,&M);
for(i=1;i<=N;i++)
scanf("%d",&weight[i]);
for(i=1;i<=N;i++)
scanf("%d",&value[i]);
for(i=1;i<=N;i++)
{
int u=i,v;
scanf("%d",&v);
while(v)
{
G1.addEdge(u,v);
scanf("%d",&v);
}
}
for(i=1;i<=n;i++)
if(!dfn[i]) tarjan(i);
rebuild();
//
for(i=1;i<=scnt;i++)
if(!inDgr[i])
{
inDgr[i]=true;
G2.addEdge(0,i);
}
dfs(0);
printf("%d\n",dp[0][M]);
return 0;
}