Tarjan SCC

Tarjan SCC

模板

以下部分内容转载自http://kmplayer.iteye.com/blog/604532

  1. 在有向图G中,如果两个顶点间至少存在一条路径,称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。非强连通图有向图的极大强连通子图,称为强连通分量(strongly connected component)。
  2. 下图中,子图{1,2,3,4}为一个强连通分量,因为顶点1,2,3,4两两可达。{5},{6}也分别是两个强连通分量。
  3. Tarjan算法是基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树。搜索时,把当前搜索树中未处理的节点加入一个堆栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。
    1
    定义几个关键数组:
    int DFN[MAX]; //记录节点u第一次被访问时的步数
    int LOW[MAX]; //记录与节点u和u的子树节点中最早的步数
    接下来是对算法流程的演示。
    从节点1开始DFS,把遍历到的节点加入栈中。搜索到节点u=6时,DFN[6]=LOW[6],找到了一个强连通分量。退栈到u=v为止,{6}为一个强连通分量。
    2
    返回节点5,发现DFN[5]=LOW[5],退栈后{5}为一个强连通分量。
    3
    返回节点3,继续搜索到节点4,把4加入堆栈。发现节点4向节点1有后向边,节点1还在栈中,所以LOW[4]=1。节点6已经出栈,(4,6)是横叉边,返回3,(3,4)为树枝边,所以LOW[3]=LOW[4]=1。
    4
    继续回到节点1,最后访问节点2。访问边(2,4),4还在栈中,所以LOW[2]=DFN[4]=5。返回1后,发现DFN[1]=LOW[1],把栈中节点全部取出,组成一个连通分量{1,3,4,2}。
    5
    至此,算法结束。经过该算法,求出了图中全部的三个强连通分量{1,3,4,2},{5},{6}。
  4. 分析:
    运行Tarjan算法的过程中,每个顶点都被访问了一次,且只进出了一次堆栈,每条边也只被访问了一次,所以该算法的时间复杂度为 O(N+M)
  5. 代码:
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. 假缩点:由于题目较为简单,缩点后不会访问图,可以只用统计入度出度的方式逻辑缩点,而不在物理上建图 例1 例2
  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;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值