再入无向图的双连通分量(tarjan神犇%%%%%%%%)

再入有向图的强连通分量

tarjan

  • 连通分量

对于分量中任意两点 u , v u,v u,v,必然可以从 u 走 到 v u走到v uv,且从 v 走 到 u v走到u vu

  • 强连通分量 S C C SCC SCC

极大连通分量(加上其它任意一个点,都不是连通分量)

  • 应用

将任意一个 有向图 ⇒ 缩 点 \Rightarrow^{缩点} 有向无环图(DAG)拓扑图

  • 求最短路/长路,递推
  • 定义:
  1. 树枝边(dfs时的树边)
  2. 前向边
  3. 后向边
  4. 横叉边(只会往左边横叉,往右边其实是树枝边了)

时间戳:对每个点定义两个时间戳

dfn:遍历到u的时间戳

low:从u开始走,所能遍历到的最小时间戳

重要性质:无向图 D F S DFS DFS后的 d f s 树 dfs树 dfs,所有的非树边都是从下往上的。

  • 算法(判断 x x x是否是在一个强连通分量里)
  1. 它可以回到当前搜索树边的祖先节点。(后向边)
  2. 走横叉边,横插边可以走到祖先节点。

时间复杂度 O ( n + m ) O(n+m) On+m

后续

缩点
for(int i = 1; i <= n; i ++ )
    for(int i = h[x]; ~i; i = ne[i]){
        int j = e[i]
        if(id[i] != id[j]){
			add(id[i], id[j]);
        }
    }
  • 缩点后的DAG图,加最少的边使得图变为强连通分量。

a d d = m a x ( P , Q ) p ∈ 起 点 , Q ∈ 终 点 add = max(P,Q) {p\in 起点, Q\in 终点} add=max(P,Q)pQ

(题目不保证图连通的话,入度和出度要分开算)

证明,不相交起点和终点路径之间连边

拓扑序

连通分量编号 递减的顺序就是拓扑序了。

证明:算导论的dfs拓扑排序做法。

板子

void tarjan(int x){
    dfn[x] = low[x] = ++dfs_clock;
    in_stk[x] = true; stk[++top] = x;
    for(int i = h[x]; ~i; i = ne[i]){
        int j = e[i];
        if(!dfn[j]){
            tarjan(j);
            low[x] = min(low[x], low[j]);
        }else if(in_stk[j]) low[x] = min(low[x], dfn[j]);
    }
    if(low[x] == dfn[x]){
        int y;
        scc_cnt++;
        do{
            y = stk[top--];
            in_stk[y] = false;
            id[y] = scc_cnt;
            siz[scc_cnt]++;
        }while(y != x);
    }
}

作者:HenriHGS
链接:https://www.acwing.com/activity/content/code/content/1737726/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Kosaraju算法

学习地址

逆图学习!!!

一、算法简介

在计算科学中, K o s a r a j u Kosaraju Kosaraju的算法(又称为– S h a r i r K o s a r a j u Sharir Kosaraju SharirKosaraju算法)是一个线性时间( l i n e a r t i m e linear time lineartime)算法找到的有向图的强连通分量。它利用了一个事实,逆图(与各边方向相同的图形反转, t r a n s p o s e g r a p h transpose graph transposegraph)有相同的强连通分量的原始图。

有关强连通分量的介绍在之前Tarjan 算法中:Tarjan Algorithm

逆图( T r a n p o s e G r a p h Tranpose Graph TranposeGraph)

我们对逆图定义如下:
G T = ( V , E T ) , E T = { ( u , v ) : ( v , u ) ∈ E } G^T=(V,E^T),E^T=\{(u,v):(v,u)\in E\} GT=(V,ET),ET={(u,v):(v,u)E}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gLY4tvxp-1633622499654)(D:\aaaa文件夹\工作\文档\模板\图论,似神仙\图床\QQ截图20210906192232.png)]

上图是有向图 G G G , 和图 G G G的逆图 G T G^T GT

K o s a r a j u Kosaraju Kosaraju 算法就是分别对原图 G G G 和它的逆图 G T G^T GT 进行两遍 D F S DFS DFS,即:

1).对原图 G G G进行深度优先搜索,找出每个节点的完成时间(时间戳)

2).选择完成时间较大的节点开始,对逆图 G T G^T GT 搜索,能够到达的点构成一个强连通分量

3).如果所有节点未被遍历,重复2). ,否则算法结束;

