Loading

【瞎口胡】2-SAT 问题

Luogu P4782

\(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;
}
posted @ 2020-09-25 14:17  Meatherm  阅读(138)  评论(0编辑  收藏  举报