强连通分量 与 2-SAT

时间:2021-02-01 12:39:26   收藏:0   阅读:0

近期一直在刷这方面的题 因为没法学新知识 但又想写点什么 就水篇博文吧

引理

简单来说,在一个有向图中,若所有点之间两两互相直接可达,则将这个图成为强连通分量

强连通分量可以是某个有向图中的子图

求强连通分量可以使用 Tarjan,Kosaraju 或者 Garbow 算法

个人感觉 Tarjan算法 最实用,而且后两种算法并不是很熟练,因此在这里只讲 Tarjan算法

Tarjan算法

发明者 Robert E.Tarjan 罗伯特·塔扬,美国计算机科学家

塔老爷子发明过很多算法,而且大多是以他的名字命名的,所以 Tarjan算法 也分很多种,这里只说如何求强连通分量

首先需要知道什么是 DFS序

一个结点 \(x\) 的 DFS序 是指深度优先搜索遍历时改结点被搜索的次序,简记为 \(dfn[x]\)

然后,再维护另一个变量 \(low[x]\)

\(low[x]\) 表示以下节点的 DFS序 的最小值:以 \(x\) 为根的子树中的结点 和 从该子树通过一条不在搜索树上的边能到达的结点

根据 DFS 的遍历原理可以发现

知道了这些,再来看 Tarjan算法 求强连通分量的具体内容

我们一般只对还没有确定其 DFS序 的节点进行 Tarjan 操作,操作主要包括两个部分

第一部分

以 DFS 的形式,处理出当前点 \(x\)\(dfn[x]\)\(low[x]\)

对当前点打一个标记表示已经遍历过,在之后的 DFS 中根据是否遍历过来进行不同处理,具体方式如下:

设当前枚举点为 \(fr\)\(fr\) 连出去的点记为 \(to\)

  1. \(to\) 未被访问:继续对 \(to\) 进行深度搜索。在回溯过程中,用 \(low[to]\) 更新 \(low[fr]\)。因为存在从 \(fr\)\(to\) 的直接路径,所以 \(to\) 能够回溯到的已经在栈中的结点, \(fr\) 也一定能够回溯到。
  2. \(to\) 被访问过,已经在栈中:即已经被访问过,根据 low值 的定义(能够回溯到的最早的已经在栈中的结点),则用 \(dfn[to]\) 更新 \(low[fr]\)
  3. \(to\) 被访问过,已不在在栈中:说明 \(to\) 已搜索完毕,其所在连通分量已被处理,所以不用对其做操作。

这一部分代码实现如下:

low[fr]=dfn[fr]=++cnt;vis[fr]=1;
for(int i=head[fr];i;i=e[i].nxt){
    int to=e[i].to;
    if(!dfn[to]) tarjan(to),low[fr]=min(low[fr],low[to]);
    else if(vis[to]) low[fr]=min(low[fr],dfn[to]);
}

第二部分

对于一个连通分量图,我们很容易想到,在该连通图中有且仅有一个 \(dfn[x]=low[x]\)

该结点一定是在深度遍历的过程中,该连通分量中第一个被访问过的结点,因为它的 DFS序 和 low值 最小,不会被该连通分量中的其他结点所影响

我们可以维护一个栈,存储所有枚举到的点

因此,在回溯的过程中,判定 \(dfn[x]=low[x]\) 的条件是否成立,如果成立,则从栈中取出一个点,处理它所在的强连通分量的编号以及大小,也可以处理其他的一些操作,这样直到把所有点处理完为止

这一部分的代码实现如下:

zhan[++top]=u;
if(dfn[u]==low[u]){
	++siz[++t];
        int pre=zhan[top--];
        vis[pre]=0;num[pre]=t;
	while(pre!=u){
	    ++siz[t];pre=zhan[top--]; 
	    vis[pre]=0;num[pre]=t;
	}
}

至此,便可以处理出一个点所在的强连通分量,时间复杂度为 \(O(n+m)\)

2-SAT

SAT 是适定性(Satisfiability)问题的简称。一般形式为 k-适定性问题,简称 k-SAT。而当 \(k>2\) 时该问题为 NP 完全的。所以我们只研究 \(k=2\) 的情况。 —— OI Wiki

个人感觉,就是一个实际应用类的知识吧

就是指定 \(n\) 个集合,每个集合包含两个元素,给出若干个限制条件,每个条件规定不同集合中的某两个元素不能同时出现,最后问在这些条件下能否选出 \(n\) 个不在同一集合中的元素

这个问题一般用 Tarjan算法 来求解,也可以使用爆搜,可以参考OI Wiki上的说明,这里就只讲用 Tarjan 实现

但这种问题的实现主要不是难在 Tarjan 怎么写,而是难在图怎么建

假设这里有两个集合 \(A=\{x_1,y_1\}\)\(B=\{x_2,y_2\}\),规定 \(x_1\)\(y_2\) 不可同时出现,那我们就建两条有向边 \((x_1,y_1)\)\((y_2,x_2)\),表示选了 \(x_1\) 必须选 \(y_1\),,选了 \(y_2\) 必须选 \(x_2\)

这样建完边之后只需要跑一边 Tarjan 判断有无解,若有解就把几个不矛盾的强连通分量拼起来就好了

这里注意,因为跑 Tarjan 用了栈,根据拓扑序的定义和栈的原理,可以得到 跑出来的强连通分量编号是反拓扑序 这一结论

我们就可以利用这一结论,在输出方案时倒序得到拓扑序,然后确定变量取值即可

时间复杂度同上为 \(O(n+m)\)

例题

[APIO2009]抢掠计划
[USACO5.3]校园网Network of Schools
[ZJOI2007]最大半连通子图
[POI2001]和平委员会

写在后面

这种知识加起来已经学过好几遍了,但是写起来还是很累,总是怕自己说不明白
有一说一,我是真不理解有人只放个题目链接然后放个代码是怎么想的,连思路都不提,是给别人写 std 还是只为了告诉别人自己做对了
比我还水

评论(0
© 2014 mamicode.com 版权所有 京ICP备13008772号-2  联系我们:gaon5@hotmail.com
迷上了代码!