#include 
#include 
#include 
#include 
#define x first
#define y second
#define For(i,x,y) for(int i = (x); i <= (y); i ++ )
#define fori(i,x,y) for(int i = (x); i < (y); i ++ )
using namespace std;
const int N =  1e4+10, M = 1e5+10;
int e1[M], e2[M], ne1[M], ne2[M], h1[N], h2[N], idx1, idx2; 
void add1(int a, int b){
    e1[idx1] = b, ne1[idx1] = h1[a], h1[a] = idx1++; 
}
void add2(int a, int b){
    e2[idx2] = b, ne2[idx2] = h2[a], h2[a] = idx2++;
}
int dfn[N], finish[N], dfs_clock;
//typedef pairPII;
//PII p[N];
int ord[N<<1];
void dfs1(int x){
    if(dfn[x]) return;
    dfn[x] = ++dfs_clock;
    for(int i = h1[x]; ~i; i = ne1[i]){
        int j = e1[i];
        dfs1(j);
    }
    finish[x] = ++dfs_clock;//点的完成时间要不同
    ord[finish[x]] = x;
}
int scc_cnt = 0;
bool st[N];
int id[N], tot;
void dfs2(int x){
    if(st[x]) return ;
    tot++;
    st[x] = true;
    id[x] = scc_cnt;
    for(int i = h2[x]; ~i; i = ne2[i]){
        int j = e2[i];
        dfs2(j);
    }
}
int dout[N], siz[N];
int main(){
    memset(h1,-1,sizeof h2);
    memset(h2,-1,sizeof h2);
    idx1 = idx2 = 0;
    int n, m;
    scanf("%d %d", &n, &m);
    For(i,1,m){
        int a,b;
        scanf("%d %d", &a, &b);
        add1(a,b); add2(b,a);
    }
    For(i,1,n)dfs1(i);
  //  For(i,1,n)p[i] = {finish[i],i};
    //sort(p+1,p+1+n);
    memset(st,0,sizeof st);
    for(int i = n*2; i ; i -- ){
        if(ord[i] && !st[ord[i]]) tot = 0,scc_cnt++,dfs2(ord[i]), siz[id[ord[i]]] = tot;
    }
    //题目部分
    For(x,1,n){
        for(int i = h1[x]; ~i; i = ne1[i]){
            int j = e1[i];
            if(id[x] == id[j]) continue;
            dout[id[x]]++;
        }
    }
   // printf("id:");
  //  For(i,1,n) printf("%d ", id[i]);
  //  puts("");
    int sum = 0, zero = 0;
    For(i,1,scc_cnt) {
        if(dout[i]) continue;
        zero++;
        sum += siz[i];
        if(zero > 1) sum = 0;
    }
    printf("%d\n", sum);
    return 0;
}

再入无向图的双连通分量(tarjan神犇%%%%%%%%)

一、分类

  1. 双连通分量
  2. 重连通分量

二、双连通分量(tarjan算法)

  • 点连通分量边连通分量之间没有必然联系
  • 割点之间没有必然联系

1. 边双连通分量 e-DCC

  • :一条边,删掉这条后,图变的不连通。 C r i t i c a l   l i n k Critical \ link Critical link

  • 定义:极大的不包含的连通块,称为边双连通分量。

  • 性质

性质1:在一个边双连通分量里,不管删掉哪条边,都是连通的。

性质2:存在两条没有公共边的路径。
再入无向图的双连通分量(tarjan神犇%%%%%%%%)_第1张图片

  • 对于一棵树 它的所有边双连通分量都是结点。,被桥所分割。

再入无向图的双连通分量(tarjan神犇%%%%%%%%)_第2张图片

2. 点双连通分量 v-DCC

  • 割点:在连通的无向图中,如果把这个删除,图变的不连通。 C r i t i c a l   n o d e Critical \ node Critical node

每个割点至少属于两个双连通分量。

如果任意两点至少存在两条“点不重复”的路径,则是点双连通的。

等价于

