万万没想到
2020-02-08 12:58:49
洛谷日报第157期 [HyyypRtf06]快速入手拓扑排序
洛谷日报第237期 [dzz1537568241]Tarjan,你真的了解吗?
如果想要系统学习2-SAT,请在阅读此篇文章前阅读过这两篇前置芝士的文章或者已知掌握前置芝士;如果想初步了解2-SAT是什么,也可以阅读第一部分。
本文将围绕四个部分讲述2-SAT。
1. SAT问题以及2-SAT问题的定义。
2. 2-SAT问题的朴素解法及说明。
3. 2-SAT的Tarjan解法及说明。
4. 2-SAT的例题及应用。
SAT问题就是给出
退而求其次,2-SAT问题同SAT问题的条件几乎一样,唯一不同的是对于所有的限制条件只会描述
给出一个例子,比如共有四门课:语文、数学、英语、物理。共有四位小朋友都提出了自己的两个要求,你要决定出一个合法的课程表,使其满足每个小朋友至少一个要求(当然,也可以是其他限制方法,比如说每个小朋友至少不满足一个要求,绝不仅限于此,但此处为了阅读方便选择满足每个小朋友至少一个要求)。
以上就是一个典型的2-SAT问题,课程表中只包含数学,就是一个合法的解,课程表中包含数学、物理和英语同样是一个合法的解,解不限于此。
我们考虑枚举每一个
第一种写法(dfs)
bool dfs(int pos){//pos记录的是枚举到第几个bool变量
if(pos>n){//如果枚举完n个变量就进行判断
if(check())return true;//返回合法
else return false;//返回不合法
a[pos]=true;//枚举此个变量为1
if(dfs(pos+1))return true;//返回合法
a[pos]=false;//枚举此个变量为0
if(dfs(pos+1))return true;//返回合法
return false;//返回不合法
}
第二种写法(二进制状态枚举)
bool sovle(){
for(int i=0;i<(1<<n);i++){//枚举每一个bool元素的01情况,共有(1<<n)种情况,最大为(1<<n)-1,最小为0
if(check(i))return true;//如果判断合法就返回合法
}
return false;//返回非法
}
对于暴力解法的第二种,已经不能再优化了,但第一种dfs还是可以优化的,对于枚举完再判断可以优化为枚举每一位并进行对这一位的单独判断,关于这一位的限制条件是否合法,可以省去大量的非法状态,我们需要预处理出每一个变量各自的要求,最坏时间复杂度仍是
bool dfs(int pos){//pos记录的是枚举到第几个bool变量
if(pos>n)return true;//如果枚举完n个变量还没有非法就返回合法
a[pos]=true;//枚举此个变量为1
if(!check(pos))return false;//不符合要求就返回非法
if(dfs(pos+1))return true;//返回合法
a[pos]=false;//枚举此个变量为0
if(!check(pos))return false;//返回非法
if(dfs(pos+1))return true;//返回合法
return false;//返回不合法
}
给出
样例:
动手算一下我们可以发现这个样例是由唯一解的,解的序列为:
若第 i 个元素为 a ,则第 j 个元素必定为 b
我们可以发现这种限制条件带有指向性,就像一条有向边,于是我们定义一条有向边
但我们发现第
小结:我们将一个元素拆成两个点表示
比如样例里的
按照这种方法建好的图是下面这个样子的。
那么,为什么
这个时候我们一旦省略掉中间点,就会发现,从点
但是如果我们反过来推的话,从点
读到这里,聪明的读者或许会有疑问,有的情况下建出来的图是非法的,但答案却可以推出合法?先保留这个疑问,想想有什么解决方法,在本文的后些地方会予以解答。
那么建图的代码我们就可以写出来了,伪代码如下。
int main(){
scanf("%d%d",&n,&m);
for(int k=1;k<=m;k++){
scanf("%d%d%d%d",&i,&a,&j,&b);
add(i+(n&a),j+(n&b));//如果a为0,那么i+(n&a)为i,否则为i+n,j同理
}
return 0;
}
小结:当拆点建图后,如果一个元素拆出的两个点
继续上面留下来的问题:如何确定一条有向图上的路径
为了简化问题,我们先假设我们建出来的图是一个有向无环图(DAG),那么进行拓扑排序之后,一旦存在一条
将上文的例子进行添加后如下图。
红色箭头所表示是拓扑序的遍历顺序为
还记得我们的合法序列吗?序列为
小结:有向无环图的情况下,合法点的拓扑序比非法点大。
我们有了有向无环图下的优秀做法,时间复杂度
void topo(){//拓扑
int tot=0;//拓扑序计数
queue<int>q;//队列
for(int i=1;i<=2*n;i++){
if(!in[i])q.push(i),ord[i]=++tot;//如果入度为0就入队
}
while(!q.empty()){
int u=q.front();//取队头
q.pop();//弹出队头
for(int i=head[x];i;i=e[i].nex){//链式前向星
int v=e[i].to;
in[v]--;//v的入度减1
if(!in[v]){//如果入度为0
q.push(v);//入队
tot++;//拓扑序+1
ord[v]=tot;//记录拓扑序
}
}
}
for(int i=1;i<=n;i++){
if(ord[i]>ord[i+n])printf("0 ");//如果点i的拓扑序大于点i+n,选点i,为0
else printf("1 ");//否则选点i+n,为1
}
printf("\n");
}
但是我们建的图不保证无环,只保证是一个有向图,那什么算法可以将一个有向图变成有向无环图呢?就是Tarjan的强联通算法了,本文的前置芝士包含了这一内容。
Tarjan的强联通算法可以在
强连通分量的性质:一个强连通分量里的任意两个点可以互相到达,意思就是只要任意强连通分量里的任意一个点合法,强连通分量的其它所有点都合法,任意两个点之间都有以一条以对方为起点自己为终点和自己为起点对方为终点的路径。
那么如何判断这种情况下的拓扑序大小呢?请看如下的这张图。
根据我们前面的几个小结可以推出点
但我们并不需要在执行完Tarjan算法后缩点拓扑,只需要判断一个元素的两个点的强连通分量编号哪个小就行,小的就是合法的,为什么呢?可以自己照着上面的图试着推一下,再想一下原因。
如果暂时想不出来没关系,再看下面这张图。
这就是执行完Tarjan强连通分量算法之后的图,红色字体所代表的就是这个点强连通分量编号,绿色字体所代表的是缩点之后的拓扑序。一共有
可以有这这种方式理解:如果跑完Tarjan缩点之后呈现出的拓扑序更大,在Tarjan会更晚被遍历到,就会更早地被弹出栈而缩点,分量编号会更小。
或者,如果一个元素拆出的两个点
小结:Tarjan后同一元素拆点强连通分量编号小的点是合法点。
读到这里不知道你有没有发现,我们只考虑了一个元素的拆出的两个点中只有一个点可以到达另一个点的情况,如果两个点可以彼此到达呢?那又该怎么办呢?
其实很好想,如果两个点可以彼此到达,说明两个点Tarjan后肯定在同一个强连通分量里,但如果看到前面加粗的字体:强连通分量的性质,就会知道,只要强连通分量里有一点合法,其它点都会合法,两个点彼此到达,说明两个点彼此合法。但一个元素拆成的两个点只会有一个点合法,所以是矛盾的,是非法的,一个元素没有合法解,整个序列就没有合法解。至此我们得到了2-SAT的第五条规律。
小结:如果一个元素拆成的两个点在同一个强连通分量里,即强连通分量编号相同,那么整个序列无解。
那么,如果拆点后的两个点之间无法从某一点到达另一点,也就是两点间没有相连的路径,该怎么办呢?
其实也很好想,两点间无相连的路径,也就是说从任意一点出发无法到达对应的另一点,也就是推不出矛盾状态,也就是两点都可以是合法状态,随意选一点合法即可,那么归到上面判断强联通分量编号小的合法讨论里取点即可,因为这两点间都没有一条有向路径,更不可能强联通,编号自然不一样,至此我们得到了2-SAT的第六条规律。
小结:如果一个元素拆成的两个点之间没有任何路径相连,即使是有向路径,那么这两点都可以成为合法点,这两点的分量编号不同,选小的即可。
伪代码如下。
struct node{
int to,nex;
}e[maxm];
int dfncnt,sccnum,scc[maxn],dfn[maxn],low[maxn],s[maxn],top;
void add(int u,int v){//链式前向星
e[++cnt].to=v,e[cnt].nex=head[u],head[u]=cnt;
}
void tarjan(int u){//tarjan求强联通
dfn[u]=low[u]=++dfncnt;
s[++top]=u;
for(int i=head[u];i;i=e[i].nex){
int v=e[i].to;
if(!dfn[v]){
tarjan(v);
low[u]=Min(low[u],low[v]);
}
else if(!scc[v]){
low[u]=Min(low[u],low[v]);
}
}
if(dfn[u]==low[u]){//弹栈
++sccnum;
while(s[top]!=u){
scc[s[top]]=sccnum;//强连通分量编号
top--;
}
scc[s[top]]=sccnum;
top--;
}
}
int main(){
scanf("%d%d",&n,&m);
for(int k=1;k<=m;k++){
int i,a,j,b;
scanf("%d%d%d%d",&i,&a,&j,&b);
add(i+(n&a),j+(n&b));
}
for(int i=1;i<=2*n;i++){//一共2n个点
if(!dfn[i])tarjan(i);
}
for(int i=1;i<=n;i++){
if(scc[i]==scc[i+n]){//编号相同非法
printf("IMPOSSIBLE");
return 0;
}
}
for(int i=1;i<=n;++i){
if(scc[i]<scc[i+n])printf("0 ");//编号小的合法
else printf("1 ");
}
return 0;
}
总结:
1. 我们将一个元素拆成两个点表示
2. 当拆点建图后,如果一个元素拆出的两个点
3. 有向无环图的情况下,合法点的拓扑序比非法点大。
4. Tarjan后同一元素拆点强连通分量编号小的点是合法点。
5. 如果一个元素拆成的两个点在同一个强连通分量里,即强连通分量编号相同,那么整个序列无解。
6. 如果一个元素拆成的两个点之间没有任何路径相连,即使是有向路径,那么这两点都可以成为合法点,这两点的分量编号不同,选小的即可。
2-SAT的原理如出一辙,关键是建边的关系,所以接下来的例题只会给出建图方法。
建边方式提醒:第三部分的例题,细心的读者可以发现,我们将
所以,建2-SAT图,必须将所有肯定关系都找出建边,否则可能会出纰漏,这里回答的是本文上面的读者思考部分。
建边方式如图。
建边小练习:
(1) 2-SAT问题,要求对于每个限制条件的两个
解:至少不满足一个说明若一个元素合法,则另一个元素一定不合法,连边
(2) 2-SAT问题,要求对于每个限制条件的两个
解:至少满足说明若一个元素不合法,则另一个元素一定合法,连边
(3) 2-SAT问题,每个限制条件的两个元素要求若第一个要求非法,则第二个要求一定非法,求建边方法。
解:同上面开始时的建边方式讲解大体一样,除题目要求的条件外还有隐含的若第二个要求合法,则第一个要求一定合法的条件,连边
(4)2-SAT问题,要求对于每个条件给出的两个元素的值
解:当
完结撒花!