2-SAT问题

46 篇文章 10 订阅
17 篇文章 2 订阅
2-SAT问题
问题的提出

有 N 个变量,每个变量只能有两种可能的取值。再给定 M 个条件,每个条件都是对这两个变量的取值限制。求是否存在对 N 个变量的合法赋值,使 M 个条件均得到满足。这个问题被称为 2-SAT (satisfiability)问题。

一般形式

设一个变量 Ai(1 <= i <= N)的取值可能是 0 或者 1,或者称 p 或 q。那么 2-SAT 问题中 M 个条件都可以转化为统一的形式 —— “若变量 Ai 被复制为 p,那么变量 Aj 必须被复制为 q”,其中 p , q ∈ \in {0, 1}。

判定方法
  1. 建立 2N 个节点的有向图,每个变量 Ai 对应 2 个节点,一般设为 i 和 i + N。
  2. 考虑每个条件,形如“若变量 Ai 赋值为 p,则变量 Aj 必须赋值为 q”, p , q ∈ \in {0, 1}。从 i + p * N 到 j + q * N 连一条有向边。

注意,上述条件蕴含着它的逆否命题“若变量 Aj 必须赋值为 1-q, 那么变量 Ai 必须赋值为 1-p”。如果在给出的 M 个限制条件中,原命题和逆否命题并不一定成对给出,那么应该从 j + (1 - q) * N 到 i + (1 - p ) * N 也连一条有向边。
总而言之,根据原命题和逆否命题的对称性,2-SAT 建出的有向图一定能画成“一侧节点 1~N,另一侧是节点 N+1 ~ 2N”。当把图中的边看作无向边时,这两侧的连边情况是对应的。

  1. 用 Tarjan 算法求出有向图中所有的强连通分量。
  2. 若存在 i ∈ [ i , N ] i \in [i , N] i[i,N] ,满足 i 和 i + N 属于同一个强连通分量,则表明:若变量 Ai 赋值为 p,则变量 Ai 必须赋值为 1-p。这显然是矛盾的,说明无解。若不存在这样的 i,则问题一定有解。

时间复杂度为 O(N+M)。

2-SAT问题例题
例1:和平委员会(peace)

题意描述
原题来自:POI 2001

根据宪法,Byteland 民主共和国的公众和平委员会应该在国会中通过立法程序来创立。 不幸的是,由于某些党派代表之间的不和睦而使得这件事存在障碍。此委员会必须满足下列条件:

  • 每个党派都在委员会中恰有1个代表,如果2个代表彼此厌恶,则他们不能都属于委员会。
  • 每个党在议会中有2个代表。代表从1编号到2n。 编号为 2i-1和 2i的代表属于第 i个党派。

任务:写一程序读入党派的数量和关系不友好的代表对,计算决定建立和平委员会是否可能,若行,则列出委员会的成员表。

解题思路
这道题属于 2-SAT 问题,按照 2-SAT 问题的一般策略,对于两对党派(a, b) 、(x , y),若 a 与 x 有矛盾,那么可以确定的是若 a 出席,则必定 y 出席,若 x 出席,则必定 b 出席。这对应着原命题与逆否命题,而其它命题虽然可能成立但并不能用“必须”修饰。
因此我们再利用 Tarjan 求强连通分量,即可解决2-SAT问题。

需要注意的是,边是有向的,也就是说添加的方向很重要,不可以随意添加。同时不能添加反向边,因为逆命题不一定成立。

由于 Tarjan 算法中不同连通块的颜色是按照拓扑排序来赋值的,也就是说颜色值越大的点越靠前(在拓扑排序中);那么在本题中自然是优先选择同一个强连通分量的点,因此我们可以考虑优先输出颜色值大的点,这样我们就会按照拓扑排序的顺序挑选节点。

代码示例

