Tarjan实现的逻辑推导

//-----------------------------------------------------------------------------------------------

记录学习 如有侵犯请联系我删除文章.

膜拜大佬文章:tarjan算法原理介绍_zakheav的博客-CSDN博客_tarjan算法证明

关于tarjan的正确性证明已经困恼了我好久 今天下定决心 去深究原理。

看到大佬的文章 感觉茅塞顿开特此记录下来

//------------------------------------------------------------------------------------------------

因为个人能力有限 不适合初学者看 仅作为理解tarjan的一份想法;(非教学 不保真)

         //仅仅代表我的看法 不保真 欢迎大家讨论;

         //仅仅代表我的看法 不保真 欢迎大家讨论;

         //仅仅代表我的看法 不保真 欢迎大家讨论;

Tarjan的简单回顾:

Tarjan 是一个O(n+m)求取强连通分量的算法

常用于: 缩点 割点和桥;

实现代码如下

/*
dfn->第一次出现的时间戳;

low->从该节点的根节点到此节点中 出现过的最小时间戳(有点复杂我重点说)

st->储存节点信息的栈

visstack->记录此点是否出在栈中

col->标记点的信息(染色)

collib-> 储存每个强连通分量都有哪些点
*/


void tarjan(int x){
    dfn[x] = low[x] = ++ccnt;
    st.push(x);
    visstack[x] = 1;
    for (int  i = head[x]; i;i=edge[i].nt){
        long long v = edge[i].to;
        if(!dfn[v]){
            tarjan(v);
            low[x] = min(low[x], low[v]);
        }else if (visstack[v]){
            low[x] = min(low[x], dfn[v]);
        }
    }
    if(dfn[x] == low[x]){
        int tot;
        ++colid;
        do{
            tot = st.top();
            st.pop();
            visstack[tot] = 0;
            col[tot] = colid;
            collib[colid].push_back(tot);
        } while (x != tot);
    }
}


首先我们声明以下几个定理:

定理0:任何一点从栈中 弹出的前置条件为此点无法再进行拓展 或者说任何一个可以被拓展的点无法被弹出 或者说任何一个点从栈弹出的前置条件是 他的所有子节点都已经被弹出;

定理1: 栈空间较下方的dfn一定小于上方的dfn(或者说 任何一个较上方的点一定可以由较下方的点到达;)

定理2:由dfs的特性可知 从节点到达u点的任何一条路径 都应该完整的位于栈中

定理2证明:

利用反证法:假设某一部分不在这个栈中 既被弹出过 则至少存在某一点w作为分叉点 w连接了一条强连通分量的道路 和w-> u的两种路径 而因为弹出的条件此点无法再进行拓展才会进行dfn和low的对比判断 所以产生矛盾 故得证

问题一:low的问题:

为什么我们强调 一定是要从根节点到该节点(这条路径上) 已经出现过的 low值的最小值?也就是说为什么我们一定要强调 else if(visstack【v】)呢

    if(!dfn[v]){
        tarjan(v);
        low[x] = min(low[x], low[v]);
    }else if (visstack[v]){ //这里就是 精华部分
        low[x] = min(low[x], dfn[v]);
    }

part1:

首先我们先证明 else if(visstack【v】)对于 保证low始终是从根节点到该节点(这条路径上) 已经出现过的 low值的最小值 的有效性

最小值自然不用证明 ,所以我们的证明义务就是 在栈中 和 从根节点到该节点 的关联性

为什么只需要在节点进行tarjan的时候 去判断该点是否位于栈中即可? 或者说 为什么想要拓展的节点不位于栈中就证明 想要拓展的节点 一定不在 根节点到该节点的路径上

欲证明:当我们由节点u 拓展节点v时 发现节点v并不在栈空间 那么由节点v 一定不在 根节点到该节点的路径上

证明如下:

v点已经被弹出 所以定理0可知 他的所有子节点 一定被弹出过了 所以 u一定不是它的子节点之一 

至此得证

既 想要拓展的节点不位于栈中就证明 想要拓展的节点 一定不在 根节点到该节点的任何一条路径上

既 想要拓展的节点一定无法到达我们此时的节点u;

part2:

我们强调 一定是要从根节点到该节点(这条路径上) 已经出现过的 low值的最小值 的目的是什么呢;

那么首先我们就要讨论 维护low值的作用:两个点可以相互到达 <==> 被弹出时两个点的low值相等  

证明如下:

我们先明确 确定强连通分量的时间是位于此节点从栈空间弹出的时候确定 ,此时 low的值不会发生改变

由定理1我们可知 在栈空间内 dfn一定是从底向上 单调递增的 

由part1 我们可知 某一点的所有可到达路径均在栈中保存;所以 一个点的low值 一定能在栈中此点往下的部分找到 

假设 dfn小的是u 大的是v

由low的拓展方法我们可知 此节点一定能拓展到low值对应的dfn节点 所以 v->u

由定理1可知 u->v;

依此类推 所有low相等的节点都一定是同一个强连通分量;

证毕;

由上一个结果 我们可知 low相等的节点一定位于同一强连通分量

之后我们在讨论 为什么维护 从根节点到该节点(这条路径上) 已经出现过的 low值的最小值而其他路径上的 既已经被弹出了的点的low值无法对该节点更新;

这就有关于我们维护的堆栈弹出临界条件;

我们给出例子:

如图 2->4已经被弹出了 但是 2之后又拓展到了7  7->4 ;

我们的矛盾点就是 现在要不要由4去更新7的low值

我们观察我们的代码 

if(dfn[x] == low[x]){
        int tot;
        ++colid;
        do{
            tot = st.top();
            st.pop();
            visstack[tot] = 0;
            col[tot] = colid;
            collib[colid].push_back(tot);
        } while (x != tot);
    }

我们的弹出阶段是由 发现即将无法在拓展的点的dfn 和 low相等开始 ;之后把他的所有剩余子叶弹干净 和自己弹出 每一个 开始阶段的前置就是 堆栈中不存在其他的 dfn和low相等的点了(因为在其无法拓展进行判断之前 之前的所有节点均进行过无法拓展的判断了) 而如果 我们此时的 7节点被已经弹出的 4 节点更新了low 我们会发先 7 6 无法被正常弹出 划分到了下一个强连通分量内 自然会出错 所以我们需要保证low没有被其他弹出的子块干扰;

问题二:为什么出栈的是完整的强连通分量

这一部分我就不献丑了 我引用的大佬文献已经很清楚了大家可以去上方的链接看 我只说一下我的理解 提供下我的观点; 

为什么出栈的强连通分支是完整的:

假设不完整的话 剩余的部分必居其一
1.之前出栈的部分
2.还没有入栈的部分
3.还没有出栈的部分

2 -->因为弹出的时间限制 定理0可知 没有入栈的一定是当前点无法拓展到的点 自然不会是强连通分量

3 -->因为临界条件是 dfn等于low 所以还没有 的部分的dfn值一定小于等于当前的low 由low的意义可知 无法到达比low还小的时间戳;

1 -->如果之前出栈的部分和此部分能组成强连通分量 站在之前出栈的视角 与结论3相违背(大佬这个想的真的是很巧)

结语:到这里就告一段落啦 可能在证明过程中某些部位由逻辑漏洞 请大家发现了一定要不惜赐教 如果有讨论的也欢迎大家找我讨论 xiexie 支持

  • 8
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值