【JavaScript】tarjan算法求强连通分量

标签:js,JavaScript,tarjan,代码,图算法,求强连通分量,开箱即用,带输入输出的例程,2020网易提前批笔试8月8日前端/客户端/算法第四题

首先要理解tarjan算法,资源如下

  1. 百度百科的注释比较多,也提供了几种编程语言的代码
  2. b站很多视频资源,搜tarjan,即出现.
  3. SHHHS的配图对理解tarjan算法的深搜流程很有帮助,下方图片来源于该资源
    本代码所用的例子

从结点1开始dfs,dfs构成dfs树,每新到达一个结点,就将它添加到dfs树中。每完成一个强连通分量,就将该强连通分量中的所有结点从dfs树中删除。每个结点所在强连通分量一定是某个dfs树的一个子树。
定义强连通分量的根节点root_x:强连通分量的所有结点中最先被深搜访问到的结点称为该强连通分量的根节点。
深搜中,发现next结点被访问过,且属于当前dfs树,那么curr与next构成强连通,就判断当前的root_x和next结点哪个更靠近dfs树的根,取为新的root_x。
为了判断哪个结点更靠近dfs树的根,增加辅助数组dfn,记录结点x是深搜中第几个被访问到的;增加辅助数组low,记录元素x所在强连通分量的根节点root_x,记录方式是low[x]=dfn[root_x]。
合并强连通分量时(dfs回退时合并,dfs子树合并父亲),更新为dfn[x]中的最小值。(x属于待合并的连通分量的并集)

深搜中,如果一个结点还没有被访问过,比如(a,b)这条边,b没有被访问过,那么b要做初始化:

  1. 已经访问过b
  2. 是深搜中第dfn[b]个被访问到的
  3. (b,a)边在当前在深搜树中一定不存在,所以初始化b所在的强连通分量的根节点是b本身,low[x]=dfn[b]
从1开始dfs,直到当前结点curr没有next,当前dfs栈是1356

// 现在6没有next,由于6的root_6==low[6]==4,同时6是第dfn[6]==4个被访问到的,两个值相等,所以得到{6}这个强连通分量;同时将该连通分量从dfs栈中删除。
// 5除了6之外也没有next,dfs退栈,栈顶5也没有没访问过的next了,类似得到{5},现在dfs栈是[13]


dfs访问顺序为135641,当前dfs栈134

// 继续dfs到[134],下一个是1,发现1已经在栈里面了,更新low[4]为dfn[1],现在4没有next了,dfs回退,更新low[3]为low[4],3没有next了,dfs回退,更新low[1]为low[3]
// 现在回到了结点1,dfs压栈没有到达过的2,下一个4,由于4在栈中,所以2也属于当前强连通,更新low[2]=dfn[4]。当前dfs栈1342
// 2没有没访问过的next了,dfs回退,更新low[1]=low[2],dfs回退,dfs退到dfs树的根节点了,当前以1的出度为起点边的弱连通分量已完成
// tarjan求的是起点结点所在的强连通分量,但当要求图中所有强连通分量时,实际选择的起点可能是5,那么1234都没有访问到,所以要对所有结点都调一次tarjan方法。

// TODO: 输入
var n = 6;//共有6个结点
var edges = [[1, 3], [3, 5], [5, 6], [3, 4], [4, 6], [4, 1], [1, 2], [2, 4]];
// 转换为结点i的直接next都有谁
var head = Array(n + 1).fill(null).map((_, x) => new Array());// 因为结点从1开始而下标从0开始//每个元素初始化为空list
for (var edge of edges) {
    head[edge[0]].push(edge[1])
}
//此时的head:[ [], [ 3, 2 ], [ 4 ], [ 5, 4 ], [ 6, 1 ], [ 6 ], [] ]第一个元素是index0,可忽略

