2-SAT问题

最近看到一篇很有意思的文章,于是就转过来了

这篇文章我们提到过sat问题,sat问题是第一个npc问题,具体是这样的SAT全称是satisfiability,他是问对于一个合取范式,是否有一种输入使得他的输出是1,具体点就是类似这样的布尔表达式(x1 or x2 or x3)and(x3 or x4)and(not x1 or x5)对于所有的x是否有一种01取值,使得最后的结果是1。而2-sat问题就是每一个由or连接的子式都只包含两个变量,比如这样(x1 or x2) and (not x3 or x1),2-sat问题是有多项式解法的,而3-sat就是npc问题了,前段时间有人宣称证明了p=np就是因为他自己找到了3-sat的多项式解法,当然最后被证明解法是错误的。。。那么对于2-sat问题解法而言,经典的就是利用强连通分支的算法来解决,最近上的coursera上的algo2有个随机算法也很有趣这里我们也要讲一下。

先来看经典的利用强连通分支的图论解法。我们把每个变量x都拆成2个点,两个点x和~x分别表示这个点取1和这个点取0,所以最后我们就是在这2n个点中选择满足要求的n个点。对于每个子式,假设子式是(x1 or x2),对于2-sat问题而言我们需要每个子式都得1,也就是对于这个子式而言,x1和x2至少要取一个,对应于图中就是,如果我们取~x1就必须取x2,取~x2就必须取x1,所以我们就在图中从~x1到x2和从~x2到x1连两条有向边。同样的如果子式中有not也是类似方法,比如(not x1 or x2)那么就是X1到x2和~x2到~x1两条有向边。一开始的图片构成的图表示的这个式子。

(x0∨x2)∧(x0∨¬x3)∧(x1∨¬x3)∧(x1∨¬x4)∧(x2∨¬x4)∧(x0∨¬x5)∧(x1∨¬x5)∧(x2∨¬x5)∧(x3∨x6)∧(x4∨x6)∧(x5∨x6)

构建好图之后对图求强连通分支,很显然的如果xi和~xi在同一个强连通分支中那么就是不存在解的。之后进行染色判定,强连通分支缩点之后,把所有的边反向,然后按照拓扑排序的顺序遍历节点,如果节点没有被染色,就涂成红色,然后把和这个点互斥的点(所谓互斥的点就是如果x和~x所在的点),以及这个点的子孙都涂成蓝色,这样取出红色的点就是满足条件的解。这里就不证明了,详细的可以看伍昱的《由对称性解2-SAT问题》和赵爽的《2-SAT解法浅析》两篇。看证明的时候注意对称性,就是说如果x,y在同一个连通分支,那么~x,~y也在同一个连通分支,如果x到y有路,那么~y到~x也有路,注意这个对称性的特点的话,那两篇文章里的证明也就不难看懂了。

talk is easy,show me the code,那么让我们看一下代码吧,这里就用poj 3648来举例。

#include <cstdio>
#include <cstring>

const int maxn=10010;
int n, m, nxt[maxn], head[maxn], pnt[maxn], ne, e, a, b, a0, b0;
char c1, c2;
int nnxt[maxn], nhead[maxn], npnt[maxn];
int order[maxn], norder, id[maxn], v[maxn];
int ans[maxn], op[maxn];

void dfs(int d){
    v[d] = 1;
    for(int i=head[d]; i!=-1; i=nxt[i])
        if(!v[pnt[i]])
            dfs(pnt[i]);
    order[norder++] = d;
}
void ndfs(int d, int k){
    v[d] = 1;
    id[d] = k;
    for(int i=nhead[d]; i!=-1; i=nnxt[i])
        if(!v[npnt[i]])
            ndfs(npnt[i], k);
}
void addedge(int s, int t){
    pnt[e] = t; nxt[e] = head[s]; head[s] = e++;
}
void addnedge(int t, int s){
    npnt[ne] = s; nnxt[ne] = nhead[t]; nhead[t] = ne++;
}
void color(int d){
    ans[d] = 2;
    for(int i=head[d]; i!=-1; i=nxt[i])
        if(!ans[pnt[i]])
            color(pnt[i]);
}
int main(){
    while(1){
        norder = e = ne = 0;
        memset(head, -1, sizeof head);
        memset(nhead, -1, sizeof nhead);
        scanf("%d%d", &n, &m);
        if(!n&&!m)
            break;
        for(int i=0; i<m; ++i){
            scanf("%d%c%d%c", &a, &c1, &b, &c2);
            if(c1=='h')
                a0 = n+a;
            else{
                a0 = a;
                a += n;
            }
            if(c2=='h')
                b0 = n+b;
            else{
                b0 = b;
                b += n;
            }
            addedge(a0, b);
            addnedge(b, a0);
            addedge(b0, a);
            addnedge(a, b0);
        }
        addedge(0, n);
        addnedge(n, 0);
        memset(v, 0, sizeof v);
        for(int i=0; i<2*n; ++i)
            if(!v[i])
                dfs(i);
        int k=0;
        memset(v, 0, sizeof v);
        memset(id, -1, sizeof id);
        for(int i=norder-1; i>=0; --i)
            if(!v[order[i]])
                ndfs(order[i], k++);
        int mark = 1;
        for(int i=0; i<n; ++i)
            if(id[i]==id[i+n])
                mark = 0;
        if(!mark){
            printf("bad luck\n");
            continue;
        }
        for(int i=0; i<n; ++i){
            op[id[i]] = id[i+n];
            op[id[i+n]] = id[i];
        }
        e = norder = 0;
        memset(head, -1, sizeof head);
        memset(v, 0, sizeof v);
        memset(ans, 0, sizeof ans);
        for(int i=0; i<n; ++i)
            for(int j=nhead[i]; j!=-1; j=nnxt[j])
                addedge(id[i], id[npnt[j]]);
        for(int i=0; i<k; ++i)
            if(!v[i])
                dfs(i);
        for(int i=norder-1; i>=0; --i)
            if(!ans[order[i]]){
                ans[order[i]] = 1;
                color(op[order[i]]);
            }
        for(int i=1; i<n; ++i){
            if(ans[id[i]] == 1)
                printf("%dh", i);
            else
                printf("%dw", i);
            if(i<n-1)
                printf(" ");
            else
                printf("\n");
        }
    }
    return 0;
}