任意两条边都在一个简单环中,即内部无割顶

  • 定义:极大的不包含割点的连通块,称为点双连通分量。(

不难发现,每条边恰好属于一个点双连通分量,但不同点双连通分量可能会会有公共点,可以证明不同双连通分量最多只有一个公共点,且它一定是割顶。 另一方面,任意割顶至少是两个不同 点双连通分量的公共点

再入无向图的双连通分量(tarjan神犇%%%%%%%%)_第3张图片

​ 它的所有点双连通分量都是边。

再入无向图的双连通分量(tarjan神犇%%%%%%%%)_第4张图片

一个点双连通分量这是特殊情况
再入无向图的双连通分量(tarjan神犇%%%%%%%%)_第5张图片

三、边双连通分量 e-DCC(类比有向图)

  • 时间戳: d f n ( x ) dfn(x) dfn(x),即dfs序,

  • l o w ( x ) low(x) low(x): x的子树,往下走的话,最早能到达的点(即dfs序较小的)。

  • 问题1:dfs时如何判断是否是桥?

看y是否会指回,祖先结点。

等价于 d f n ( x ) < l o w ( y ) dfn(x) < low(y) dfn(x)<low(y)

再入无向图的双连通分量(tarjan神犇%%%%%%%%)_第6张图片
(图6)

  • 问题2:如何找所有的边双连通分量?

法1:把所有桥都删掉。

法2:stack,(利用 d f n ( x ) = = l o w ( x ) dfn(x) == low(x) dfn(x)==low(x), 那么 x x x 连向父亲的边是桥)

acwing eg1: 395. 冗余路径

思路:
先求边双,之后缩点。
a n s = ⌈ c n t 2 ⌉ = c n t + 1 2 ans = \lceil \frac{cnt}{2} \rceil =\frac{cnt+1}{2} ans=2cnt=2cnt+1
再入无向图的双连通分量(tarjan神犇%%%%%%%%)_第7张图片
图片(7)

# include 
# include 
# include 
# define mst(x,a) memset(x,a,sizeof(x))
using namespace std;
const int N = 5e3+10, M = 1e4+10;
int n, m;
int dfn[N], low[N], dfs_clock, id[N], du[N];
int stk[N], top;
int e[M], ne[M], h[M], idx;
bool is_bridge[M];
int dcc_cnt;

void init(){
    mst(h,-1);	mst(dfn,0);
    mst(id,0);	mst(du,0);
    top = dcc_cnt = dfs_clock = idx = 0;
}

void add(int a, int b){
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

void dfs(int x, int from){
    dfn[x] = low[x] = ++dfs_clock;
    stk[++top] = x;
    for(int i = h[x]; ~i; i = ne[i]){
        int j = e[i];
        if(!dfn[j]){
            dfs(j,i);
            low[x] = min(low[x],low[j]);
            if(dfn[x] < low[j])
                is_bridge[i] = is_bridge[i^1] = true;
        }else if(i != (from^1)) low[x] = min(low[x],dfn[j]);
        //^的优先级较低,所以要加括号
    }
    if(dfn[x] == low[x]){
        ++dcc_cnt;
        int y;
        do{
            y = stk[top--];
            id[y] = dcc_cnt;//缩点
        }while(y != x);
    }
}

void sol(){
    init();
    scanf("%d%d", &n, &m);
    int a, b;
    while(m--){
        scanf("%d%d", &a, &b);
        add(a,b);
        add(b,a);
    }
    ///这道题保证连通,所以不用每个点都求。
    /*
    for(int i = 1; i <= n; i ++ ){
        if(!dfn[i]) dfs(i,-1);
    }
    */
    dfs(1,-1);
    for(int i = 0; i < idx; i ++){
        if(is_bridge[i]) du[id[e[i]]]++;
    }
    int cnt = 0;
    for(int i = 1; i <= dcc_cnt; i ++ )if(du[i] == 1) cnt++;
    cnt = (cnt+1)/2;
    printf("%d\n", cnt);
}

int main(){
    sol();
    return 0;
}

tips

类比有向图。
a n s = max ⁡ ( p , q ) ans = \max(p,q) ans=max(p,q)
(p:出度为0的点,q:入度为0的点)

四、点双连通分量 v-DCC

  • 问题1:如何求割点?

l o w ( y ) ≥ d f n ( x ) low(y) \geq dfn(x) low(y)dfn(x)

  1. 如果x不是根结点(root),x是割点
  2. x是根结点,至少有两个子结点。(看child)
  • 问题2:如何求点双连通分量。

i f : d f n ( x ) ≤ l o w ( y ) if : dfn(x) \leq low(y) if:dfn(x)low(y)

c n t + + ; cnt++; cnt++;

i f : x ! = r o o t ∣ ∣ c n t > 1 if: x != root || cnt > 1 if:x!=rootcnt>1

  1. 将栈中元素弹出,直至弹出y为止。
  2. 且x也属于该点双连通分量。(=情况,看下图8。因为前面的<情况已经被弹出了)
  3. 孤立点,也是双连通分量。

再入无向图的双连通分量(tarjan神犇%%%%%%%%)_第8张图片

  • acwing 1183. 电力

题意: 删除一个点后,连通块有几个。

思路:类似求割点

# include 
# include 
# include 
# include 
# define For(i,x,y) for(int i = (x); i <= (y); i ++ )
# define fori(i,x,y) for(int i = (x); i < (y); i ++ )
# define mst(x,a) memset(x,a,sizeof(x))
using namespace std;
const int N = 1e4+10, M = 3e4+10;

int ne[M], h[N], e[M], idx;
int low[N], dfn[N], dfs_clock;
int n, m;
int ans,  root;

void dfs(int u){
    dfn[u] = low[u] = ++dfs_clock;
    int cnt = 0;
    for(int i = h[u]; ~i; i = ne[i]){
        int j = e[i];
        if(!dfn[j]){
            dfs(j);
            low[u] = min(low[u], low[j]);
            if(low[j] >= dfn[u]){
                cnt++;
            }
        }else low[u] = min(low[u],dfn[j]);
    }
    if(root != u && cnt) cnt++;
    ans = max(ans,cnt);
}

void init(){
    mst(h, -1 ); mst(dfn,0);
    idx = dfs_clock = ans = 0;
}

void add(int a, int b){
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

void sol(){
    init();
    while(m--){
        int a, b;
        scanf("%d%d", &a,&b);
        add(a,b);
        add(b,a);
    }
    int cnt = 0;
    fori(i,0,n){
        if(!dfn[i]){
            root = i;
            dfs(i);
            cnt++;
        }
    }
    printf("%d\n", ans + cnt - 1);
}

int main(){
    while(scanf("%d%d", &n, &m) && (n||m))sol();
    return 0;
}
  • 矿场搭建:
    题意:给定一个无向图,问最少在几个点设置出口,可以使得不管哪个点坍塌,其余所有点都可以与这个出口连通。

    思路

    1. 为了保证出口本身不坍塌,所以出口数量 >= 2.
    2. 分别看每个连通块。
    1. 无割点(度数为0

    C c n t 2 C_{cnt}^{2} Ccnt2

    2)有割点(缩点,注意这里的缩点和前面几种连通分量都不同,2倍原来的点

    1>每个割点单独作为一个点。

    2>从每个v-DCC向其他包含的割点连边(如图9)
    再入无向图的双连通分量(tarjan神犇%%%%%%%%)_第9张图片

    3>V-DCC,度数为1,需要在该分量内部放出口,并且非割点,放一个即可。
    c n t − 1 cnt - 1 cnt1
    再入无向图的双连通分量(tarjan神犇%%%%%%%%)_第10张图片

    (图10)

    4> V-DCC, 度数大于1,无需设置出口

# include 
# include 
# include 
# include 
# include 
# define For(i,x,y) for(int i = (x); i <= (y); i ++ )
# define mst(x,a) memset(x,a,sizeof(x))
# define pb push_back
using namespace std;
typedef unsigned long long ULL;
const int N = 1010;
const int M = 1010;

int e[M], ne[M], idx, h[N];
vector<int> dcc[N<<1];
int dcc_cnt, stk[N], top, root;
int dfn[N], low[N], dfs_clock;
bool is_cut[N];
int n, m;

void add(int a, int b){
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

void init(){
    mst(h,-1); mst(is_cut, 0); mst(dfn,0);
    For(i,1,dcc_cnt) dcc[i].clear();
   n  =  dcc_cnt = idx = top = dfs_clock = 0;
    while(m--){
        int a, b;
        scanf("%d%d", &a, &b);
        add(a,b);
        add(b,a);
        n = max(a,n);
        n = max(n,b);
    }
}


void dfs(int u){
    dfn[u] = low[u] = ++ dfs_clock;
    stk[++top] = u;
    if(root == u && h[u] == -1){
        dcc_cnt ++ ;
        dcc[dcc_cnt].pb(stk[top--]);
        return ;
    }
    int child = 0;
    for(int i = h[u]; ~i; i = ne[i]){
        int j = e[i];
        if(!dfn[j]){
            dfs(j);
            low[u] = min(low[u], low[j]);
            if(dfn[u] <= low[j]){
                child ++;
                if(root != u || child > 1) is_cut[u] = true;
                int y;
                dcc_cnt++;
                do{
                    y = stk[top--];
                    dcc[dcc_cnt].pb(y);
                }while(y != j);
                dcc[dcc_cnt].pb(u);
            }
        }else low[u] = min(low[u],dfn[j]);
    }
}

void sol(){
    init();
    for(root = 1; root <= n; root ++ )
        if(!dfn[root])
            dfs(root);
    ULL ans2 = 1;
    int ans1 = 0;
    For(i,1,dcc_cnt){
        vector<int>& v = dcc[i];
        int cnt = 0 ;
        for(int j = 0; j < v.size(); j ++ )
            if(is_cut[v[j]]) cnt++;
        if(cnt == 0){
            if(v.size() > 1) ans1 += 2, ans2 *= v.size()*(v.size()-1)/2;
            else ans1++;
        }else if(cnt == 1) ans1++, ans2 *= v.size()-1;
    }
    printf("%d %llu\n", ans1, ans2);
}
int main(){
    int cas = 0;
    while(scanf("%d", &m) && m){
        printf("Case %d: ", ++cas);
        sol();
    }
    return 0;
}

你可能感兴趣的:(大专题,#,tarjan,算法)