题解 P4782 【【模板】2-SAT 问题】
TopCarry
·
·
题解
果然没有Kosaraju的题解╮(╯_╰)╭。
事实上tarjan解2-SAT会很不自然,因为它的拓扑序其实是倒过来的,而Kosaraju就不会有这个烦恼,正好没人打,我来水一发。
考虑到这个优秀的冷门算法大多数人不用,所以简单介绍一下。
关于Kosaraju
-
代码不容易错
-
思维上更好理解,更加自然。
-
和2-SAT问题的原理上更加共通
先上一波代码:
void add(int x,int y){
e[++tot].to=y;e[tot].next=head[x];head[x]=tot;
e[++tot].to=x;e[tot].next=HEAD[y];HEAD[y]=tot;
}
void dfs1(int u){
int i;
vis[u]=1;
for(i=head[u];i;i=e[i].next)
if(!vis[e[i].to])
dfs1(e[i].to);
turn[++cnt]=u;
}
void dfs2(int u){
belong[u]=cnt;vis[u]=1;
for(int i=HEAD[u];i;i=e[i].next)
if(!vis[e[i].to])
dfs2(e[i].to);
}
void kosaraju(){
for(int i=1;i<=2*n;i++)
if(!vis[i])dfs1(i);
reset(vis);cnt=0;
for(int i=2*n;i>=1;i--)
if(!vis[turn[i]])
cnt++,dfs2(turn[i]);
}
原理很简单:
-
考虑无向图的缩点,对于每个强联通分量,只要从这个块任意一个点开始跑到没点可去就可以了。
-
然而有向图不可以,设A,B为两个强联通分量,当A->B时,我们依然无脑跑,把跑到的缩一起,但是呢,如果我们从A中的点开始跑,就会把A和B缩一起,这是我们不希望的,所以我们考虑让他总会从B开始遍历。
-
我们使用“逆后序”,即先遍历该点的出点,在把它丢进去,相当于是“每个点跑完的顺序”。
void dfs1(int u){
int i;
vis[u]=1;
for(i=head[u];i;i=e[i].next)
if(!vis[e[i].to])
dfs1(e[i].to);
turn[++cnt]=u;
}
这样做会有一个很棒的性质,模拟一波过程:
(1).从A开始跑,遍历顺序为:A->B->把B丢进栈->回溯到A->把A丢进栈。
(2).从B开始跑,遍历顺序为:跑完B->把B丢进栈->因为边的方向是A到B而到不了A,退出->发现vis[A]=0->跑A->把A丢进栈。
我们发现不管怎么样,栈顶都是A。
4.但是这个A很让人难受,相当于我们找到了一种方法,使栈顶永远都是那个不应该跑的点,这和第二条中的期望不符合诶。
5.于是我们建一个反图,对于A和B,他们依然都是强联通的(比如环反向了还是一个环),但是A->B的边变成了B->A,而A在栈顶,这就很完美!现在我们像无向图一样无脑遍历一遍就OK了。
呼,总算是讲完了,那么开始正式讲一波2-SAT。
分析一波逻辑:
1.<A=1或者B=1> = (当A不选,B一定得选;当B不选,A一定得选)
2.<A=0或者B=0> =(当A选,B一定不选;当B选,A一定不选)
3.<A=0或者B=1> =(当A选,B一定选;当B不选,A一定不选)
  这样一来,一个个令人不爽的“或者”就变成了可以确定的“一定”,我们就可以根据这个关系建图。
  $tips$:当你把上面四句理解了,下面的代码应该也就可以看懂了,如果不理解把4种情况带进去看一看应该就清晰了。(在下方代码之中,$1-n$表示$1-n$决策选,$n+1-2n$表示$1-n$决策不选)
```cpp
for(i=1;i<=m;i++){
x=read();f1=read();y=read();f2=read();
add(x+n*(f1&1),y+n*(f2^1));
add(y+n*(f2&1),x+n*(f1^1));
}
```
  为什么这个逻辑问题会和强联通分量沾上关系呢?
  构建出了图之后,我们发现,若$A->B$代表选了决策$A$一定选决策$B$,那么我们如果这样指了一圈之后指到了相悖的决策,就像$A=1->B=1->A=0$,那么这个$2-SAT$肯定是无解的:
