TWO-SAT题目总结

 

最小字典序输出

 

题意:

        现在有n个党派,每个党派有2个代表,我们需要从每个党派中选一个代表出来,构成一个n个人的立法委员会.但是可能有一些代表互相讨厌,所以他们不能同时出现在立法委员会中.现在问你是否存在一个合理的方案,且输出所有可能立法委员会的最小字典序结果.

分析:

        要输出最小字典序的2-SAT问题,用刘汝佳 训练指南上的方法是最简单的(即按字典序顺序构造每个选择即可). 这里我们把每个党派看出一个点,从0到n-1. 如果对于a(a从0到2*n-1)代表与b代表不和,那么G[a].push_back(b^1) 且 G[b].push_back(a^1).

AC代码:

#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
const int maxn= 8000+10;
struct TwoSAT
{
    int n;
    vector<int> G[maxn*2];
    int S[maxn*2], c;
    bool mark[maxn*2];
 
    bool dfs(int x)
    {
        if(mark[x^1]) return false;
        if(mark[x]) return true;
        mark[x]= true;
        S[c++]=x;
 
        for(int i=0;i<G[x].size();i++)
            if(!dfs(G[x][i])) return false;
        return true;
    }
 
    void init(int n)
    {
        this->n=n;
        for(int i=0;i<2*n;i++) G[i].clear();
        memset(mark,0,sizeof(mark));
    }
 
    void add_clause(int x,int y)//注意这里的修改
    {
        G[x].push_back(y^1);
        G[y].push_back(x^1);
    }
 
    bool solve()
    {
        for(int i=0;i<2*n;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;
            }
        }
        return true;
    }
 
    void print()
    {
        if(!solve()) printf("NIE\n");
        else
        {
            for(int i=0;i<2*n;i++)if(mark[i])
                printf("%d\n",i+1);
        }
    }
}TS;
int main()
{
    int n,m;
    while(scanf("%d%d",&n,&m)==2)
    {
        TS.init(n);
        while(m--)
        {
            int a,b;
            scanf("%d%d",&a,&b);
            a--, b--;
            TS.add_clause(a,b);
        }
        TS.print();
    }
    return 0;

 

题意:

        一个N个顶点和M条边的有向图,每个顶点能取0或1两个值.现在每条边被一个操作符(or,and,xor)以及一个值(0或1)标记了,表示a与b按操作符运算的结果是值(0或1).问你该有向图是否有可行解?

分析:

        由于每个点只能取0或1两个值,所以我们把该问题转化为2-SAT问题.原图中的每个点对应2-SAT中的每个点.对于每种运算有下列转换方式:

a and b = 0 转换为 a=0 或 b=0

a and b = 1 转换为 a=1 且 b=1 即添加边 2*a->2*a+1  2*b->2*b+1(只要a为0或b为0必然引起矛盾)

a or b = 0 转换为 2*a+1->2*a  2*b+1->2*b(只要a为1或b为1必然引起矛盾)

a or b = 1 转换为 a=1 或b=1

a xorb=0转换为 a=1且b=1 或 a=0且b=0 即连下面的边:

2*a->2*b    2*b->2*a    2*a+1->2*b+1        2*b+1->2*a+1.

a xor b=1 转换为a=1且b=0 或a=0且b=1 则连下面的边:

2*a+1->2*b      2*b->2*a+1      2*a->2*b+1     2*b+1->2*a

AC代码:


#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
const int maxn=1000+10;
struct TwoSAT
{
    int n;
    vector<int> G[maxn*2];
    int S[maxn*2],c;
    bool mark[maxn*2];
 
    bool dfs(int x)
    {
        if(mark[x^1]) return false;
        if(mark[x]) return true;
        mark[x]=true;
        S[c++]=x;
        for(int i=0;i<G[x].size();i++)
            if(!dfs(G[x][i])) return false;
        return true;
    }
 
    void init(int n)
    {
        this->n=n;
        for(int i=0;i<n*2;i++) G[i].clear();
        memset(mark,0,sizeof(mark));
    }
 
    void add_clause(int x,int xval,int y,int yval)//这里的函数做了修改,只加单向边
    {
        x=x*2+xval;
        y=y*2+yval;
        G[x].push_back(y);
    }
 
    bool solve()
    {
        for(int i=0;i<2*n;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;
            }
        }
        return true;
    }
}TS;
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    TS.init(n);
    int a,b,c;
    char op[10];
    for(int i=0;i<m;i++)
    {
        scanf("%d%d%d%s",&a,&b,&c,op);
        if(op[0]=='A')
        {
            if(c==0)
            {
                TS.add_clause(a,1,b,0);
                TS.add_clause(b,1,a,0);
            }
            else if(c==1)
            {
                TS.add_clause(a,0,a,1);
                TS.add_clause(b,0,b,1);
            }
        }
        else if(op[0]=='O')
        {
            if(c==0)
            {
                TS.add_clause(a,1,a,0);
                TS.add_clause(b,1,b,0);
            }
            else if(c==1)
            {
                TS.add_clause(a,0,b,1);
                TS.add_clause(b,0,a,1);
            }
        }
        else if(op[0]=='X')
        {
            if(c==0)
            {
                TS.add_clause(a,0,b,0);
                TS.add_clause(a,1,b,1);
                TS.add_clause(b,0,a,0);
                TS.add_clause(b,1,a,1);
            }
            else if(c==1)
            {
                TS.add_clause(a,0,b,1);
                TS.add_clause(a,1,b,0);
                TS.add_clause(b,0,a,1);
                TS.add_clause(b,1,a,0);
            }
        }
    }
    if(TS.solve()) printf("YES\n");
    else printf("NO\n");
    return 0;
}
 

