题解 P4782 【【模板】2-SAT 问题】

· · 题解

果然没有Kosaraju的题解╮(╯_╰)╭。

  事实上tarjan2-SAT会很不自然,因为它的拓扑序其实是倒过来的,而Kosaraju就不会有这个烦恼,正好没人打,我来水一发。

  考虑到这个优秀的冷门算法大多数人不用,所以简单介绍一下。

关于Kosaraju

  1. 代码不容易错

  2. 思维上更好理解,更加自然。

  3. 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]);
    }

    原理很简单:

  4. 考虑无向图的缩点,对于每个强联通分量,只要从这个块任意一个点开始跑到没点可去就可以了。

  5. 然而有向图不可以,设AB为两个强联通分量,当A->B时,我们依然无脑跑,把跑到的缩一起,但是呢,如果我们从A中的点开始跑,就会把AB缩一起,这是我们不希望的,所以我们考虑让他总会从B开始遍历。

  6. 我们使用“逆后序”,即先遍历该点的出点,在把它丢进去,相当于是“每个点跑完的顺序”。

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丢进栈->因为边的方向是AB而到不了A,退出->发现vis[A]=0->A->A丢进栈。

  我们发现不管怎么样,栈顶都是A。   4.但是这个A很让人难受,相当于我们找到了一种方法,使栈顶永远都是那个不应该跑的点,这和第二条中的期望不符合诶。

  5.于是我们建一个反图,对于AB,他们依然都是强联通的(比如环反向了还是一个环),但是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一定不选)

&emsp;&emsp;这样一来,一个个令人不爽的“或者”就变成了可以确定的“一定”,我们就可以根据这个关系建图。 &emsp;&emsp;$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)); } ``` &emsp;&emsp;为什么这个逻辑问题会和强联通分量沾上关系呢? &emsp;&emsp;构建出了图之后,我们发现,若$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; } ``` &emsp;&emsp;这个逻辑其实很好想,但是“如果没有这种情况即有解”这一结论又是怎么来的呢? &emsp;&emsp;我们考虑这样一件事情:如果逻辑关系$A->B->C->D->A$存在,那么他们的“非”关系一定有$!D$$->$$!C$$->$$!B$$—>$$!A->!D$存在,如下图所示: ![](https://cdn.luogu.com.cn/upload/pic/50490.png) 此时我们任取其中一组强联通分量就可以产生一组解了。 &emsp;&emsp;但是事情往往不如人意,我们加一条$B->!C$,随之产生了$C->!B$:![](https://cdn.luogu.com.cn/upload/pic/50492.png ) &emsp;&emsp;那么 ($ABCD$全选) 和 ($ABCD$都不选) 两个“逻辑块”就不是任意取了,显然我们如果选 ($ABCD$全选) 这个块会出现问题。 &emsp;&emsp;有没有觉得这一部分和之前的Kosaraju某一部分相似呢? &emsp;&emsp;怕你们懒得翻,帮你们回忆一下: &emsp;&emsp;“$2.$然而有向图不可以,设$A$,$B$为两个强联通分量,当$A->B$时,我们依然无脑跑,把跑到的缩一起,但是呢,如果我们从$A$中的点开始跑,就会把$A$和$B$缩一起,这是我们不希望的,所以我们考虑让他总会从$B$开始遍历。” &emsp;&emsp;$2-SAT$原理是这样的:当出现强联通分量$A->B$的时候,我们要满足B的要求,也就是满足拓扑序更靠后的“联通块”的要求。(这和$Kosaraju$不是一样的:当遇到了$A->B$,先缩$B$) &emsp;&emsp;回忆一下$Kosaraju$干了什么事情:当$A->B$,$A$总是在栈顶,也就是说比$B$更早遍历,拓扑序更小,这真是太棒了! &emsp;&emsp;在$Kosaraju$算法的实现当中,每个节点所属的强联通分量的编号其实就是他们的拓扑序,于是,当我们想知道$A$选不选,只需要比较$A$和$!A$所属联通块的编号就$OK$了,选择那个编号更大,即拓扑序更靠后的决策。 ```cpp cout<<"POSSIBLE\n"; for(i=1;i<=n;i++){ cout<<(belong[i]>belong[i+n])<<' '; } ``` &emsp;&emsp;因为拓扑序较大的决策肯定不会和拓扑序较小的决策冲突,那么我们按照拓扑序,从后往前进行满足,在有解状态下一定可以找到一组可行解。 &emsp;&emsp;至此 问题完美解决 当逻辑关系不止$4$个的时候只要修改建边方式就好。 &emsp;&emsp;完整代码如下: ```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; } ```