```cpp
for(i=1;i<=n;i++)
if(belong[i]==belong[i+n]){
cout<<"IMPOSSIBLE";return 0;
}
```
  这个逻辑其实很好想,但是“如果没有这种情况即有解”这一结论又是怎么来的呢?
  我们考虑这样一件事情:如果逻辑关系$A->B->C->D->A$存在,那么他们的“非”关系一定有$!D$$->$$!C$$->$$!B$$—>$$!A->!D$存在,如下图所示:
![](https://cdn.luogu.com.cn/upload/pic/50490.png)
此时我们任取其中一组强联通分量就可以产生一组解了。
  但是事情往往不如人意,我们加一条$B->!C$,随之产生了$C->!B$:![](https://cdn.luogu.com.cn/upload/pic/50492.png )
  那么 ($ABCD$全选) 和 ($ABCD$都不选) 两个“逻辑块”就不是任意取了,显然我们如果选 ($ABCD$全选) 这个块会出现问题。
  有没有觉得这一部分和之前的Kosaraju某一部分相似呢?
  怕你们懒得翻,帮你们回忆一下:
  “$2.$然而有向图不可以,设$A$,$B$为两个强联通分量,当$A->B$时,我们依然无脑跑,把跑到的缩一起,但是呢,如果我们从$A$中的点开始跑,就会把$A$和$B$缩一起,这是我们不希望的,所以我们考虑让他总会从$B$开始遍历。”
  $2-SAT$原理是这样的:当出现强联通分量$A->B$的时候,我们要满足B的要求,也就是满足拓扑序更靠后的“联通块”的要求。(这和$Kosaraju$不是一样的:当遇到了$A->B$,先缩$B$)
  回忆一下$Kosaraju$干了什么事情:当$A->B$,$A$总是在栈顶,也就是说比$B$更早遍历,拓扑序更小,这真是太棒了!
  在$Kosaraju$算法的实现当中,每个节点所属的强联通分量的编号其实就是他们的拓扑序,于是,当我们想知道$A$选不选,只需要比较$A$和$!A$所属联通块的编号就$OK$了,选择那个编号更大,即拓扑序更靠后的决策。
```cpp
cout<<"POSSIBLE\n";
for(i=1;i<=n;i++){
cout<<(belong[i]>belong[i+n])<<' ';
}
```
  因为拓扑序较大的决策肯定不会和拓扑序较小的决策冲突,那么我们按照拓扑序,从后往前进行满足,在有解状态下一定可以找到一组可行解。
  至此 问题完美解决 当逻辑关系不止$4$个的时候只要修改建边方式就好。
  完整代码如下:
```cpp
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#define reset(a) memset((a),0,sizeof((a)))
using namespace std;
static char buf[100000],*pa,*pd;
#define gc pa==pd&&(pd=(pa=buf)+fread(buf,1,100000,stdin),pa==pd)?EOF:*pa++
inline int read(){
register int x(0);register char c(gc);
while(c>'9'||c<'0')c=gc;
while(c>='0'&&c<='9')x=x*10+c-48,c=gc;
return x;
}
const int N=4001000;
struct edge{
int to,next;
}e[N];
int head[N],tot,HEAD[N];
int n,m,cnt,turn[N],belong[N],vis[N];
void add(int x,int y){
e[++tot].to=y;e[tot].next=head[x];head[x]=tot;
e[++tot].to=x;e[tot].next=HEAD[y];HEAD[y]=tot;
}
void dfs1(int u){
int i;
vis[u]=1;
for(i=head[u];i;i=e[i].next)
if(!vis[e[i].to])
dfs1(e[i].to);
turn[++cnt]=u;
}
void dfs2(int u){
belong[u]=cnt;vis[u]=1;
for(int i=HEAD[u];i;i=e[i].next)
if(!vis[e[i].to])
dfs2(e[i].to);
}
void kosaraju(){
for(int i=1;i<=2*n;i++)
if(!vis[i])dfs1(i);
reset(vis);cnt=0;
for(int i=2*n;i>=1;i--)
if(!vis[turn[i]])
cnt++,dfs2(turn[i]);
}
int main(){
n=read();m=read();
register int i,x,y,f1,f2;
for(i=1;i<=m;i++){
x=read();f1=read();y=read();f2=read();
add(x+n*(f1&1),y+n*(f2^1));
add(y+n*(f2&1),x+n*(f1^1));
}
kosaraju();
for(i=1;i<=n;i++)
if(belong[i]==belong[i+n]){
cout<<"IMPOSSIBLE";return 0;
}
cout<<"POSSIBLE\n";
for(i=1;i<=n;i++){
cout<<(belong[i]>belong[i+n])<<' ';
}
return 0;
}
```