#include<bits/stdc++.h>
using namespace std;
const int N = 2*8100;
const int M = 1e5+10;
int n,m;
int head[N],ver[M],nex[M],tot = 1;
void addEdge(int x,int y){
	ver[++tot] = y; nex[tot] = head[x]; head[x] = tot;
}
int dfn[N],clk,co[N],col,low[N];
int Stack[N],top;
void Tarjan(int x){
	dfn[x] = low[x] = ++clk;
	Stack[++top] = x;
	for(int i = head[x];i ;i = nex[i]){
		int y = ver[i];
		if(!dfn[y]) Tarjan(y),low[x] = min(low[x],low[y]);
		else if(!co[y]) low[x] = min(low[x],dfn[y]);
	}
	if(dfn[x] == low[x])
		for(++col;Stack[top+1] != x;top--)
			co[Stack[top]] = col;
}
void solve(){
	for(int i = 1;i <= n;i++) if(!dfn[i]) Tarjan(i);
	for(int i = 1;i <= n;i += 2) 
		if(co[i] == co[i+1]){puts("NIE"); return;}
	for(int i = 1;i <= n;i += 2)
		if(co[i] < co[i+1]) printf("%d\n",i+1);//star
		else printf("%d\n",i);
}
int main(){
	scanf("%d%d",&n,&m), n <<= 1;
	for(int i = 1,x,y;i <= m;i++){
		scanf("%d%d",&x,&y);
		int u = x%2 == 0?x-1:x+1;
		int v = y%2 == 0?y-1:y+1;
		addEdge(u,y); addEdge(v,x); //star
	}
	solve();
	return 0;
}
例2:P4782 【模板】2-SAT 问题

题意描述
地址
共 n 个元素,满足 m 个要求,每个要求形如“x 为 1 或 y 为 0”.
解题思路
假设有一条约束为“x 为 1 或者 y 为 0”,那么可以得知,若令 x = 0,那么 y = 0;若 y = 1 则必有 x = 1。因此我们建立有向图,然后通过强连通分量来解决2-SAT问题即可,总体来说这题就是按照解决 2-SAT 问题的一般步骤来解决的。

代码示例

#include<bits/stdc++.h>
using namespace std;
const int N = 2e6+10;
const int M = 2*N;
int head[N],ver[M],nex[M],tot = 1;
void addEdge(int x,int y){
	ver[++tot] = y; nex[tot] = head[x]; head[x] = tot;
}
int getInt(){
	int res = 0;
	bool neg = false;
	char c = getchar();
	while(c != '-' && (c < '0' || c > '9')) c = getchar();
	if(c == '-') neg = true, c = getchar();
	while(c >= '0' && c <= '9') res = res*10 + c-'0',c = getchar();
	return neg?-res:res;
}
int n,m;
int dfn[N],clk,co[N],col,low[N];
int Stack[N],top;
void Tarjan(int x){
	dfn[x] = low[x] = ++clk;
	Stack[++top] = x;
	for(int i = head[x]; i;i = nex[i]){
		int y = ver[i];
		if(!dfn[y]) Tarjan(y),low[x] = min(low[x],low[y]);
		else if(!co[y]) low[x] = min(low[x],dfn[y]);
	}
	if(dfn[x] == low[x])
		for(++col;Stack[top+1] != x;top--)
			co[Stack[top]] = col;
}
void solve(){
	for(int i = 1;i <= 2*n;i++) if(!dfn[i]) Tarjan(i);
	for(int i = 1;i <= n;i++) 
		if(co[i] == co[i+n]){puts("IMPOSSIBLE"); return;}
	puts("POSSIBLE");
	for(int i = 1;i <= n;i++)
		if(co[i] < co[i+n]) printf("0 ");
		else printf("1 ");
}
int main(){
	n = getInt(); m = getInt();
	for(int i = 1,a,b,x,y;i <= m;i++){
		x = getInt(); a = getInt();
		y = getInt(); b = getInt();
		addEdge(x+(1-a)*n,y+b*n);
		addEdge(y+(1-b)*n,x+a*n);
	}
	solve();
	return 0;
}
例3:POJ3683 Priest John’s Busiest Day