输出方案

题意:

        有N对新人举行婚礼,且每次婚礼需要持续d时间,从s时间到t时间之间举行且只能选择s到s+d时间或t-d到t时间这两个完整的时间段举行.现在只有一个神父,问他有没有可能参加所有新人的婚礼(待完整段时间且任意两对新人的婚礼时间不重叠)? 输出一个可行的方案.

分析:

        每对新人的婚礼时间只有两种选择,直接就可以转化为2-SAT问题.其中如果对于第i个婚礼与第j个婚礼来说:

        假设i先办的时间区间为[a,b]而j后办的时间区间为[c,d],如何判断[a,b]与[c,d]是否发生了冲突呢?(边界相交不算).

        只有下面两种情况下区间[s1,e1]与区间[s2,e2]才规范相交.

        1. s1<e2 且 s2<e1  

        2. s2<e1 且 s1<e2

        仔细一看上面两种情况是相同的,只要相交的两个区间的e1 e2 > s1 s2 即可保证这两个区间相交.

        (仔细想想上面情况)

        然后对于冲突的每对新人添加边即可.

AC代码:

#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
const int maxn=1000+10;
struct Time
{
    int s,e,d;//开始,结束,持续
    Time(){}
    Time(int s,int e,int d):s(s),e(e),d(d){}
}t[maxn];
struct TwoSAT
{
    int n;
    vector<int> G[maxn*2];
    int S[maxn*2],c;
    bool mark[maxn*2];
 
    bool dfs(int x)
    {
        if(mark[x^1]) return false;
        if(mark[x]) return true;
        mark[x]=true;
        S[c++]=x;
        for(int i=0;i<G[x].size();i++)
            if(!dfs(G[x][i])) return false;
        return true;
    }
 
    void init(int n)
    {
        this->n=n;
        for(int i=0;i<n*2;i++) G[i].clear();
        memset(mark,0,sizeof(mark));
    }
 
    void add_clause(int x,int xval,int y,int yval)//这里做了修改,指x与y值有冲突
    {
        x=x*2+xval;
        y=y*2+yval;
        G[x].push_back(y^1);
        G[y].push_back(x^1);
    }
 
