Loading web-font TeX/Math/Italic

Tarjan算法<笔记与补充>

一、Tarjan算法求强连通分量

1.简要

强连通的定义:有向图 G 强连通是指,G 中任意两个结点互相可达。

更好的理解:强连通图类似于嵌套的,强连通图一定有环,但 n 个节点的强连通图不一定有 n 元环。

强连通分量(Strongly Connected Components,SCC)的定义:强连通分量是有向图的极大的强连通子图,所谓“极大”意味着,把图划分为若干个强连通分量后,不存在两个强连通分量相互可达。

A.参考博客

pecco大佬的博客:强连通分量定义,DFS生成树定义,Tarjan算法的正确性证明。

对求有向图强连通分量的tarjan算法原理的一点理解by naturerun

讲解视频:形象的例子,基础;视频中low的意义与下文的第二种写法一样

B.Code

Tarjan算法求强连通分量的板子:

int n,m;
vector<int> G[MAXN],lxx;
int dfn[MAXN],low[MAXN];//这里的low表示 u 所在子树中的节点经过至多一条非树边能到达的节点中最小的dfs序
int st[MAXN],tp;//数组维护栈
bool ins[MAXN];//是否在栈中
vector<int> scc[MAXN];//每个强连通分量,用vector维护
int bel[MAXN];//bel[u]:u属于哪一个强连通分量
int cnt;//强连通分量数量
void Tarjan(int u){
    dfn[u]=low[u]=++dfn[0];
    st[++tp]=u;ins[u]=1;
    for(int v:G[u]){
        if(!dfn[v]){//v在u的DFS序生成树中
            Tarjan(v);
            low[u]=min(low[u],low[v]);
        }else if(ins[v])    low[u]=min(low[u],dfn[v]);//在栈中
    }
    if(low[u]==dfn[u]){
        cnt++;
        while(st[tp]!=u){
            int v=st[tp];tp--;ins[v]=0;
            scc[cnt].push_back(v);
            bel[v]=cnt;
        }
        int v=st[tp];tp--;ins[v]=0;
        scc[cnt].push_back(v);
        bel[v]=cnt;
    }
}
map<pii,bool> mp,lx;//用于判重边
vector<int> Gv[MAXN];//新图
int in[MAXN],out[MAXN];//入度,出度
void Init(){...}
int main(){
    ...
    for(int i=1;i<=n;i++){//求割点
        if(!dfn[i]) Tarjan(i);
    }
    for(int u=1;u<=n;u++){//缩点,重建图
        // printf("%d belongs to %d\n",u,bel[u]);
        for(int v:G[u]){
            int x=bel[u],y=bel[v];
            if(x==y||mp[{x,y}])    continue;
            mp[{x,y}]=1;
            out[x]++,in[y]++;
            Gv[x].push_back(y);
        }
    }
    ...
    return 0;
}

C.初学会遇到的问题:

  1. 为什么把 else if(ins[v]) low[u]=min(low[u],dfn[v]); 写成了:else if(ins[v]) low[u]=min(low[u],dfn[v]); ,也能过题? 答案见下
  2. 为什么 if(ins[v]),要求v 要在栈中?因为要保证 v 能到达 u,详见第一篇参考文章。

2.low的两种写法(个人补充)

上面是第一种写法。大部分时间都用的这一种,因为这种写法和求割点割边很像。求割点和割边不能用第二种写法!!!

很少看到对第两种写法的具体说明,在这里做简单的补充。

第一种写法的中间部分可以改为:

for (int v : G[u]) {
    if (!vis[v])
        dfs(v), low[u] = min(low[u], low[v]);
    else if (ins[v])
        low[u] = min(low[u], low[v]);
}

区别在于:对 栈中的节点 v 的更新方式.

产生区别的原因是:

第一种写法中的 low[u] 表示 u 所在子树中的节点经过至多一条非树边能到达的节点中最小的dfs序。

第二种写法中的 low[u] 表示 u 所在子树中的节点能到达的节点中最小的dfs序。这是更好理解的做法。

