Tarjan(塔杨)算法详解

前言

Tarjan(塔杨)算法其实不是很复杂,对于图这一块而言,Tarjan算法还是很有作用的,咱们现在一起来看看这个算法是啥东西以及咋实现。

简介

tarjan算法最直接的作用就是求图的联通分量。啥是联通分量呢?请你出去搞清楚再进来,谢谢。(啥是图?别进来了)
这算法如同基石一般,在此基础上我会研究联通分量的作用,后话了。
如果有人可以做个gif图,形象展示一下tarjan算法的过程,那就很棒了,可惜我不会。

教学代码-有向图的强联通分量

咱们先来个教学篇,讲一下这个tarjan算法的实现流程。
先给你一张单向图:
随便拷贝的
首先我得描述这些关系:

    vector<pair<int, int> >Relations;
    Relations.push_back(make_pair(1, 3));
    Relations.push_back(make_pair(1, 2));
    Relations.push_back(make_pair(2, 4));
    Relations.push_back(make_pair(3, 5));
    Relations.push_back(make_pair(3, 4));
    Relations.push_back(make_pair(4, 6));
    Relations.push_back(make_pair(5, 6));
    Relations.push_back(make_pair(4, 1));

接着看你喜欢了,我用邻接表保存这些单向关系

    vector<int> rel1, rel2, rel3, rel4, rel5, rel6;
    vector<int>* details[6] = {&rel1, &rel2, &rel3, &rel4, &rel5, &rel6};
    for (int i = 0; i < Relations.size(); ++i)
    {
        details[Relations[i].first-1]->push_back(Relations[i].second-1);
    }

然后我再定义几个要维护的数组还有一个stack

    int Vis[6] = { 0 };     //0:not visited        1:in stack       2:leave stack
    int Dfn[6] = { 0 };
    int Low[6] = { 0 };
    int TimeStamp = 1;
    stack<int> sk;

这个Vis数组就有趣了,0表示没有访问过,1表示还在stack中,2表示已经出stack
timeStamp表示全局时间,没事可以暂时不理解。
Dfn数组用来记录每次的timeStamp
Low数组就没那么省心了。每个节点都或多或少有一片以自己为根节点的树。Low就代表了,这棵树能触碰到的最小的Dfn,加上dfs的帮助,于是这个Low就可以一直上传上去。
我相信你肯定已经懵逼了,毕竟这样说,太晦涩难懂。我这么说吧:啥叫强联通呢?从一个节点出发,兜兜转转,最后回到了自己。tarjan算法就是一种基于深度优先的算法,从起始节点一直找下去,最后找到了自己,那咋知道找到自己了呢?肯定是深度优先算法在回退的时候要一层层的将“哦吼,我找到最开始那个节点啦”这件事情带回去。
这样一来你就清楚了,tarjan算法对一个节点,要干点啥了:
tarjan:
1.先将Vis数组的下标置为已经访问,但是还没出stack(1),同时将Dfn和Low维护一下(就按照timeStamp走就行),顺便将这个节点入栈;
2.开始处理每一个自己的关系指向。
2.1如果这个关系指向是Vis=2,也就是早就经历过入栈出栈,那就别管了,pass
2.2如果这个关系指向是Vis=0,那就对其执行Tarjan算法(对没错,深度优先嘛,就递归了),反正执行完成后你总得出来(出来以后真的有可能也成2了,但是没关系,这个时候变2,那也是OK的,至少和自己有关系)。等出来之后,就直接借此更新Low下标,棒
2.3如果这个关系指向是Vis=1,也就是说,已经访问过了,那就没必要继续递归,直接借此更新Low下标,棒
3.深度,这事儿已经在第二步都做完了,下面咱们得开始退出了。这里退出的时候要伴随着可能发生的pop(每次pop出来的都是一组强联通分量)。像节点6走了一圈发现没啥,就要出去,很显然6是强联通分量,于是6pop出去了,同时更新一些Vis=2,开心。
轮到5了,显然和6一样的下场。
轮到3了,3找到4
4找到1,发现1的Low比自己的Low要Low,于是4更新了Low。
回到3,3发现4的Low变了,于是3也更新了Low。
回到1
1找到2
2找到4(因为4没pop出去,因为4的Low和dfn不相等),于是更新了2的Low
2也没法pop出去
回到1
1的Low=Dfn,于是开始慢慢pop,全部一个个pop出来,直到将1自己pop出来,这些作为一组强联通分量,结束