    bool solve()
    {
        for(int i=0;i<2*n;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;
            }
        }
        return true;
    }
}TS;
int main()
{
    int n;
    scanf("%d",&n);
    for(int i=0;i<n;i++)
    {
        int sh,sm,eh,em,d;
        scanf("%d:%d %d:%d %d",&sh,&sm,&eh,&em,&d);
        t[i]=Time(sh*60+sm,eh*60+em,d);
    }
    TS.init(n);
    for(int i=0;i<n;i++)
    for(int j=i+1;j<n;j++)
    {
        if(t[i].s < t[j].s+t[j].d && t[j].s < t[i].s+t[i].d )
            TS.add_clause(i,0,j,0);
        if(t[i].s < t[j].e && t[j].e-t[j].d < t[i].s+t[i].d )
            TS.add_clause(i,0,j,1);
        if(t[i].e-t[i].d < t[j].s+t[j].d && t[j].s < t[i].e)
            TS.add_clause(i,1,j,0);
        if(t[i].e-t[i].d < t[j].e && t[j].e-t[j].d < t[i].e)
            TS.add_clause(i,1,j,1);
    }
    if(!TS.solve()) printf("NO\n");
    else
    {
        printf("YES\n");
        for(int i=0;i<n;i++)
        {
            if(TS.mark[i*2])
                printf("%02d:%02d %02d:%02d\n",t[i].s/60,t[i].s%60,(t[i].s+t[i].d)/60,(t[i].s+t[i].d)%60);
            else
                printf("%02d:%02d %02d:%02d\n",(t[i].e-t[i].d)/60,(t[i].e-t[i].d)%60,t[i].e/60,t[i].e%60);
        }
    }
    return 0;
}

TWO-SAT输出

题意:

        有一对新人结婚,n-1对夫妇去参加婚礼.有一个很长的座子,新娘与新郎坐在座子的两边(相反).接下来n-1对夫妇就坐,其中任何一对夫妇都不能坐在同一边,且(有一些人有奸情)这些有奸情的两个人不能同时坐在新娘对面.(只能分开做,或者都坐到新娘一边去)。对于每个输入实例,输出应该坐在新娘同一边的人编号。

分析:

        由于有n对夫妇(0号表示新婚夫妻).所以我们这里用0表示第0对的妻子,1表示第0对的丈夫. 2*i表示第i对的夫人,2*i+1表示第i对的丈夫.一共就有2*n个人了.

        然后对于每个人来说,把他分成两个节点,如果该人在做左边就mark[i*2],如果该人坐右边就mark[i*2+1].

        我令新娘直接坐左边即第0个人mark[0]=true,新郎直接坐右边即第1个人mark[1*2+1]=true.

        然后对于每对夫妻,因为他们不能在同一边,所以第i对夫妻中a= 2*i表示妻子,b=2*i+1表丈夫. 有这样的关系:

a在左边,那么b就在右边,a*2->b*2+1

a在右边,那么b就在左边,a*2+1->b*2

b在左边,那么a就在右边,b*2->a*2+1

b在右边,那么a就在左边,b*2+1->a*2

        然后对于每对有奸情的人a与b,因为它们不能同时在新娘对面(右边),所以:

a*2+1->b*2

b*2+1->a*2

        注意首先我们定了新娘(0号)在左边,新郎(第1号人)一定在右边,所以我们要先加上:

0*2+1->0*2  和 1*2->1*2+1 

这样就保证了新娘和新郎在固定的那边不动.

AC代码:

#include<cstdio>
#include<cstring>
#include<vector>
#include<algorithm>
using namespace std;
const int maxn = 1000*2+100;
struct TwoSAT
{
    int n;
    vector<int> G[maxn*2];
    int S[maxn*2],c;
    bool mark[maxn*2];
 
    bool dfs(int x)
    {
        if(mark[x^1]) return false;
        if(mark[x]) return true;
        mark[x]=true;
        S[c++]=x;
        for(int i=0;i<G[x].size();i++)
            if(!dfs(G[x][i])) return false;
        return true;
    }
 
    void init(int n)
    {
        this->n=n;
        for(int i=0;i<2*n;i++) G[i].clear();
        memset(mark,0,sizeof(mark));
    }
 
    void add_clause(int x,int xval,int y,int yval)
    {
        x=x*2+xval;
        y=y*2+yval;
        G[x].push_back(y);
    }
 
