2-SAT问题是这样的:
有n个布尔变量xi,另外有m个需要满足的条件,每个条件的形式都是“xi为真\假 或者xj为真\假”。比如“x1为真或者x3为假”、“x7为假或者x2为假 ”都是合法的条件,注意这里的“或”是指两个条件至少有一个是正确的,比如x1和x3一共有3种组合满足“x1为真或者x3为假”,即x1真x3真,x1真x3假,x1假x3假。2-SAT问题的目标是给每个变量赋值,使得所有条件得到满足。
2-SAT的解法有多种不同的叙述方式,这里采用一种比较容易理解,且效率也不错的方式。构造一张有向图G,其中每个变量xi拆成两个结点2i和2i+1,分别表示xi为真和xi为假。最后要为每个变量选其中的一个结点标记。比如:若标记了结点2i,表示xi为真,如果标记了2i+1,表示xi为假
对于xi为真或者xj为真这一条件,我们连一条有向边2i+1——>2j,表示如果标记结点2i+1,那么也必须标记结点2j,(因为如果xi为假,则xj必须为真才能使条件成立 PVQ 等价于 (非P)->Q,离散数学知识)这条有向边相当于“推导出”的意思,同理,还需要连一条有向边2j+1——>2i。对于其他情况,也可以类似连边。换句话说,每个条件对应两条“对称”的边(但有时会出现已知某个变量为某个取值的情况,导致整个图不对称)
接下来逐一考虑每个没有赋值的变量,设为xi。我们先假定它为真,然后标记结点2i,并且沿着有向边标记所有能标记的结点。如果标记过程中发现某个变量对应的两个结点都被标记,则“xi为真”这个假定不成立,需要改成xi为假,然后重新标记
注意这个算法没有回溯过程。如果当前考虑的变量不管赋值为真还是假都会引起矛盾,可以证明整个2-SAT问题无解(即使调整以前赋值的其他变量也没有用)
struct TwoSAT
{
int n;
vector<int> G[maxn*2];
bool mark[maxn*2];
int S[maxn*2],c;
bool dfs(int x)
{
if(mark[x^1])return false;//' ^ '是异或运算符100010(34)^000001==100011(35)
//100011(35)^1=100010(34)
//所以,x^1表示,跟x一组的另一个标记结点,即,如果x=2i,则x^1=2i+1,如果x=2i+1,x^1=2i
//如果x^1为真,则x一定不能为真了
if(mark[x])return true;
mark[x]=true;//如果x和x^1都没有被标记,则先假定x为真
s[c++]=x;//s数组是标记dfs过程中遍历的所有点,一旦遇到矛盾,则dfs过程中所有假设为真的结点都要设为假
for(int i=0;i<G[x].size();i++)
{
if(!dfs(G[x][i])) return false;
}
return true;
}
void init(int n)//initialization,初始化
{
this->n=n;
for(int i=0;i<n*2;i++) G[i].clear();
memset(mark,0,sizeof(mark));
}
//x=xval or y=yval
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);
}
bool solve()
{
for(int i=0;i<n*2;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;//i和i+1都不能选,则表示无解
}
}
}
return true;
}
};