定义
每个方程含有n个未知数的布尔方程组称为n-SAT问题,当n大于2的时候该问题为NP-Hard问题。当n等于2时,可以转化为图论问题求解。
解法
每一个变量拆分为两个点,代表0和1两种状态,将方程组转化为有向边,代表点之间的依赖关系,最后的方案便是选出一个点集,该点集的所有出边所连的点全在该点集内,也就是强联通分量。这些点便代表了变量的取值情况;当一个变量拆分出的两个点位于同一个强联通分量中时,说明该变量的取值相互矛盾,此时无解。
具体求解时,先根据限制条件连边,然后用Tarjan求强联通分量,再判断是否有一个变量的两个分点在一个强联通分量中即可。
举个例子,有两个变量X和Y,拆分出的点为X,X',Y,Y',其中X,Y表示变量值取1,另外两个变量代表变量值取0.假如有如下限制关系:
X|Y=1
则连边:
X'->Y,Y'->X
在这种连边情况下,如果选择点X',则一定会选择Y,但是选择Y不一定选择X'。观察后发现,这种连边符合上面的布尔约束条件。
相关的连边情况整理如下:
X|Y=1:X'->Y,Y'->X
X\&Y=0:X->Y',Y->X'
X\&Y=1:X->Y,Y->X
X|Y=0:X'->Y',Y'->X'
X=1,Y=0:X'->Y',Y->X
X=0,Y=1:Y'->X',X->Y
X=Y:X'->Y',Y'->X',X->Y,Y->X
例题
[BZOJ1823][JSOI2010]满汉全席
题目大意:
有n个布尔变量,和m对限制条件。每对限制条件限制了两个变量的取值,当且仅当存在一种取值方案,使得每对限制条件至少满足其中一条时有解。求在这些约束条件下是否存在解。
解法:
2-SAT裸题,参考上面连边方式3-5条即可
注意清空变量。。
代码:
#include<cstring>
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<vector>
#include<cmath>
#include<queue>
#include<map>
#define N 220
#define l(x) (x<<1)
#define r(x) ((x<<1)+1)
#define LL long long
#define INF 0x3f3f3f3f
using namespace std;
int n,m,i,d,dp,cnt,T;
int tag[N],dfn[N],low[N],f[N],z[N];
char a[20],b[20];
vector <int> son[N];
inline int Abs(int x){return (x<0)?-x:x;}
inline void Swap(int &x,int &y){x^=y^=x^=y;}
inline int Min(int a,int b){return (a<b)?a:b;}
inline int Max(int a,int b){return (a>b)?a:b;}
inline int read(){
int p=0; char c=getchar();
while (c<48||c>57) c=getchar();
while (c>=48&&c<=57) p=(p<<1)+(p<<3)+c-48,c=getchar();
return p;
}
inline void Find(int x){
++cnt;
while (z[dp]!=x){
tag[z[dp]]=cnt; f[z[dp]]=0; z[dp--]=0;
}
tag[x]=cnt; f[z[dp]]=0; z[dp--]=0;
}
inline void Tarjan(int x){
int i=0;
dfn[x]=low[x]=++d; f[x]=1; z[++dp]=x;
for (i=0;i<son[x].size();i++)
if (!dfn[son[x][i]]){
Tarjan(son[x][i]);
low[x]=Min(low[x],low[son[x][i]]);
} else if (f[son[x][i]])
low[x]=Min(low[x],dfn[son[x][i]]);
if (low[x]==dfn[x]) Find(x);
}
inline void Ready(){
int i=0,j=0,p=0,q=0,x=0,y=0;
memset(tag,0,sizeof(tag)); memset(z,0,sizeof(z));
memset(dfn,0,sizeof(dfn)); memset(low,0,sizeof(low));
memset(f,0,sizeof(f)); d=dp=cnt=0;
for (i=1;i<=2*n;i++) son[i].clear();
for (i=1;i<=m;i++){
scanf("%s%s",&a,&b);
p=(a[0]=='m')?0:1; q=(b[0]=='m')?0:1;
x=y=0;
for (j=1;j<strlen(a);j++) x=(x<<3)+(x<<1)+a[j]-48;
for (j=1;j<strlen(b);j++) y=(y<<3)+(y<<1)+b[j]-48;
if (p+q==2){
son[x+n].push_back(y); son[y+n].push_back(x);
} else
if (p+q==0){
son[x].push_back(y+n); son[y].push_back(x+n);
} else
if (p==1){
son[x+n].push_back(y+n); son[y].push_back(x);
} else {
son[y+n].push_back(x+n); son[x].push_back(y);
}
}
}
inline void Work(){
int i=0;
for (i=1;i<=2*n;i++)
if (!dfn[i]) Tarjan(i);
for (i=1;i<=n;i++)
if (tag[i]==tag[i+n]) {
printf("BAD\n"); return;
}
printf("GOOD\n");
}
int main(){
scanf("%d",&T);
while (T--){
scanf("%d%d",&n,&m);
Ready(); Work();
}
return 0;
}
[Kattis]Ghostbusters 2
题目大意:
在平面上有n个整点,每个整点需要选择一种攻击方向(横或者纵),所有整点有一个共同的整数攻击半径R,每个点的攻击范围是该点沿攻击方向向两边延伸R个单位。为使所有点的攻击范围互不重合,求R的最大值。
解法:
春训团队赛的一道题。
首先设横向攻击为1,纵向攻击为0。然后二分R,如果有同一列两个点不可同时纵向攻击,则构造方程
X|Y=1
如果有同一行两个点不可同时横向攻击,则构造方程
X\&Y=0
之后按照2-SAT解法求是否有合法方案即可。
代码:
#include<cstring>
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<vector>
#include<cmath>
#include<queue>
#include<map>
#define N 8010
#define l(x) (x<<1)
#define r(x) ((x<<1)+1)
#define LL long long
#define INF 0x3f3f3f3f
using namespace std;
int i,n,cnt,dp,d;
int x[N],y[N],low[N],dfn[N],z[N],f[N],v[N],tag[N],l[N];
vector <int> son[N];
inline int Abs(int x){return (x<0)?-x:x;}
inline void Swap(int &x,int &y){x^=y^=x^=y;}
inline int Min(int a,int b){return (a<b)?a:b;}
inline int Max(int a,int b){return (a>b)?a:b;}
inline int read(){
int p=0; char c=getchar();
while (c<48||c>57) c=getchar();
while (c>=48&&c<=57) p=(p<<1)+(p<<3)+c-48,c=getchar();
return p;
}
inline void Ready(){
int i=0,j=0;
for (i=1;i<=n;i++)
for (j=1;j<=n;j++)
if (x[i]==x[j]&&i!=j){
l[i]++; break;
}
for (i=1;i<=n;i++)
for (j=1;j<=n;j++)
if (y[i]==y[j]&&i!=j){
l[i]++; break;
}
for (i=1;i<=n;i++) l[i]=(l[i]==2)?1:0;
}
inline void Work(int x){
cnt++;
while (z[dp]!=x){
tag[z[dp]]=cnt; f[z[dp--]]=0;
}
tag[x]=cnt; f[z[dp--]]=0;
}
inline void Tarjan(int x){
int i=0;
dfn[x]=low[x]=++d; z[++dp]=x; f[x]=1;
if (son[x].size())
for (i=0;i<son[x].size();i++)
if (!v[son[x][i]]){
v[son[x][i]]=1; Tarjan(son[x][i]);
low[x]=Min(low[x],low[son[x][i]]);
} else
if (f[son[x][i]]) low[x]=Min(low[x],dfn[son[x][i]]);
if (low[x]==dfn[x]) Work(x);
}
inline bool Check(int t){
int i=0,j=0;
for (i=1;i<=2*n;i++) son[i].clear();
memset(low,0,sizeof(low)); memset(dfn,0,sizeof(dfn));
memset(f,0,sizeof(f)); memset(v,0,sizeof(v));
memset(tag,0,sizeof(tag)); memset(z,0,sizeof(z));
dp=cnt=d=0;
for (i=1;i<=n;i++)
for (j=i+1;j<=n;j++){
if (l[i]+l[j]!=2) continue;
if (x[i]==x[j]&&2*t>=Abs(y[i]-y[j])){
son[n+i].push_back(j); son[n+j].push_back(i);
}
if (y[i]==y[j]&&2*t>=Abs(x[i]-x[j])){
son[i].push_back(n+j); son[j].push_back(n+i);
}
}
for (i=1;i<=2*n;i++)
if (!v[i]&&l[i]){
v[i]=1; Tarjan(i);
}
for (i=1;i<=n;i++)
if (l[i]&&tag[i]==tag[i+n]) return 0;
return 1;
}
inline void Work(){
int i=0,j=0,low=0,high=INF,mid=0;
if (Check(INF)) {
cout<<"UNLIMITED"<<endl;
return;
}
while (low<=high){
mid=(low+high)>>1;
if (Check(mid)) low=mid+1;
else high=mid-1;
}
while (Check(low+1)) low++;
while (!Check(low)) low--;
printf("%d\n",low);
}
int main(){
scanf("%d",&n);
for (i=1;i<=n;i++) scanf("%d%d",&x[i],&y[i]);
Ready(); Work();
return 0;
}