void Tarjan(int index, stack<int>& sk, int Vis[], int& TimeStamp, int Dfn[], int Low[], vector<int>* details[], vector<vector<int>>& res)
{
    //1.先将index压入stack并修改visited数组
    Dfn[index] = TimeStamp;
    Low[index] = TimeStamp;
    Vis[index] = 1;
    TimeStamp++;
    sk.push(index);
    //2.枚举每一条边
    for (int i = 0; i < details[index]->size(); ++ i)
    {
        if (Vis[details[index]->at(i)] == 0)
        {
            Tarjan(details[index]->at(i), sk, Vis, TimeStamp, Dfn, Low, details, res);
            Low[index] = (Low[index] < Low[details[index]->at(i)]) ? Low[index] : Low[details[index]->at(i)];
        }
        else if (Vis[details[index]->at(i)] == 1)
        {
            Low[index] = (Low[index] < Dfn[details[index]->at(i)]) ? Low[index] : Dfn[details[index]->at(i)];
        }
    }
    //3.开始回退并弹出结果
    if (Dfn[index] == Low[index])
    {
        vector<int> temp;
        while (sk.top() != index)
        {
            temp.push_back(sk.top());
            Vis[sk.top()] = 2;
            sk.pop();
        }
        temp.push_back(sk.top());
        Vis[sk.top()] = 2;
        sk.pop();
        res.push_back(temp);
    }
}

讲的细碎,淦。

教学代码-点双联通分量

点双联通图,就是这张无向联通图,不存在割点。给你看张图就明白:
在这里插入图片描述
这个B点就将整张图分成两部分,对吧,B点就是割点。割点将无向联通图分成几个独立部分,作为点双连通分量。
咋用tarjan算法求这个东西呢?
这也非常简单,需要用到入栈出栈(边双不需要stack),然后判断条件是Dfn[u] <= Low[v],很好理解。
代码引用自CSDN@xyyxyyx,侵删。

void Tarjan(int u)
{
    dfn[u] = low[u] = ++idx;
    if (rt == u && point[u] == -1){ // 孤立的点特判
        dccn++;
        dcc[dccn].clear();
        dcc[dccn].push_back(u);
        return;
    }
    stk[++st] = u;
    for (int i = point[u]; i != -1; i = edge[i].nxt){
        int v = edge[i].v;
        if (!dfn[v]){
            Tarjan(v);
            low[u] = min(low[u], low[v]);
            // 因为v在u的子树内,所以low[v]可以用于更新low[u]
            if (low[v] >= dfn[u]){ // 子树中没有可以连到父亲上面的边
            	cut[u] = 1;
                dccn++;
                dcc[dccn].clear();
                dcc[dccn].push_back(u); // u是割点,可能包含在多个点双中,不能弹出
                while (1){
                    int w = stk[st];
                    dcc[dccn].push_back(w);
                    st--;
                    if (w == v){ // 做到子树全部弹出为止,不然v的兄弟也会被弹出
                        break;
                    }
                }
            }
        }
        else{
            low[u] = min(low[u], dfn[v]);
            // low表示u的子树中的非树边能到的最小的dfn值,所以不能和low[v]比较,详见下图
        }
    }
}

和边双不同的是,点双判断条件中对于等号成立的情况是允许的,所以点双允许父节点到子节点,然后在子节点中再去判断父节点,这没问题的(当然前提是dfn[父亲])。
反观边双,因为判断条件是Dfn[u]<Low[v],所以无向图的双边关系会影响这一结果,所以咱们得排除上述情况。

教学代码-边双联通分量