    bool solve()
    {
        for(int i=0;i<2*n;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;  //注意细节,这里写成了return true;
            }
        }
        return true;
    }
}TS;
int main()
{
    int n,m;
    while(scanf("%d%d",&n,&m)==2)
    {
        if(n==0&&m==0) break;
        TS.init(n*2);
        TS.add_clause(0,1,0,0);//新娘放左
        TS.add_clause(1,0,1,1);//新郎放右
        for(int i=1;i<n;i++)
        {
            int a=i*2;  //妻子
            int b=i*2+1;//丈夫
            TS.add_clause(a,0,b,1);
            TS.add_clause(a,1,b,0);
            TS.add_clause(b,0,a,1);
            TS.add_clause(b,1,a,0);
        }
 
        for(int i=0;i<m;i++)
        {
            int a,b;
            char s1[10],s2[10];
            scanf("%d%s%d%s",&a,s1,&b,s2);
            if(s1[0]=='w') a=a*2;
            else a=a*2+1;
            if(s2[0]=='w') b=b*2;
            else b=b*2+1;
            TS.add_clause(a,1,b,0);
            TS.add_clause(b,1,a,0);
        }
        if(!TS.solve()) printf("bad luck\n");
        else
        {
            for(int i=2;i<2*n;i+=2)
            {
                if(TS.mark[i*2])
                    printf("%dw ",i/2);
                if(TS.mark[(i+1)*2])
                    printf("%dh ",i/2);
            }
            printf("\n");
        }
    }
    return 0;
}

题意:n 个人选举 m 条民意结果,编码方式如下:

若 i 与 j 至少一人当选,我会高兴    +i +j
若 i 与 j 至少一人不当选,我会高兴    -i -j
若 i 当选或 j 不当选,我会高兴    +i -j
若 i 不当选 或 j 当选,我会高兴    -i +j
现在要根据给出的 m 条民意结果,判断是否符合存在相应的选举结果,输出 1 或 0

思路:

对于 n 个参选人,每个人都有两种状态,当选或不当选,根据民意编码结果可知,+ 代表当选,- 代表不当选

设对于参选人 i、j,0 代表不当选,1 代表当选,则有:

i 与 j 至少一人当选:or(i,1,j,1)
i 与 j 至少一人不当选:or(i,0,j,0)
i 当选或 j 不当选:or(i,1,j,0)
i 不当选或 j 当选:or(i,0,j,1)
根据当选与不当选的编码方式可以看出,每条民意都是或的关系,因此直接根据 m 条民意结果进行建图即可,要注意的是,候选人是从 1 开始的,因此处理是要将编号由 1~n 改为由 0~n-1
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
const int maxn = 1000+10;
struct TwoSAT
{
    int n;
    vector<int> G[maxn*2];
    int S[maxn*2],c;
    bool mark[maxn*2];
 
    bool dfs(int x)
    {
        if(mark[x^1]) return false;
        if(mark[x]) return true;
        mark[x]= true;
        S[c++]=x;
 
        for(int i=0;i<G[x].size();i++)
            if(!dfs(G[x][i])) return false;
        return true;
    }
 
    void init(int n)
    {
        this->n=n;
        for(int i=0;i<n*2;i++) G[i].clear();
        memset(mark,0,sizeof(mark));
    }
 
    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<2*n;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;
            }
        }
        return true;
    }
}TS;
int main()
{
    int n,m;
    while(scanf("%d%d",&n,&m)==2)
    {
        TS.init(n);
        for(int i=0;i<m;i++)
        {
            int a,b;
            scanf("%d%d",&a,&b);
            TS.add_clause(abs(a)-1,a<0?1:0,abs(b)-1,b<0?1:0); //记得abs(a)-1
        }
        printf("%d\n",TS.solve()?1:0);
    }
    return 0;
}
 