// TODO: 初始化
var dfn = Array(n+1).fill(0);//深度优先搜索中i的访问次序,dfn[i]==0则说明没有访问过
var low = Array(n+1).fill(null);//结点i所在强连通分量的跟(能追溯到的最早被访问过的)
var in_stack = Array(n+1).fill(false);//元素i当前不在栈中,这里的栈指的是Tarjan算法中的栈
var stack = [];//Tarjan算法中的栈
var res = [];//一个元素是一个强连通分量,每个强连通分量是list
var dfs_num = 0;//当前已经访问了dfs_num个结点了。当前结点是深搜中第dfs_num个被访问的。
/**
 * 以x为起点深搜得到的弱连通分量中所有强连通分量(包括整个图中包含x的强连通)都push到res中 
 * @param {*} x 结点/结点名称/图中的结点编号,从1开始
 * @param {*} indentation 打印深搜过程时提供缩进
 */
function Tarjan(x,indentation) {
    // TODO:x被访问的初始化
    // 打印深搜过程
    console.log(indentation,x,head[x]);
    indentation+='|  ';
    // 深度优先搜索中x是第几个被访问的,当前强连通分量的根是元素本身
    dfs_num+=1;
    dfn[x] = dfs_num;
    low[x] = dfs_num;
    // 压栈x(Tarjan算法中的栈)
    in_stack[x] = true;
    stack.push(x);
    // TODO:深搜,将x所在的,且以x为起点的弱连通分量中所有元素压栈
    // 遍历x的所有next结点,更新x所在弱连通分量的栈底为能追溯到的最早的结点
    for(var curr of head[x]) {
        if (dfn[curr]==0) {//结点curr没被访问过
            Tarjan(curr,indentation);//继续找next结点
            low[x] = Math.min(low[x], low[curr]);// x和curr的强连通分量的根节点中更早被访问的一个
        }
        else if (in_stack[curr]){// curr当前在栈中
            low[x] = Math.min(low[x], dfn[curr]);// x的强连通的根和curr哪个更早被访问
            // low[x] = Math.min(low[x], low[curr]);// 仅限求强连通时,可以
        } 
    }
    //构成强连通分量
    //深搜构成递归栈,该栈退栈到结点x时发现x是x所在强连通的根节点,
    // 此时将以x为起点的弱连通分量从栈中弹出,这些元素组成了强连通分量
    
    if (dfn[x] == low[x]) {
        // console.log('stack',stack)
        var curr = stack.pop();
        in_stack[curr]=false;
        res.push([curr]);
        while (curr != x && stack.length>0) {
            // 出栈
            curr=stack.pop();
            in_stack[curr] = false;
            // res
            res[res.length-1].push(curr)
        }
        // console.log('save',res)
    }
}
// TODO:遍历所有结点,找到所有强连通分量
for(var i=1;i<=n;i++){
    // i没有被访问过,否则x所在强连通分量已知
    if(dfn[i]==0){
        Tarjan(i,'|  ');
    }
}
// TODO:打印输出
console.log(res)
/**
深搜过程:(每行开头数字是当前结点,list是它的next数组)
|   1 [ 3, 2 ]
|  |   3 [ 5, 4 ]
|  |  |   5 [ 6 ]
|  |  |  |   6 []
|  |  |   4 [ 6, 1 ]
|  |   2 [ 4 ]
结果:
[ [ 6 ], [ 5 ], [ 2, 4, 3, 1 ] ]
 */

FYI:安装了node,则命令行执行node 文件名.js即可看到输出

一道练习题,2020网易云提前批笔试8月8日前端/客户端/算法第四题,求互相认可的教授有几对
输入内容的格式相同,把edges和n改下代码即可。
输出则得到各个强连通分量各有几个结点tmp,比如[2,3]有两个强连通分量,一个有2个结点,一个有3个结点,然后利用组合的公式来计算从tmp里选2个结点有几种结果:C(tmp,2),公式推导结果C(tmp,2)=A(tmp,2)/A(2,2)=[ tmp!/(tmp-2)! ] / [ 2!/(2-2)! ]=tmp*(tmp-1)/2

total = 0
for tmp of res:
    total += tmp*(tmp-1)/2
return total

更多练习题推荐leetcode或牛客中找图相关的内容

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353