代码自我感觉还是比较清晰的就不解释了,整个过程就是按照上面说的算法进行的。ok,到这里而言acmer的内容已经足够了,下面我们来说说algo2里提到的随机算法。

在讲随机算法之前,首先要介绍“random walks on a line”,假设我们在数轴的点0的位置,在点0的话,下一个时刻我们会移动到点1上,如果在其他的点x,我们有50%的概率下一个时刻移动到点x-1上,有50%的概率移动到x+1上。现在让我们来算一下,从点0开始移动到点n,期望需要移动多少步。这个期望的算法和之前写的这篇的方法差不多。设E(i)是从点i(i<=n)移动到点n所需要的期望的步数。于是我们有

E(n)=0
E(0)=E(1)+1
E(i)=0.5∗(1+E(i+1))+0.5∗(1+E(i−1))

有上面的式子我们可以得到最后的结果,推导比较简单,就不赘述了

E(0)=n2

设Tn表示从0走到n的步数,于是我们有

n2=E(Tn)=∑2n2k=0(k∗P(Tn=k))+∑∞k=2n2+1(k∗P(Tn=k))
因为∑2n2k=0(k∗P(Tn=k))≥0
所以n2≥∑∞k=2n2+1(k∗P(Tn=k))
≥∑∞k=2n2+1((2n2)∗P(Tn=k))
=2n2∗P(Tn>2n2)

于是我们得到

P(Tn>2n2)≤12

也就是说我们从0开始走2*n*n步,我们有大于1/2的概率已经经过n这个点了。

ok回到我们的2-sat问题。

我们的算法是这样的,我们开始时随机给变量赋值0或者1,然后迭代足够多的次数(大于2*n*n的次数),如果某次迭代,我们找到了答案就可以返回结果了,如果我们迭代足够多的次数都没找到答案就返回无解。每次迭代,我们找一个不满足要求的子式,然后两个变量随机挑一个改变他的值。

假设我们现在的解是a,满足要求的解是a*,那么我们的解和满足要求的解,值不同的变量的个数是x,某次迭代,我们找到一个子式(xi or xj)不满足条件,因为子式不满足条件,所以xi和xj不可能同时和a*里值是相同的,如果两个值都不同,那么我们这次改动使得x=x-1,如果有一个值不同,那么我们有50%的概率使得x=x-1有50%的概率使得x=x+1,所以,答案变好的概率要大于50%,所以迭代2*n*n次能得到解的概率就会小于1/2。

很有趣的算法,并且我们用这个算法,可以AC掉poj 3648这道题!看代码

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>

const int maxn=10010;
int n, m, a, b;
char c1, c2;
int ans[maxn], record[maxn][2];

int get(int x){
    if(x<n)
        return ans[x];
    return !ans[x-n];
}
int main(){
    //srand(time(NULL));这里如果调用time,poj会报 runtime error的错误
    srand(7);
    while(1){
        scanf("%d%d", &n, &m);
        memset(ans, 0, sizeof ans);
        if(!n&&!m)
            break;
        for(int i=0; i<m; ++i){
            scanf("%d%c%d%c", &a, &c1, &b, &c2);
            if(c1=='w')
                a += n;
            if(c2=='w')
                b += n;
            record[i][0] = a;
            record[i][1] = b;
        }
        int mark = 0;
        for(int i=0; i<5*n*n; ++i){
            mark = 1;
            for(int j=0; j<m; ++j)
                if((record[j][0]%n!=record[j][1]%n)&&!(get(record[j][0])||get(record[j][1]))){
                    int tmp = rand()%2;
                    if(record[j][0]%n==0)
                        tmp = 1;
                    if(record[j][1]%n==0)
                        tmp = 0;
                    ans[record[j][tmp]%n] = 1-ans[record[j][tmp]%n];
                    mark = 0;
                    break;
                }
            if(mark)
                break;
        }
        if(!mark){
            printf("bad luck\n");
            continue;
        }
        for(int i=1; i<n; ++i){
            if(ans[i])
                printf("%dh", i);
            else
                printf("%dw", i);
            if(i<n-1)
                printf(" ");
            else
                printf("\n");
        }
    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值