【瞎口胡】2-SAT 问题
有 \(n\) 个变量 \(x_1 \sim x_n(x_i \in \{0,1\})\),另有 \(m\) 个需要满足的条件,每个条件的形式都是 「\(x_i\) 为 \(1\) / \(0\) 或 \(x_j\) 为 \(0\) / \(1\)」。比如 「\(x_1\) 为 \(0\) 或 \(x_3\) 为 \(1\)」、「\(x_7\) 为 \(0\) 或 \(x_2\) 为 \(0\)」。
2-SAT 问题的目标是给每个变量赋值使得所有条件得到满足。
\(n,m \leq 10^6\)
上述问题即为一个 2-SAT 问题。这类问题通常是给只有两种取值的变量赋值,给出限制(但种类不仅仅是上题中出现的),要求给出一组合法解。
解决 2-SAT 问题一般转化为图论模型,然后使用图论相关算法求解。
我们考虑把每一个限制转化为若干条边 \(i \to j\),含义是 「选择 \(i\) 就必须选 \(j\)」。
因为每个变量有 \(0\) 和 \(1\) 两种选择,所以我们将每个变量 \(i\) 表示为两个图上的点 \(i_0,i_1\)。其中 \(i_0\) 表示 \(i\) 为 \(0\),\(i_1\) 表示 \(i\) 为 \(1\)。
考虑如何将限制转化为图上的若干条边。
-
一元限制:强制变量 \(i\) 为 \(0\) 或 \(1\)。
以 \(i\) 为 \(0\) 举例,则我们需要建边 \(i_0 \to i_1\),含义是 「选择了 \(i=0\) 就必须选择 \(i=1\)」,即强制使 \(i\) 为 \(1\)。
-
变量 \(i\) 和变量 \(j\) 同时为 \(0\) 或 \(1\)(\(i~\text{or}~j=0\) 或 \(i ~\text{and}~j=1\))
那么 \(i,j\) 都为 \(0\) / \(1\)。即建立两个一元限制。
-
变量 \(i\) 为 \(1\) 或变量 \(j\) 为 \(1\)(\(i~\text{or}~j=1\))
当 \(i\) 为 \(0\) 时,\(j\) 必须为 \(1\),建边 \(i_0 \to j_1\)。
当 \(j\) 为 \(0\) 时,\(i\) 必须为 \(1\),建边 \(j_0 \to i_1\)。
-
变量 \(i\) 为 \(0\) 或变量 \(j\) 为 \(0\)(\(i~\text{and}~j=0\))
跟上个限制类似,建边 \(i_1 \to j_0\) 和 \(j_1 \to i_0\)。
-
变量 \(i\) 和变量 \(j\) 相同(\(i=j\))
注意:这里一共要建 \(4\) 条边。
当 \(i\) 为 \(0\) 时,\(j\) 必须为 \(0\),建边 \(i_0 \to j_0\)。
剩余的边类似,\(i_1 \to j_1,j_0 \to i_0,j_1 \to i_1\)。
-
变量 \(i\) 和变量 \(j\) 不同(\(i \neq j\))
\(i_0 \to j_1,i_1 \to j_0,j_0 \to i_1,j_1 \to i_0\)。
我们观察到,如果在图上 \(i_0\) 和 \(i_1\) 互相可达(即存在从 $i_0 $ 到 \(i_1\) 的路径,也存在从 \(i_1\) 到 \(i_0\) 的路径),那么此问题无解,因为每个变量不可能同时取多个值。
又观察到如果互相可达,则它们在一个强连通分量中。所以求出强连通分量,对于每个 \(i_0,i_1\) 依次判断即可知道问题是否有可行解。
如果题目要求构造可行解,则将原图缩点成 DAG 后拓扑排序。我们注意到,如果 \(i_0\) 所在强连通分量的拓扑序比 \(i_1\) 小,将 \(i\) 赋值为 \(0\) 可能会出现包括但不限于下面的问题: DAG 上可能会存在一条 \(i_0 \to ... \to i_1\) 的路径。此时如果将 \(i\) 赋值为 \(0\),那么从 \(i_0\) 沿着路径往前走到 \(i_1\),会发现 \(i\) 的值必须为 \(1\),此时矛盾,所以应该将 \(i\) 赋值为 \(1\)。
事实上,如果 \(i_x\) 的拓扑序小于 \(i_{x~\text{xor}~1}\),将 \(i\) 赋值为 \(x~\text{xor}~1\) 一定合法。
证明
考虑这样一个事实,我们最终在 \(2n\) 个点的有向图上恰好选出了 \(n\) 个点。只需要证明,不存在一个 \(i\),使得 \(i_x\) 被选中,而其它被选中的点可以走到 \(i_{x \operatorname{xor} 1}\) 即可。
事实上,为了逻辑自洽,对于所有 \(i_x \to j_y\) 的边,\(j_{y \operatorname{xor} 1 } \to i_{x\operatorname{xor} 1}\) 一定存在(包括 \(i_0 \to i_1\)!);进而,一旦有 \(j_y\),使得我们选中了 \(j_y,i_x\),且 \(j_y\) 能走到 \(i_{x \operatorname{xor} 1}\),就一定存在路径 \(i_{x} \to j_{y \operatorname{xor} 1}\)。
设 \(d(p)\) 表示点 \(p\) 所在强连通分量的拓扑序,考虑上面的事实,我们可以得出,\(d(j_y)\leq d(i_{x \operatorname{xor} 1})\),\(d(i_x) \leq d(j_{y \operatorname{xor} 1})\);因为我们选择了令 \(i=x,j=y\),所以 \(d(i_{x \operatorname{xor} 1})< d(i_x)\),从而 \(d(j_y)\leq d(i_{x \operatorname{xor} 1})<d(i_x) \leq d(j_{y \operatorname{xor} 1})\),但是 \(d(i_{y \operatorname{xor} 1})< d(i_y)\) 应当成立,这显然推出了矛盾。因此,这样的情况不存在。
小技巧:如果使用 Tarjan 算法,则先找到的强连通分量,在新 DAG 中的拓扑序靠后。在代码实现中将找到的强连通分量依次标号 \(col_x\),那么当 \(col_x < col_y\) 时,\(x\) 的拓扑序比 \(y\) 大。
回到模板题。此题中的限制都是 \(i~\text{or}~j=1\) 类型的,按照上面的方法建边即可。
# include <bits/stdc++.h>
# define rr register
const int N=2000010;
struct Edge{
int to,next;
}edge[N<<2];
int head[N],sum;
std::stack <int> k;
int n,m;
bool vis[N];
int dfn[N],low[N],dfncount,scccount;
int col[N];
inline int read(void){
int res,f=1;
char c;
while((c=getchar())<'0'||c>'9')
if(c=='-')f=-1;
res=c-48;
while((c=getchar())>='0'&&c<='9')
res=res*10+c-48;
return res*f;
}
inline void add(int x,int y){
edge[++sum].to=y;
edge[sum].next=head[x];
head[x]=sum;
return;
}
void tarjan(int i){
low[i]=dfn[i]=++dfncount;
vis[i]=true;
k.push(i);
for(rr int j=head[i];j;j=edge[j].next){
int to=edge[j].to;
if(!vis[to]&&dfn[to]){
continue;
}
if(!dfn[to]){
tarjan(to);
}
low[i]=std::min(low[i],low[to]);
}
if(low[i]==dfn[i]){
++scccount;
while(k.top()!=i){
int u=k.top();
vis[u]=false,col[u]=scccount,k.pop();
}
vis[i]=false,col[i]=scccount,k.pop();
}
return;
}
inline int id(int a,int b){
return a+n*b;
}
int main(void){
n=read(),m=read();
for(rr int i=1;i<=m;++i){
int u=read(),a=read(),v=read(),b=read();
add(id(u,a^1),id(v,b));
add(id(v,b^1),id(u,a));
}
for(rr int i=1;i<=n*2;++i){
if(!dfn[i]){
tarjan(i);
}
}
for(rr int i=1;i<=n;++i){
if(col[id(i,0)]==col[id(i,1)]){
printf("IMPOSSIBLE");
return 0;
}
}
puts("POSSIBLE");
for(rr int i=1;i<=n;++i){
printf("%d ",(col[id(i,1)]<col[id(i,0)]));
}
return 0;
}