两种写法是等价的

如上图 (假设点的编号就是该点的DFN序)

在第一中写法中,low[6]=4low[4]=2

在第二中写法中,low[6]=low[4]=2

可以肯定的是,第二种写法是对的,但是第一种为什么也是对的呢?因为low[u]最终只要小于dfn[u]就不会认定这个点为强连通分量的根。我们只需要保证找到的“根”是对的,强连通分量就是对的,所以只用保证low[u]<dfn[u]就可以了,我们不关心low[u]的具体值。

3.运用

常见的使用场景是:

对一张存在环的有向图求出强连通分量,缩点,即将原图转化为DAG

缩点的过程:求出每个点属于的强连通分量编号,原图中的边更新到新图上,注意判重边(map)。

习题:

  1. P2272 [ZJOI2007] 最大半连通子图:典型缩点题目,一个结论:G的最大半连通子图拥有的节点数K就是最长链长度

  2. 「POJ2762」Going from u to v or from v to u? : 这个题是最大半连通子图的简单版,放在这里是提醒一下,判断DAG中的最长链不能只用入度、出度判断(1->2, 2->->3 ,1->3)。要用DP。

  3. 网络协议:有一个性质补充,使一张图变为强连通的需要添加的最少边数为:这张图缩点后的DAG中,入度为0的点数量出度为0的点的数量的最大值

二、Tarjan算法求双连通分量(割点和桥)

1.简要:

连通的:略

割点:对于一个无向图,如果把一个点删除后这个图的极大连通分量数增加了,那么这个点就是这个图的割点(又称割顶)。

割边(桥):对于一个无向图,如果删掉一条边后图中的连通分量数增加了,则称这条边为桥或者割边。

A.参考博客

从动态规划的角度理解 Tarjan 算法:解释了low数组的由来,为什么会有这个算法。

讲解视频:可以听一听例子的讲解,后面代码讲解部分是错的(low错了)

B.Code

求割点:

vector<int> G[MAXN],lxx;
int n,m;
int dfn[MAXN],low[MAXN];
bool cnt[MAXN];//是否是割点
void dfs(int u,int fa){//找割点
    dfn[u]=low[u]=++dfn[0];
    int son=0;
    for(int v:G[u]){
        if(!dfn[v]){//v在u的DFS生成数的子树中
            dfs(v,u);
            son++;
            if(fa!=-1&&low[v]>=dfn[u])  cnt[u]=1;//注意特判根节点
            low[u]=min(low[u],low[v]);
        }else if(v!=fa) low[u]=min(low[u],dfn[v]);//当然不能走回父亲
    }
    if(fa==-1&&son>=2)  cnt[u]=1;//注意特判根节点
}
int vis[MAXN];//注意:割点多次被计算!!!vis要用int
int tot=0;//当前双联通分量的大小(不同的题目要维护的值不一样,这里维护的是大小)
int Cnt=0;//该双连通分量连接的割点数量
int group;//当前边双连通分量的编号
void DFS(int u){//遍历双连通分量
    vis[u]=group;tot++;
    for(int v:G[u]){
        if(cnt[v]){
            if(vis[v]!=group)   vis[v]=group,Cnt++;
        }else if(vis[v]!=group)  DFS(v);
    }
}
void Init(){...}
int main(){
    Init();
    ...//输入
    for(int i=1;i<=n;i++){//求割点
        if(!dfn[i])    dfs(i,-1);
    }
    for(int i=1;i<=n;i++){//求点双
        if((!cnt[i])&&(!vis[i])){//这个点不是割点,也没有被遍历过
            tot=Cnt=0;
            group++;
            DFS(i);
            ...//其他的计算
        }
    }
    ...
    return 0;
}

求割边:

