ACM算法总结 图论(三)





强连通分量

有向图的强连通分量是指有向图的极大强联通子图,在这个子图中,任意两点相互可达。

  • 一个图强联通当且仅当存在一个包含所有顶点的有向回路;

对于下图这个例子来说:

在这里插入图片描述

强连通分量分别为 { 1 } , { 2 } , { 3 } , { 4 } , { 5 , 6 , 7 , 8 , 9 } \{1\},\{2\},\{3\},\{4\},\{5,6,7,8,9\} ,其中对于有向图中的边,我们分为三种:

  1. 有向图dfs搜索时每次访问没有搜索过的结点时的边,图中蓝色的边;
  2. 指向已经搜索过的祖先结点的边,图中黄色的边;
  3. 指向已经搜索过的非祖先结点的边,图中红色的边。

求有向图强连通分量可以用Tarjan算法,这是一种基于有向图dfs生成树的算法,算法流程如下:

用 dfn[i] 记录 i 号结点的dfs序,low[i] 记录 i 号结点的子树中的所有边指向的祖先结点的最小 dfn 值,然后循环对所有未搜索过的结点dfs搜索。用栈维护未归类 scc(强连通分量)的结点序列(这个结点序列按照搜索顺序放入,故具有父子关系),当搜索到结点 u 时,首先进栈,然后对于其所有子节点 v,如果 v 还未被访问过,那么递归处理 v;如果 v 访问过但是还没有归类 scc(说明这个 v 在栈中),可以得出 <u,v> 是一条指向祖先的边;以上两种情况分别更新 low[u] 。如果遇到 dfn[u]==low[u] 的情况时,说明以 u 为根节点的子树是一个强连通分量,进行出栈和归类 scc。

这个算法不要求有向图弱连通,而其正确性在于上面提到的强连通的等价条件。栈实际维护了一条单向连通的父子结点序列,然后如果找到了反向边,说明找到了一个环(回路),于是环上的所有结点都在同一个强连通分量中。但是找到了环我们并不马上进行 scc 归类,而是通过记录 low 的方式,这样只用最后处理根结点。

Tarjan算法的复杂度是 O(n+m) ,代码如下:

const int maxn=1e2+5;
vector<int> G[maxn];
int dfn[maxn],low[maxn],scc[maxn],dfs_cnt,scc_cnt;
int n,m;
stack<int> S;

void dfs(int u)
{
    dfn[u]=low[u]=++dfs_cnt;
    S.push(u);
    REP(i,0,G[u].size()-1)
    {
        int v=G[u][i];
        if(!dfn[v]) dfs(v),low[u]=min(low[u],low[v]);
        else if(!scc[v]) low[u]=min(low[u],dfn[v]);
    }
    if(dfn[u]==low[u])
    {
        scc_cnt++;
        while(1)
        {
            int x=S.top(); S.pop();
            scc[x]=scc_cnt;
            if(u==x) break;
        }
    }
}

void get_scc()
{
    REP(i,1,n) if(!dfn[i]) dfs(i);
}

求解强连通分量有利于对有向图缩点,就是把一个强连通分量缩成一个点,缩点后的图是一个DAG(有向无环图),并且通过Tarjan算法缩点过后,这个DAG的结点编号满足反拓扑序(由dfs的顺序可以得出)。




割顶和桥

无向图的连通性定义为图中任意两点可以达到。无向图的极大连通子图(分支)就是一个跟任何其他顶点都没有连边的子图。

割顶的定义:无向图中的一个顶点,去掉这个点之后,无向图的分支个数增加。

的定义:无向图中的一条边,去掉这条边之后,无向图的分支个数增加。

求解无向图的割顶和桥也是用 Tarjan算法。不过跟有向图的不大一样,基本思路还是基于dfs搜索,但是在无向图之中,low[i] 记录的是结点 i 的子树不经过 i 所能达到的最小的 dfn 值。所以在搜索的过程中,对于某个结点 u,如果它的子节点 v 的 low[v]>=dfn[u],说明 v 无法不通过 u 到达 u 的任意祖先,所以 u 是割顶;而如果 low[v]>dfn[u],说明 v 无法不通过无向边 <u,v> 到达 u,所以无向边 <u,v> 是桥。

另外要注意更新 low 的时候不能把dfs生成树的父结点认为是子结点,所以dfs时要传入父结点,处理过程中特判。

该算法代码如下:

const int maxn=1e5+5;
vector<int> G[maxn];
int dfn[maxn],is_cut[maxn],dfs_cnt;
int n,m;

int dfs(int u,int far)
{
    int lowu=dfn[u]=++dfs_cnt,ch=0;
    REP(i,0,G[u].size()-1)
    {
        int v=G[u][i];
        if(!dfn[v])
        {
            ch++;
            int lowv=dfs(v,u);
            lowu=min(lowu,lowv);
            if(lowv>=dfn[u]) is_cut[u]=1;
            //if(lowv>dfn[u]) is_bridge(u,v);
        }
        else if(dfn[v]<dfn[u] && v!=far) lowu=min(lowu,dfn[v]);
    }
    if(far<0 && ch<=1) is_cut[u]=0;
    return lowu;
}

