2-SAT问题
现有一个由N个布尔值组成的序列A,给出一些限制关系,比如A[x]AND A[y]=0、A[x] OR A[y] OR A[z]=1等,要确定A[0..N-1]的值,使得其满足所有限制关系。这个称为SAT问题,特别的,若每种限制关系中最多只对两个元素进行限制,则称为2-SAT问题。
由于在2-SAT问题中,最多只对两个元素进行限制,所以可能的限制关系共有11种:
A[x]
NOT A[x]
A[x] AND A[y]
A[x] AND NOT A[y]
A[x] OR A[y]
A[x] OR NOT A[y]
NOT (A[x] AND A[y])
NOT (A[x] OR A[y])
A[x] XOR A[y]
NOT (A[x] XOR A[y])
A[x] XOR NOT A[y]
进一步,A[x] ANDA[y]相当于(A[x]) AND (A[y])(也就是可以拆分成A[x]与A[y]两个限制关系),NOT(A[x]OR A[y])相当于NOT A[x] AND NOT A[y](也就是可以拆分成NOT A[x]与NOT A[y]两个限制关系)。因此,可能的限制关系最多只有9种。
在实际问题中,2-SAT问题在大多数时候表现成以下形式:有N对物品,每对物品中必须选取一个,也只能选取一个,并且它们之间存在某些限制关系(如某两个物品不能都选,某两个物品不能都不选,某两个物品必须且只能选一个,某个物品必选)等,这时,可以将每对物品当成一个布尔值(选取第一个物品相当于0,选取第二个相当于1),如果所有的限制关系最多只对两个物品进行限制,则它们都可以转化成9种基本限制关系,从而转化为2-SAT模型。
(引自:http://www.cnblogs.com/kuangbin/archive/2012/10/05/2712429.html)
在程序实现中,我们把初始的n个物品变成2n个节点,然后从0开始编号到2*n-1号。其中原始第i个物品对应节点i*2和i*2+1。如果我们mark[i*2]节点,那么表示我们i节点设为假,如果我们mark[i*2+1]节点,那么我们i节点设为真。同一个节点只能mark一种结果(即对于原始i来说,我们只能mark[i*2]或mark[i*2+1]其中之一)。
然后加入存在i假或j假的论述,我们就引一条图中从2*i+1到2*j的边,再引一条2*j+1到2*i的边,表示如果i是真的,那么j肯定是假的(否则之前的结论不成立)。且如果j是真的,那么i肯定是假的(否则之前的结论也不成立)。
如果存在i为真的论述,那么我们直接mark[i*2+1]即可。
最终判断整个问题是否有解,就是做多次dfs来设置每个节点可能的值(真或假),看看是否所有可能取值情况都会冲突。如果不冲突,那么有解。(这里并不需要暴力枚举所有的可能,具体请看刘汝佳<<训练指南>>P323)
注意:下面解题的过程中,比如我们要设定i为假,那么我们不是mark[i*2]=true,而是添加一条i*2+1->i*2的边,即只要i设为真了,那么就会使得导出矛盾。因为每个节点只有两种选择,所以上面添加边的思路更直观。反而每次都去转换成一条或语句更不直观。我基本上所有的题解都是以添加边的角度来做的。
下面给出2-SAT的模板代码:
#include<cstdio> #include<cstring> #include<vector> using namespace std; const int maxn=10000+10; struct TwoSAT { int n;//原始图的节点数(未翻倍) vector<int> G[maxn*2];//G[i]==j表示如果mark[i]=true,那么mark[j]也要=true bool mark[maxn*2];//标记 int S[maxn*2],c;//S和c用来记录一次dfs遍历的所有节点编号 void init(int n) { this->n=n; for(int i=0;i<2*n;i++) G[i].clear(); memset(mark,0,sizeof(mark)); } //加入(x,xval)或(y,yval)条件 //xval=0表示假,yval=1表示真 void add_clause(int x,int xval,int y,int yval) { x=x*2+xval; y=y*2+yval; G[x^1].push_back(y); G[y^1].push_back(x); } //从x执行dfs遍历,途径的所有点都标记 //如果不能标记,那么返回false bool dfs(int x) { if(mark[x^1]) return false;//这两句的位置不能调换 if(mark[x]) return true; mark[x]=true; S[c++]=x; for(int i=0;i<G[x].size();i++) if(!dfs(G[x][i])) return false; return true; } //判断当前2-SAT问题是否有解 bool solve() { for(int i=0;i<2*n;i+=2) if(!mark[i] && !mark[i+1]) { c=0; if(!dfs(i)) { while(c>0) mark[S[--c]]=false; if(!dfs(i+1)) return false; } } return true; } };
2-SAT应用
POJ 3207 Ikki‘s Story IV - Panda‘s Trick(2-SAT):圆内外连线问题。解题报告!
POJ 3678 Katu Puzzle(2-SAT):与或非判断数值。解题报告!
POJ 3683 Priest John‘s Busiest Day(2-SAT输出方案):判断区间是否重叠。解题报告!
POJ 3648 Wedding(2-SAT):婚礼座位安排。解题报告!
POJ 2723 Get Luffy Out(2-SAT):钥匙开门问题。解题报告!
POJ 2749 Building roads(2-SAT):如何构建道路。解题报告!
POJ 2296 Map Labeler(2-SAT):放置正方形。解题报告!
HDU 3062 Party(2-SAT简单题):夫妻选择出席聚会。解题报告!
HDU 1814 Peaceful Commission(2-SAT:最小字典序):代表参加会议。解题报告!
HDU 3622 Bomb Game(2-SAT+二分):放置炸弹问题。解题报告!
HDU 3715 Go Deeper(2-SAT):程序递归深度问题。解题报告!
HDU 1824 Let‘s go home(2-SAT):队员去留问题。解题报告!
POJ 3905 Perfect Election(简单2-SAT):选举问题。解题报告!
HDU 4115 Eliminate the Conflict (2-SAT):出拳问题。解题报告!