题意简述
在一天内一共有 n 个人结婚,结婚要牧师发言,每一个婚礼有开始时间、结束时间、牧师发言时长。牧师可以选择在婚礼一开始就发言,或者发完言婚礼结束。牧师是否能安排成功?若成功还需要找到一组解。

解题思路
假设对于一个婚礼,选择在开始时发言为 1,结束时发言为 0,那么这就成了2-SAT问题,每个婚礼有 2 种选择,共 m 种约束,约束形如“若 i 选择在开头发言,则 j 必须在结束发言”。化为 2-SAT 问题的一般形式,即可求解。
因为读入输出是时钟形式,所以最好将读入和输出封装成单独的函数转化,增强可读性。同时为了使得解题步骤清晰一点,最好将判断是否时间冲突的函数也封装出去。

代码示例

#include<algorithm>
#include<cstdio>
#include<cstring>
using namespace std;
const int N = 4100;
const int M = 2e6+10;
int head[N],ver[M],nex[M], tot = 1;
void addEdge(int x,int y){
	ver[++tot] = y; nex[tot] = head[x]; head[x] = tot;
}
int dfn[N],low[N],clk,col,co[N];
int Stack[N],top;
void Tarjan(int x){
	dfn[x] = low[x] = ++clk;
	Stack[++top] = x;
	for(int i = head[x];i ;i = nex[i]){
		int y = ver[i];
		if(!dfn[y]) Tarjan(y), low[x] = min(low[x],low[y]);
		else if(!co[y]) low[x] = min(low[x],dfn[y]);
	}
	if(dfn[x] == low[x])
		for(++col;Stack[top+1] != x;top--)
			co[Stack[top]] = col;
}
int n;
inline bool check(int l1,int r1,int l2,int r2){
	if(r1 <= l2 || r2 <= l1) return false;
	return true;
}
inline void printClock(int t){
	printf("%d",t/600); t%= 600;
	printf("%d:",t/60); t%= 60;
	printf("%d",t/10); t%= 10;
	printf("%d",t);
}
int be[N],ed[N],last[N];
void solve(){
	for(int i = 1;i <= n;i++)
		for(int j = i+1;j <= n;j++){
			if(check(be[i],be[i]+last[i],be[j],be[j]+last[j]))
				addEdge(i+n,j), addEdge(j+n,i);
			if(check(be[i],be[i]+last[i],ed[j]-last[j],ed[j]))
				addEdge(i+n,n+j), addEdge(j,i);
			if(check(ed[i]-last[i],ed[i],ed[j]-last[j],ed[j]))
				addEdge(i,j+n), addEdge(j,i+n);
			if(check(ed[i]-last[i],ed[i],be[j],be[j]+last[j]))
				addEdge(i,j),addEdge(j+n,i+n);
		}
	for(int i = 1;i <= 2*n;i++) if(!dfn[i]) Tarjan(i);
	for(int i = 1;i <= 2*n;i++) 
		if(co[i] == co[i+n]) {puts("NO"); return;}
	puts("YES");
	for(int i = 1;i <= n;i++){
		if(co[i] > co[i+n]) printClock(be[i]),putchar(' '),printClock(be[i]+last[i]);
		else printClock(ed[i]-last[i]),putchar(' '),printClock(ed[i]);
		puts("");
	}
}
inline int getMinute(const char* s){
	int res = 0;
	res = (s[0] -'0')*600 + (s[1]-'0')*60 + (s[3]-'0')*10 + s[4]-'0';
	return res;
}
int main(){
	scanf("%d",&n);
	char a[30],b[30];
	for(int i = 1;i <= n;i++){
		scanf("%s%s%d",a,b,&last[i]);
		be[i] = getMinute(a);
		ed[i] = getMinute(b);
	}
	solve();
	return 0;
}
参考资料
  • 李煜东,算法竞赛进阶指南,郑州:河南电子音像出版社,2013.6,389-393
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

迷亭1213

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值