Tarjan算法小结1——SCC

引入

  许多最短单源路径算法,如Dijkstra,SPFA, floyd, Bellman-Ford等,在运用时只能给出指定点到任意点的最短距离,抑或是给出图中是否有环的信息,并不能准确确定环的个数、包含的点等等。那么我们该如何解决这类问题呢?
  例题:
  (来源:https://www.luogu.org/problemnew/show/2863
  约翰的N (2 <= N <= 10,000)只奶牛非常兴奋,因为这是舞会之夜!她们穿上礼服和新鞋子,别 上鲜花,她们要表演圆舞.
只有奶牛才能表演这种圆舞.圆舞需要一些绳索和一个圆形的水池.奶牛们围在池边站好, 顺时针顺序由1到N编号.每只奶牛都面对水池,这样她就能看到其他的每一只奶牛.
  为了跳这种圆舞,她们找了 M (2< M< 50000)条绳索.若干只奶牛的蹄上握着绳索的一端, 绳索沿顺时针方绕过水池,另一端则捆在另一些奶牛身上.这样,一些奶牛就可以牵引另一些奶 牛.有的奶牛可能握有很多绳索,也有的奶牛可能一条绳索都没有.
  对于一只奶牛,比如说贝茜,她的圆舞跳得是否成功,可以这样检验:沿着她牵引的绳索, 找到她牵引的奶牛,再沿着这只奶牛牵引的绳索,又找到一只被牵引的奶牛,如此下去,若最终 能回到贝茜,则她的圆舞跳得成功,因为这一个环上的奶牛可以逆时针牵引而跳起旋转的圆舞. 如果这样的检验无法完成,那她的圆舞是不成功的.
  如果两只成功跳圆舞的奶牛有绳索相连,那她们可以同属一个组合.
  给出每一条绳索的描述,请找出,成功跳了圆舞的奶牛有多少个组合?
  输入格式:

Line 1: Two space-separated integers: N and M
Lines 2..M+1: Each line contains two space-separated integers A and B that describe a rope from cow A to cow B in the clockwise direction.

  输出格式:

Line 1: A single line with a single integer that is the number of groups successfully dancing the Round Dance.(成功跳了圆舞的奶牛的组数)

  分析:显然此题目标明确,考察求有向图中环的个数。
  知识预备:如果两个顶点可以相互通达,则称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向图的极大强连通子图,称为强连通分量(strongly connected components)。
  Tarjan算法可以在O(N+M)的时间复杂度下找出该图强连通分量的个数及成员。它是一种基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树。搜索时,把当前搜索树中未处理的节点加入一个堆栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。
  因此我们需要如下变量:
  

const int N=10005;//最大奶牛数
struct sd
{
    int node;//当前节点
    vector <int> next;//下一个连接到的节点
};
sd data[N];
int low[N];//栈中最早的与自己在同一环中的位置
int dfn[N];//时间戳,表示dfs时的顺序 
bool vis[N];
int color[N];//染色结果(染色即将同一强连通分量标记为同一号码)
bool gone[N]; //表示某节点是否在栈中
int dye[N];//统计每种颜色的个数
int dfnnum=0,col=0;//col表示染色的种类,dfnnum为遍历的顺序 
stack <int> mystack;//博主喜欢STL,当然手写栈也是可以的

工作原理

  考虑dfs过程中两种可能遇到的环状的情况:
  一.平行联通路:
原创图片,转载注明出处
  此时dfs至点5(栈中元素1,2,3,4,5)——>回溯至点2(栈中元素1,2)——>搜索至点4(栈中元素1,2)——>点4不在当前遍历栈中——>2和4是两个独立的强连通分量。(单独一个点也要算一个强连通分量)
  二.环状联通路
  原创图片,转载注明出处
  
  此时dfs至点5(栈中元素1,2,3,4,5)——>回溯至点4(栈中元素1,2,3,4)——>搜索至点2(栈中元素1,2,3,4)——>点2在当前遍历栈中——2和4是一个强连通分量中的元素——>继续搜索点2,发现无其他可走路径——>退栈直至再次到达点2,将其间所有元素标记为一个颜色。
  
  理解了这两点,整个算法的核心也就不难理解了。
  

inline void tarjan(int p)
{
    gone[p]=true;//入栈
    dfnnum++;
    dfn[p]=dfnnum;
    low[p]=dfn[p];
    vis[p]=true;
    mystack.push(p);
    for(register int i=data[p].next.size()-1;i>=0;i--)
    {
        int tar=data[p].next[i];
        if(dfn[tar]==0)//未访问过的节点 
        {
            tarjan(tar);//继续向下搜索
            low[p]=min(low[tar],low[p]);
            //此步骤是在找到环后将low值改为根节点low值方便统计数量
        }
        else if(vis[tar])//栈中有此点,找到环 
        {
            low[p]=min(low[p],dfn[tar])
        }
    }
    if(dfn[p]==low[p])//回溯涂色,将整个强连通分量涂成一种颜色 
    //dfn[p]==low[p]说明当前节点为子树根节点,递归过程后所有在该强连通
    //分量内的点都已入栈
    {
        vis[p]=false;
        col++;
        color[p]=col;
        while(mystack.top()!=p)//退栈时标记颜色
        {
            color[mystack.top()]=col;
            vis[mystack.top()]=false;//别忘了改回来
            mystack.pop();
        } 
        mystack.pop();//根节点也应该弹出,应注意
    }
}

下面贴上main函数:

int main()
{
    int cow, rope,a,b;
    memset(gone,false,sizeof(gone));
    memset(dye,false,sizeof(dye));
    memset(vis,false,sizeof(vis));
    memset(dfn,false,sizeof(dfn));
    scanf("%d%d",&cow,&rope);
    for(int i=1;i<=rope;i++)
    {
        scanf("%d%d",&a,&b);
        data[a].next.push_back(b);
    }
    for(int i=1;i<=cow;i++)//为了确保没有“离群的牛”每头牛都尝试搜一遍 
    {
        if(!gone[i])
        {
            tarjan(i);
        }
    }
    int ans=0;
    for(int i=1;i<=cow;i++)//确定颜色数量 
    {
        if(dye[color[i]]==0)
        {
            dye[color[i]]++;
            continue;
        }
        if(dye[color[i]]==1)
        {
            dye[color[i]]++;
            ans++;
        }
        else dye[color[i]]++;
    }
    printf("%d",ans);
    return 0;
} 

  ※博主个人认为Tarjan算法较为抽象,推荐大家在纸上模拟推演几遍出栈入栈搜索的操作,才可能熟练准确运用此算法。
  小结1到此结束,下期的小结2我们将会探讨Tarjan算法的缩环为点的操作。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Tarjan算法和Kosaraju算法都是求解有向图强连通分量的算法,它们的时间复杂度都为O(N+M),其中N为图中节点数,M为图中边数。 Tarjan算法的基本思想是通过DFS遍历图中的节点,并在遍历的过程中维护一个栈,用于存储已经遍历过的节点。在遍历的过程中,对于每个节点,记录它被遍历到的时间戳和能够到达的最小时间戳,当一个节点的最小时间戳等于它自身的时间戳时,说明这个节点及其之前遍历到的节点构成了一个强连通分量,将这些节点从栈中弹出即可。 Kosaraju算法的基本思想是先对原图进行一次DFS,得到一个反向图,然后再对反向图进行DFS。在第二次DFS的过程中,每次从未被访问过的节点开始遍历,遍历到的所有节点构成一个强连通分量。 两种算法的具体实现可以参考以下代码: ```python # Tarjan算法 def tarjan(u): dfn[u] = low[u] = timestamp timestamp += 1 stk.append(u) for v in graph[u]: if not dfn[v]: tarjan(v) low[u] = min(low[u], low[v]) elif v in stk: low[u] = min(low[u], dfn[v]) if dfn[u] == low[u]: scc = [] while True: v = stk.pop() scc.append(v) if v == u: break scc_list.append(scc) # Kosaraju算法 def dfs1(u): vis[u] = True for v in graph[u]: if not vis[v]: dfs1(v) stk.append(u) def dfs2(u): vis[u] = True scc.append(u) for v in reverse_graph[u]: if not vis[v]: dfs2(v) # 构建图和反向图 graph = [[] for _ in range(n)] reverse_graph = [[] for _ in range(n)] for u, v in edges: graph[u].append(v) reverse_graph[v].append(u) # Tarjan算法求解强连通分量 dfn = [0] * n low = [0] * n timestamp = 1 stk = [] scc_list = [] for i in range(n): if not dfn[i]: tarjan(i) # Kosaraju算法求解强连通分量 vis = [False] * n stk = [] scc_list = [] for i in range(n): if not vis[i]: dfs1(i) vis = [False] * n while stk: u = stk.pop() if not vis[u]: scc = [] dfs2(u) scc_list.append(scc) ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值