void get_cut()
{
    REP(i,1,n) if(!dfn[i]) dfs(i,-1);
}




双连通分量

双连通这个性质分为两种:

  • 点双连通:任意两点之间存在至少两条“点不重复”路径,这等价于任意两条边都在同一个简单环中,或者等价于删去任意一个点都不影响连通性,即内部没有割顶;
  • 边双连通:任意两点之间存在至少两条“边不重复”路径,这等价于任意一条边都至少在一个简单环中,或者等价于删去任意一条边都不影响连通性,即内部没有桥;

边双连通分量的求解很简单,只要标记出所有的桥,去掉桥之后的所有分支就是边双连通分量。

点双连通分量(bcc)稍微复杂一些,先说几个它的性质:

  • 任意两个bcc之间有且仅有一个公共点,且这个公共点是割顶;
  • 任意一个割顶都属于至少两个bcc;

求解点双连通分量代码如下:

const int maxn=1e5+5;
vector<int> G[maxn],B[maxn];
int dfn[maxn],bcc[maxn],dfs_cnt,bcc_cnt;
int n,m;
struct edge {int u,v;};
stack<edge> S;

int dfs(int u,int far)
{
    int lowu=dfn[u]=++dfs_cnt;
    REP(i,0,G[u].size()-1)
    {
        int v=G[u][i]; edge e=(edge){u,v};
        if(!dfn[v])
        {
            S.push(e);
            int lowv=dfs(v,u);
            lowu=min(lowu,lowv);
            if(lowv>=dfn[u])
            {
                bcc_cnt++;
                while(1)
                {
                    edge x=S.top(); S.pop();
                    if(bcc[x.u]!=bcc_cnt) B[bcc_cnt].push_back(x.u),bcc[x.u]=bcc_cnt;
                    if(bcc[x.v]!=bcc_cnt) B[bcc_cnt].push_back(x.v),bcc[x.v]=bcc_cnt;
                    if(x.u==u && x.v==v) break;
                }
            }
        }
        else if(dfn[v]<dfn[u] && v!=far) S.push(e),lowu=min(lowu,dfn[v]);
    }
    return lowu;
}

void get_bcc()
{
    REP(i,1,n) if(!dfn[i]) dfs(i,-1);
}

算法其实也属于Tarjan算法的一种,这个类似求割顶,不过使用栈将当前bcc的边存起来,然后对于每个 low[v]>=dfn[u] 的情况统计一次bcc,形象地理解如下图所示:

在这里插入图片描述

需要注意的是,尽管两个点和一条边这样的子图不太符合点双连通分量的定义,但按照该算法运行之后这也会被统计为点双连通分量。




2-SAT

2-SAT问题可以等价描述如下:对于逻辑表达式 ( A 1 B 1 ) ( A 2 B 2 ) . . . ( A n B n ) (A_1 \bigotimes B_1)\land (A_2\bigotimes B_2)\land ... \land (A_n \bigotimes B_n) ,其中 A i A_i 文字,代表 a i a_i a i \overline{a_i} \bigotimes 代表某一种二元逻辑运算,我们的目标是找到合适的布尔变量,使得该逻辑表达式为真。

可以用图论的方法解决2-SAT问题。假设涉及到 n 个逻辑变量 x 1 , x 2 , . . . , x n x_1,x_2,...,x_n ,构造一个含有 2n 个结点的有向图G,其中第 i 个结点代表 x i x_i 为真,第 i+n 个结点代表 x i x_i 为假;如果存在边 <i,j>,说明假设 x i x_i 成立,那么 x j x_j 必须成立,即 x i x j x_i\rightarrow x_j (注意这里如果 i>n,实际意义是 x i \overline{x_i} 成立);对于常见的逻辑运算加边方法如下:

x i = 1 x_i=1 x i x i \overline{x_i}\rightarrow x_i
x i x j \overline{x_i} \lor x_j x i x j     x j x i x_i \rightarrow x_j \ \land \ \overline{x_j}\rightarrow \overline{x_i}
x i x j x_i\lor x_j x i x j     x j x i \overline{x_i} \rightarrow x_j \ \land \ \overline{x_j}\rightarrow x_i

以此类推。(对于与运算其实就是两个都为1)

构造好图之后,我们实际上希望避免一切( 1 0 1\rightarrow 0 )的情况出现,所以对图进行强连通分量缩点,如果存在 x i x_i x i \overline{x_i} 在同一个 scc 中,则一定无解;否则,对于每个逻辑变量,如果 x i x_i 拓扑序小于 x i \overline{x_i} ,则令 x i = 0 x_i=0 ,反之令 x i = 1 x_i=1

由于Tarjan算法求出的 scc 满足反拓扑序,故:如果 s c c [ x i ] < s c c [ x i ] scc[x_i]<scc[\overline{x_i}] ,则 x i = 1 x_i=1 ;否则 x i = 0 x_i=0

发布了12 篇原创文章 · 获赞 5 · 访问量 522

猜你喜欢

转载自blog.csdn.net/dragonylee/article/details/103969197
今日推荐