2-SAT 小结
PS:今天(2014.10.27)准备PPT,明天在组合数学课上与大家一起分享一下2-SAT。我以为是一件简单的事情。但是,当我看了自己这篇博客以后,发现居然还是不懂。很多资料不全,也没仔细讲。整理了一下,又修改了。尽量让自己下次再看到这博客的时候,一遍下来就能懂。虽然引用了别人的博客,我尽量讲得不需要进入别人的博客就能看懂。
一些这方面的内容写得很好的博客。(排列是随机的。可以略过了)
http://www.cnblogs.com/kuangbin/archive/2012/10/05/2712429.html 解释很清晰,但是代码风格不是我想要的。
http://blog.csdn.net/hqd_acm/article/details/5881655 内容很详细,方法也很好,不过没有代码是个致命缺憾。
定义:
如果存在一个真值分配,使得布尔表达式的取值为真,则这个布尔表达式称为可适定的,简称SAT。
例如(x1+x2)(┐x1+┐x2)是一个布尔表达式,如果p(x1)=“真”,p(x1)=“假”,则表达式的值为真,则这个表达式是适定的。上面的“+”是逻辑或。不是所有的布尔表达式都是可适定的,必须存在使得表达式为真的情况。
2-SAT:设X={x1,x2,…,xn,┐x1,┐x2,…,┐xn},布尔表达式中只有两种形式,一种是单个布尔变量a属于X,另一种是两个变量a,b的或(a+b),a,b都属于X。
例如:(a+b)·(c+d)·e ,这是一个合取范式,每一个布尔表达式,如(a+b)最多只有两个元素。k-SAT,当k>2时,问题为完全的。
2-SAT问题就是求使表达式为真的解。
学习2-SAT必须先掌握强连通和拓扑排序。
2-SAT有三种题型,不同题型的解题方法不同。
第一种,暴力求最小字典序解。复杂度O(n*m)。
第二种,强连通判断有没有解。O(n+m)。
第三种,强连通+缩点+拓扑排序,求任意解。O(n+m)(第三种会了,第二种也就会了,严格说来,第二种应该和第三种合并成第二种)
虽然题型不同,但是建图的方式还是相同的。边u->v的含义是选择了u ,就必须选择v。
第一种类型,暴力求最小字典序解。
典型题目 hdu1814 Peaceful Commission
题目大意:
根据宪法,Byteland民主共和国的公众和平委员会应该在国会中通过立法程序来创立。
不幸的是,由于某些党派代表之间的不和睦而使得这件事存在障碍。
此委员会必须满足下列条件:
1、每个党派都在委员会中恰有1个代表,
2、如果2个代表彼此厌恶,则他们不能都属于委员会。
3、每个党在议会中有2个代表。代表从1编号到2n。 编号为2i-1和2i的代表属于第I个党派。
计算决定建立和平委员会是否可能,若行,则列出委员会的成员表。求出字典序最小的解
题目分析:
此题麻烦之处在于要求出字典序最小的解。所以才用暴力枚举来做,也就是一个一个试过去。找到解就是最小的字典序解。
我们给结点染色,假设白色是未染色,红色是要选取的结点,蓝色是抛弃的结点。
首先从第一个点开始染色,染成红色,同时将同组另一节点染成蓝色,然后将这个点的所有后继结点也染成红色,同时开一个数组记录都染了哪些结点。如果后来发现某结点的后继是蓝色,说明本次染色失败,因为碰到了矛盾(假如一个结点被选取,那么所有后继肯定也得全部选取)。那么因为刚才染色的时候记录了染色的结点,靠这个数组将结点全部还原回白色。然后从第二个结点开始探索,直到全部染色完毕,最后的红色结点就是答案。
这种方法的时间复杂度为O(m*n)。m为边数,n为顶点数。
此题最好的题解(我认为)是http://blog.163.com/shengrui_step/blog/static/20870918720141201262750/ 染色,很容易懂
下面是我的代码:vis[]标记选择,-1为没有标记,1是标记为选择,0为放弃。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int N=20010,M=100010; int vis[N],head[N],ch[N]; int tot,cnt; struct node { int to,next; }edge[M]; void addedge(int i,int j) { edge[tot].to=j;edge[tot].next=head[i];head[i]=tot++; } int dfs(int u) { if(vis[u]==1) return 1; if(vis[u]==0) return 0; vis[u]=1; vis[u^1]=0; ch[cnt++]=u; for(int k=head[u];k!=-1;k=edge[k].next) if(!dfs(edge[k].to)) return 0; return 1; } int sat2(int n) { for(int i=0;i<2*n;i++) { if(vis[i]==-1) { cnt=0; if(!dfs(i)) { for(int j=0;j<cnt;j++) vis[ch[j]]=vis[ch[j]^1]=-1; if(!dfs(i^1)) return 0; } } } return 1; } void init() { tot=0; memset(head,-1,sizeof(head)); memset(vis,-1,sizeof(vis)); } int main() { //freopen("test.txt","r",stdin); int n,m,i,j,k; while(scanf("%d%d",&n,&m)!=EOF) { init(); while(m--) { scanf("%d%d",&i,&j); i--;j--; addedge(i,j^1); addedge(j,i^1); } if(sat2(n)) { for(i=0;i<2*n;i++) if(vis[i]==1)printf("%d\n",i+1); } else printf("NIE\n"); } return 0; }
第二种类型,强连通求可行解。
典型题目 hdu3622 Bomb Game
参照http://www.cnblogs.com/kuangbin/archive/2012/10/05/2712424.html 不过,要说一说,强连通的三个模版中,二次搜索的那个很弱的。
题目大意:
给n对炸弹可以放置的位置(每个位置为一个二维平面上的点),每次放置炸弹是时只能选择这一对中的其中一个点,每个炸弹爆炸的范围半径都一样,控制爆炸的半径使得所有爆炸范围都不相交(可以相切),求解这个最大半径。
分析:
求强连通的目的是什么?属于一个强连通的点是必须一起选择的。如果一对关系点(只能且必须选其中一个)属于同一个强连通,就产生了矛盾,就无解。这是这种题型的解题思想。
这题多了二分查找,显得麻烦。但是二分技术在最大流和二分匹配中都有的。懂了就好了。
时间复杂度是O(m),主要是求强连通分量。
下面的我的代码:
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #include<cmath> using namespace std; const int N =210, M=100010; const double eps=10e-6; struct node { int to, next; }edge[M]; int head[N], low[N], dfn[N], sta[N], belg[N], num[N]; bool vis[N]; int scc,index,top, tot; void tarbfs(int u) { int i,j,k,v; low[u]=dfn[u]=++index; sta[top++]=u; vis[u]=1; for(i=head[u];i!=-1;i=edge[i].next) { v=edge[i].to; if(!dfn[v]) { tarbfs(v); if(low[u]>low[v]) low[u]=low[v]; } else if(vis[v]&&low[u]>dfn[v]) low[u]=dfn[v]; } if(dfn[u]==low[u]) { scc++; do { v=sta[--top]; vis[v]=0; belg[v]=scc; num[scc]++; } while(v!=u) ; } } bool Tarjan(int n) { memset(vis,0,sizeof(vis)); memset(dfn,0,sizeof(dfn)); memset(num,0,sizeof(num)); memset(low,0,sizeof(low)); index=scc=top=0; for(int i=0;i<n;i++) if(!dfn[i]) tarbfs(i); for(int i=0;i<n;i+=2) if(belg[i]==belg[i+1]) return 0; return 1; } void init() { tot=0; memset(head,-1,sizeof(head)); } void addedge(int i,int j) { edge[tot].to=j; edge[tot].next=head[i];head[i]=tot++; } struct point { int x,y; }s[N]; double Distance(point a, point b) { return sqrt((double)(a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y)); } int main() { //freopen("test.txt","r",stdin); int n,i,j,k; double left,right,mid; while(scanf("%d",&n)!=EOF) { for(i=0;i<n;i++) scanf("%d%d%d%d",&s[2*i].x,&s[2*i].y,&s[2*i+1].x,&s[2*i+1].y); left=0; right=40000.0; n*=2; while(right-left>=eps) { mid=(left+right)/2; init(); for(i=0;i<n-2;i++) { if(i%2==0) k=i+2; else k=i+1; for(j=k;j<n;j++) if(Distance(s[i],s[j])<2*mid) { addedge(i,j^1); addedge(j,i^1); } } if(Tarjan(n)) left=mid; else right=mid; } printf("%0.2f\n",right); } return 0; }
第三种题型,强连通+拓扑排序求任意解。
典型题目:poj3622
讲得特别好的博客: http://blog.csdn.net/qq172108805/article/details/7603351
题意:有对情侣结婚,请来n-1对夫妇,算上他们自己共n对,编号为0~~n-1,他们自己编号为0.所有人坐在桌子两旁,新娘不想看到对面的人有夫妻关系或偷奸关系,若有解,输出一组解,无解输出bad luck 。
题目分析:
题目的大致意思可以理解为把HDU1814求字典序最小的解改为求任意解。
这一类的题目的做法是:
第一步,建图。规则是边<u,v>表示选u就必须选v。
第二步,求强连通分量。这一步我觉得就是用模版了。
第三步,对每一对点进行判断,如果它们属于同一强连通,就无解。有解才进行下面的步骤。比如此题,通奸的人不能做同一边。
第四步,对缩点后的图进行反向建图。
第五步,对建的图进行拓扑排序染色。选择一个没有被染色的点a,将其染成红色,把所有与a点矛盾的点b和b的子孙染成黑色。不断重复操作,知道没有点可以染色而止。
最后,红色的点就是一个解。
时间复杂度是O(m),主要是求强连通分量。
要注意的是:强连通缩点以后,建立的是反向图。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int N =210, M=100010; struct node { int to, next;; }edge[M],e[M]; int stk[N],stk2[N],head[N],low[N],belg[N]; int id[N],vis[N],h[N], t, que[N],dui[N]; int cn,cm,tot,scc,lay; int Garbowbfs(int cur,int lay) { stk[++cn]=cur; stk2[++cm]=cur; low[cur]=++lay; for(int i=head[cur];i!=-1;i=edge[i].next) { int v=edge[i].to; if(!low[v]) Garbowbfs(v,lay); else if(!belg[v]) while(low[stk2[cm]]>low[v]) cm--; } if(stk2[cm]==cur) { cm--; scc++; do belg[stk[cn]]=scc; while(stk[cn--]!=cur) ; } return 0; } bool Garbow(int n) { scc=lay=0; memset(belg,0,sizeof(belg)); memset(low,0,sizeof(low)); for(int i=0;i<n;i++)//顶点从0开始(若是1需要修改) if(!low[i]) Garbowbfs(i,lay); for(int i=0;i<n;i+=2) { if(belg[i]==belg[i+1]) return 0; dui[belg[i]]=belg[i+1]; dui[belg[i+1]]=belg[i]; } return 1; } void addedge(int i,int j) { edge[tot].to=j; edge[tot].next=head[i];head[i]=tot++; } void add_e(int i,int j) { e[t].to=j; e[t].next=h[i];h[i]=t++; } void init() { tot=t=0; memset(head,-1,sizeof(head)); memset(h,-1,sizeof(h)); memset(vis,-1,sizeof(vis)); memset(id,0,sizeof(id)); } void topo() { int i,k,j=0; for(i=0;i<scc;i++) if(id[i]==0) que[j++]=i; for(i=0;i<j;i++) { int u=que[i]; if(vis[u]==-1) { vis[u]=1; vis[dui[u]]=0; } for(k=h[u];k!=-1;k=e[k].next) { int v=e[k].to; id[v]--; if(id[v]==0) que[j++]=v; } } } int main() { //freopen("test.txt","r",stdin); int n,m,i,j,k; char ch1,ch2; while(scanf("%d%d",&n,&m)!=EOF) { if(!n) break; init(); while(m--) { scanf("%d%c %d%c",&i,&ch1,&j,&ch2); if(ch1=='w') i=2*i; else i=2*i+1; if(ch2=='w') j=2*j; else j=2*j+1; addedge(i,j^1); addedge(j,i^1); } addedge(0,1); n*=2; if(Garbow(n)==0) {printf("bad luck\n"); continue;} for(i=0;i<n;i++)//反向图 { for(k=head[i];k!=-1;k=edge[k].next) { int u=belg[i], v=belg[edge[k].to]; if(u!=v) { add_e(v,u); id[u]++; } } } topo(); for(i=2;i<n;i+=2) { k=belg[i]; if(vis[k]==1)printf("%dh ",i/2); else printf("%dw ",i/2); } printf("\n"); } return 0; }