2-SAT 学习笔记
2-SAT 学习笔记
有一类生活中的问题:
一个人邀请了 A,B,C,D 四个人来他的 party,但是 A 和 B 有矛盾,所以他俩不会一起来,C 和 D 也有矛盾不会一起来,要求找到一种合适的方式来邀请他们,判断是否有这种方案,并且找出这种方案。
把这个问题抽象一下,有 \(n\) 个 \(\texttt{bool}\) 变量,有 \(m\) 组形似 \((a,flag_a,b,flag_b)\) 的条件,表示当 \(a=flag_a\) 时,\(b\neq flag_b\),或者当 \(b=flag_b\) 时,\(a\neq flag_a\),要求判断这些条件是否可行,如果可行,给出一种具体赋值方案。
如果把这个说法再简化一下,就是条件 \((A,B)\),满足 \(A\land (\lnot B)=1\) 或 \((\lnot A)\land B=1\)。
前置知识:强连通分量(本博客采用 Tarjan 算法)。
P4782 【模板】2-SAT 问题
题目描述
有 \(n\) 个布尔变量 \(x_1\)\(\sim\)\(x_n\),另有 \(m\) 个需要满足的条件,每个条件的形式都是 「\(x_i\) 为 true
/ false
或 \(x_j\) 为 true
/ false
」。比如 「\(x_1\) 为真或 \(x_3\) 为假」、「\(x_7\) 为假或 \(x_2\) 为假」。
2-SAT 问题的目标是给每个变量赋值使得所有条件得到满足。
输入格式
第一行两个整数 \(n\) 和 \(m\),意义如题面所述。
接下来 \(m\) 行每行 \(4\) 个整数 \(i\), \(a\), \(j\), \(b\),表示 「\(x_i\) 为 \(a\) 或 \(x_j\) 为 \(b\)」(\(a, b\in \{0,1\}\))
输出格式
如无解,输出 IMPOSSIBLE
;否则输出 POSSIBLE
。
下一行 \(n\) 个整数 \(x_1\sim x_n\)(\(x_i\in\{0,1\}\)),表示构造出的解。
样例 #1
样例输入 #1
3 1
1 1 3 0
样例输出 #1
POSSIBLE
0 0 0
提示
\(1\leq n, m\leq 10^6\) , 前 \(3\) 个点卡小错误,后面 \(5\) 个点卡效率。
由于数据随机生成,可能会含有(10 0 10 0)之类的坑,但按照最常规写法的写的标程没有出错,各个数据点卡什么的提示在标程里。
Solution
此题就是最显然的 \(\texttt{2-SAT}\) 问题(毕竟模板)。
尝试建图,将每一个条件建成一个点,比如有两个点 \(A,B\),边 \(A\rightarrow B\) 就表示当 \(A\) 满足时,\(B\) 也会满足。根据这样的建图方式,读入数据 \((A,B)\),就可以连两条边 \(A\rightarrow(\lnot B)\) 和 \(B\rightarrow(\lnot A)\)。不难发现,在同一个强连通分量中的变量的值一定是相同的。换言之,如果 \(A\) 与 \(\lnot A\) 在同一个强连通分量中,就是无解的情况。到此为止,我们就会判定 \(\texttt{2-SAT}\) 的解的存在性问题了。
考虑怎么输出方案。不难发现,如果 \(A\) 所在的强连通分量的拓扑序在 \(\lnot A\) 的之前,就使 \(A\) 为真就行了(可以理解为先到先得),这样就可以给出一组可行解。
Code
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof a)
#define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
char buf[1<<23],*p1=buf,*p2=buf;
//#define int long long
using namespace std;
template<typename T> void read(T &k)
{
k=0;T flag=1;char b=getchar();
while (!isdigit(b)) {flag=(b=='-')?-1:1;b=getchar();}
while (isdigit(b)) {k=k*10+b-48;b=getchar();}
k*=flag;
}
template<typename T> void write(T k) {if (k<0) {putchar('-'),write(-k);return;}if (k>9) write(k/10);putchar(k%10+48);}
template<typename T> void writewith(T k,char c) {write(k);putchar(c);}
const int _SIZE=2e6;
int n,m;
struct EDGE{
int nxt,to;
}edge[(_SIZE<<1)+5];
int tot,head[_SIZE+5];
void AddEdge(int x,int y) {edge[++tot]=(EDGE){head[x],y};head[x]=tot;}
int dfn[_SIZE+5],bl[_SIZE+5],low[_SIZE+5],cnt;
bool vis[_SIZE+5];
stack<int> s;
void tarjan(int x)
{
dfn[x]=low[x]=++cnt;
vis[x]=1,s.push(x);
for (int i=head[x];i;i=edge[i].nxt)
{
int twd=edge[i].to;
if (!dfn[twd]) tarjan(twd),low[x]=min(low[x],low[twd]);
else if (vis[twd]) low[x]=min(low[x],dfn[twd]);
}
if (low[x]==dfn[x])
{
int v;cnt++;
do{
v=s.top(),s.pop(),vis[v]=0;
bl[v]=cnt;
}while(v!=x);
}
}
int Num(int x,int flag) {return x+flag*n;}//用于建图计算点,如果值为0则为x,值为1就为x+n
signed main()
{
read(n),read(m);
for (int i=1;i<=m;i++)
{
int a,b,c,d;read(a),read(b),read(c),read(d);
AddEdge(Num(a,b),Num(c,!d)),AddEdge(Num(c,d),Num(a,!b));
}
for (int i=1;i<=(n<<1);i++) if (!dfn[i]) tarjan(i);
for (int i=1;i<=n;i++)
if (bl[i]==bl[i+n]) return puts("IMPOSSIBLE")&0;
puts("POSSIBLE");
for (int i=1;i<=n;i++)
writewith(bl[i]<bl[i+n],' ');
return 0;
}