vector<pii> G[MAXN],lxx;
int n,m;
void Init(){...}
int dfn[MAXN],low[MAXN];
map<pii,bool> mp,Mp;//存割边的方式
void dfs(int u,int fa){
    dfn[u]=low[u]=++dfn[0];
    for(pii t:G[u]){
        int v=t.fi;
        if(!dfn[v]){
            dfs(v,u);
            low[u]=min(low[u],low[v]);
            if(low[v]>dfn[u])   mp[{u,v}]=mp[{v,u}]=1;//注意和求割点不一样了
        }else if(v!=fa) low[u]=min(low[u],dfn[v]);
    }
}
int bel[MAXN];//每个点属于哪一个边双
int cnt;//边双个数
bool vis[MAXN];//与点双不同,每个点只会被遍历一次,所以vis可以用bool
void DFS(int u){//根据割边求出边双
    vis[u]=1;bel[u]=cnt;
    // printf("%d belongs to %d\n",u,cnt);
    for(pii t:G[u]){
        int v=t.fi;
        if(mp[{u,v}]||vis[v])   continue;
        DFS(v);
    }
}
int dep[MAXN],Fa[MAXN];
void pre(int u){//树上的处理
    for(pii t:G[u]){
        int v=t.fi;
        if(v!=Fa[u]){
            Fa[v]=u;dep[v]=dep[u]+1;
            pre(v);
        }
    }
}
int main(){
    ...//输入
    for(int i=1;i<=n;i++){//求割边
        if(!dfn[i])    dfs(i,-1);
    }
    for(int i=1;i<=n;i++){//求边双
        if(!vis[i]) {cnt++;DFS(i);}
    }
    //重建图 变为树
    Init();//重建了图,这里把之前的图覆盖了因为之前的已经没用了
    for(int u=1;u<=n;u++){
        for(pii t:G[u]){
            int v=t.fi;
            int x=bel[u],y=bel[v];
            // printf("%d %d %d %d\n",u,v,x,y);
            G[x].push_back({y,t.se});
        }
    }
    rt=1;
    pre(rt);//缩点后,原图变为树
    ...//树上的操作
    return 0;
}

注意:

  1. 求割点和求割边不一样的地方:点双:low[v]>=dfn[u],边双low[v]>dfn[u]。解释可以看参考视频,很直观。
  2. 关于else if(v!=fa):求割点不一定需要,求割边一定需要。
  3. 与求“强连通分量”中的不同:low 的意义,见下。

2.只有一种写法

其实上面的那篇博客讲得很清楚,low[u]的由来,。下面解释为什么low[u]不能像在求强连通分量是那样表示 u 所在子树中的节点经过非树边能到达的节点中最小的dfs序,low[u]只能表示表示 u 所在子树中的节点只经过一条非树边能到达的节点中最小的dfs序

看上图,如果 low[u]表示 u 所在子树中的节点经过非树边能到达的节点中最小的dfs序,那么

low[6]=2
low[5]=2
low[4]=2
low[3]=3
low[2]=2
low[1]=1

那么根据 割点low[v]>dfn[u] 的判断,4号点不会被判为割点。但是事实上4号点是割点。

更广泛的解释:如果一个点 u 子树内的某一结点 v 有一条连向 u 的非树边,那么在错误的做法下,low[v] 可能会被更新成小于 dfn[u] 的值。但是如果 u 是割点,那在求 删除 u 点后的连通性时,就完全不能考虑 v.

3.具体运用

割点和割边,通常都是与点/边双连通分量有关系。

  1. 求出的割点来把图划分为若干个双连通分量,通常会对每一个双连通分量,考虑其连接的割点数,再进行分类讨论。

  2. 求出割边,缩点把原图变为一棵树,就可以把原问题转化为树上问题。

例题:

点双:

矿场搭建:典型的求割点并划分原图,对每个点双分讨

夺回据点: 也是对每个点双分讨

边双:

越狱老虎桥:转化为树上问题

每个点恰属于一个边双,每条边可能恰属于一个边双(非割边),也可能不属于任何边双(割边);每条边恰属于一个点双,每个点可能属于一个点双(非割点),也可能属于多个点双(割点)。

Alex_Wei的博客 %%%%%

posted @   bwartist  阅读(53)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示