2-SAT问题

        现有一个由N个布尔值组成的序列A,给出一些限制关系,比如A[x]AND A[y]=0、A[x] OR A[y] OR A[z]=1等,要确定A[0..N-1]的值,使得其满足所有限制关系。这个称为SAT问题,特别的,若每种限制关系中最多只对两个元素进行限制,则称为2-SAT问题。

        由于在2-SAT问题中,最多只对两个元素进行限制,所以可能的限制关系共有11种:

        A[x]

        NOT A[x]   

        A[x] AND A[y]

        A[x] AND NOT A[y]

        A[x] OR A[y]

        A[x] OR NOT A[y]

        NOT (A[x] AND A[y])

        NOT (A[x] OR A[y])

        A[x] XOR A[y]

        NOT (A[x] XOR A[y])

        A[x] XOR NOT A[y]

        进一步,A[x] ANDA[y]相当于(A[x]) AND (A[y])(也就是可以拆分成A[x]与A[y]两个限制关系),NOT(A[x]OR A[y])相当于NOT A[x] AND NOT A[y](也就是可以拆分成NOT A[x]与NOT A[y]两个限制关系)。因此,可能的限制关系最多只有9种。

        在实际问题中,2-SAT问题在大多数时候表现成以下形式:有N对物品,每对物品中必须选取一个,也只能选取一个,并且它们之间存在某些限制关系(如某两个物品不能都选,某两个物品不能都不选,某两个物品必须且只能选一个,某个物品必选)等,这时,可以将每对物品当成一个布尔值(选取第一个物品相当于0,选取第二个相当于1),如果所有的限制关系最多只对两个物品进行限制,则它们都可以转化成9种基本限制关系,从而转化为2-SAT模型。

(引自:http://www.cnblogs.com/kuangbin/archive/2012/10/05/2712429.html)

         在程序实现中,我们把初始的n个物品变成2n个节点,然后从0开始编号到2*n-1号。其中原始第i个物品对应节点i*2和i*2+1。如果我们mark[i*2]节点,那么表示我们i节点设为假,如果我们mark[i*2+1]节点,那么我们i节点设为真。同一个节点只能mark一种结果(即对于原始i来说,我们只能mark[i*2]或mark[i*2+1]其中之一)。

         然后加入存在i假或j假的论述,我们就引一条图中从2*i+1到2*j的边,再引一条2*j+1到2*i的边,表示如果i是真的,那么j肯定是假的(否则之前的结论不成立)。且如果j是真的,那么i肯定是假的(否则之前的结论也不成立)。

         如果存在i为真的论述,那么我们直接mark[i*2+1]即可。

         最终判断整个问题是否有解,就是做多次dfs来设置每个节点可能的值(真或假),看看是否所有可能取值情况都会冲突。如果不冲突,那么有解。(这里并不需要暴力枚举所有的可能,具体请看刘汝佳<<训练指南>>P323)

         注意:下面解题的过程中,比如我们要设定i为假,那么我们不是mark[i*2]=true,而是添加一条i*2+1->i*2的边,即只要i设为真了,那么就会使得导出矛盾。因为每个节点只有两种选择,所以上面添加边的思路更直观。反而每次都去转换成一条或语句更不直观。我基本上所有的题解都是以添加边的角度来做的。

         下面给出2-SAT的模板代码:


#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
const int maxn=10000+10;
struct TwoSAT
{
    int n;//原始图的节点数(未翻倍)
    vector<int> G[maxn*2];//G[i]==j表示如果mark[i]=true,那么mark[j]也要=true
    bool mark[maxn*2];//标记
    int S[maxn*2],c;//S和c用来记录一次dfs遍历的所有节点编号
 
    void init(int n)
    {
        this->n=n;
        for(int i=0;i<2*n;i++) G[i].clear();
        memset(mark,0,sizeof(mark));
    }
 
    //加入(x,xval)或(y,yval)条件
    //xval=0表示假,yval=1表示真
    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);
    }
 
    //从x执行dfs遍历,途径的所有点都标记
    //如果不能标记,那么返回false
    bool dfs(int x)
    {
        if(mark[x^1]) return false;//这两句的位置不能调换
        if(mark[x]) return true;
        mark[x]=true;
        S[c++]=x;
        for(int i=0;i<G[x].size();i++)
            if(!dfs(G[x][i])) return false;
        return true;
    }
 
    //判断当前2-SAT问题是否有解
    bool solve()
    {
        for(int i=0;i<2*n;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;
            }
        }
        return true;
    }
};
 

模板

#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
const int maxn=10000+10;
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]) return true;
        if(mark[x^1]) return false;
        mark[x]=true;
        S[c++]=x;
        for(int i=0;i<G[x].size();i++)
            if(!dfs(G[x][i])) return false;
        return true;
    }
 
    void init(int n)
    {
        this->n=n;
        for(int i=0;i<2*n;i++) G[i].clear();
        memset(mark,0,sizeof(mark));
    }
 
    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<2*n;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;
            }
        }
        return true;
    }
};
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值