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;
}
View Code


第二种类型,强连通求可行解。
典型题目 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;
}
View Code

 

第三种题型,强连通+拓扑排序求任意解。

典型题目: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;
}
View Code

 

 

 

 
posted @ 2014-09-11 00:09  pengmq  阅读(519)  评论(0编辑  收藏  举报