差不多的意思,看图:
这里引用CSDN@hailiang70303的图,侵删
咋写的呢?其实也很简单,你若是理解了Low,其实也就明白了。
作者:力扣(LeetCode)
链接:https://zhuanlan.zhihu.com/p/101923309
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

// x 代表当前搜索树的根节点,in_edge 代表其对应的序号(tot)
void tarjan(int x, int in_edge) {
    // 在搜索之前,先初始化节点 x 的时间戳与追溯值
    dfn[x] = low[x] = ++num;
    // 通过 head 变量获取节点 x 的直接连接的第一个相邻节点的序号
    // 通过 Next 变量,迭代获取剩下的与节点 x 直接连接的节点的序号
    for (int i = head[x]; i; i = Next[i]) {
        // 此时,i 代表节点 y 的序号
        int y = ver[i];
        // 如果当前节点 y 没有被访问过
        if (!dfn[y]) {
            // 递归搜索以 y 为跟的子树
            tarjan(y, i);
            // 计算 x 的追溯值
            low[x] = min(low[x], low[y]);
            // 桥的判定法则
            if (low[y] > dfn[x])
                bridge[i] = bridge[i ^ 1] = true; // 标记当前节点是否为桥(具体见下文)
        }
        else if (i != (in_edge ^ 1)) // 当前节点被访问过,且 y 不是 x 的“父节点”(具体见下文)
            low[x] = min(low[x], dfn[y]);
    }
}

leetcode给的解法我不喜欢,我觉得判断一个节点的父节点没必要这么搞,直接可以放在tarjan算法形参中,下面我将在实战中给你演示一下。

实战代码-边双

leetcode:1192 边双联通分量 hard
上代码!
这就是非常典型的边双问题嘛!
这里我与教学代码唯一不同的地方是,我的tarjan形参是curNode和preNode,很简洁。

class Solution {
private:
    vector<bool> Vis;
    vector<int> Dfn;
    vector<int> Low;
    int timeStamp;
    vector<vector<int>> details;
    vector<vector<int>> res;
public:
    /*全局参数初始化*/
    void paramInit(int n)
    {
        timeStamp = 0;
        vector<int> temp;
        for(int i = 0; i < n; ++ i)
        {
            Vis.push_back(false);
            Dfn.push_back(0);
            Low.push_back(0);
            details.push_back(temp);
        }
    }
    /*初始化单向链表*/
    void detailsInit(vector<vector<int>>& ds)
    {
        int detailsSize = ds.size();
        for(int i = 0; i < detailsSize; ++ i)
        {
            details[ds[i][0]].push_back(ds[i][1]);
            details[ds[i][1]].push_back(ds[i][0]);
        }
    }
    /*tarjan算法*/
    void tarjan(int curNode, int preNode)
    {
        Vis[curNode] = true;
        Dfn[curNode] = Low[curNode] = ++ timeStamp;
        for(int i = 0; i < details[curNode].size(); ++ i)
        {
            if(Vis[details[curNode][i]] == false)
            {
                tarjan(details[curNode][i], curNode);
                Low[curNode] = (Low[curNode] < Low[details[curNode][i]]) ? Low[curNode] : Low[details[curNode][i]];
                if(Dfn[curNode] < Low[details[curNode][i]])
                {
                    vector<int> tempRes;
                    tempRes.push_back(curNode);
                    tempRes.push_back(details[curNode][i]);
                    res.push_back(tempRes);
                }
            }
            else if(details[curNode][i] != preNode)
                Low[curNode] = (Low[curNode] < Dfn[details[curNode][i]]) ? Low[curNode] : Dfn[details[curNode][i]];
        }
    }
public:
    vector<vector<int>> criticalConnections(int n, vector<vector<int>>& connections) {
        paramInit(n);
        detailsInit(connections);
        tarjan(0, -1);
        return res;
    }
};

实战代码-点双

没找到合适的题目,铁子,就算了吧。

猜你喜欢

转载自blog.csdn.net/weixin_44039270/article/details/106402907
今日推荐