图论 —— 2-SAT 问题

【问题概述】

2-SAT问题是这样的:有n个布尔变量xi,另有m个需要满足的条件,每个条件的形式都是“xi为真/假或者xj为真/假“

SAT 是适定性(Satisfiability)问题的简称,一般形式为:k-适定性问题,简称:k-SAT。

当 k>2 时,k-SAT 是 NP 完全的,因此一般讨论的是 k=2 的情况,即:2-SAT 问题。

关于 2-SAT 问题,简单的来说就是给出 n 个集合,每个集合中有两个元素,然后从每个集合中选出一个元素,一共选 n 个两两不矛盾的元素, 显然可能有多种选择方案,一般题目只需要求任意输出一种即可。

简单来说,就是给出一个由 n 个布尔值组成的序列 A,再给出 m 个限制关系,每个条件的形式都是 Xi 为真/假 或 Xj 为真/假(比如: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]:A[x]
  • NOT A[x]:非 A[x]
  • A[x] AND A[y]:A[x] 与 A[y]
  • A[x] OR A[y]:A[x] 或 A[y]
  • A[x] XOR A[y]:A[x] 异或 A[y]
  • A[x] AND NOT A[y]:A[x] 与 非A[y]
  • A[x] OR NOT A[y]:A[x] 或 非A[y]
  • A[x] XOR NOT A[y]:A[x] 异或 非A[y]
  • NOT (A[x] AND A[y]):非(A[x] 与 A[y])
  • NOT (A[x] OR A[y]):非(A[x] 或 A[y])
  • NOT (A[x] XOR A[y]):非(A[x] 异或 A[y])

进一步来说,A[x] AND A[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 问题模型。

【问题解决】

以 A、B、C 三个人出去玩为例:如果 B 去,则 A 也去;B、C 只去一个;C一定去

假设用 ' 表示某个点不选,那么根据上面的叙述有:

  • B->A
  • B'->C,C'->B
  • C'->C

对于这种问题,我们可以用 Tarjan 来解决

首先将给出的 n 个变量的每个变量 i 拆分成两个结点 i、i+n,分别表示第 i 个变量为真、第 i+n 个变量为假,这样就将 n 个点拆分成了 n 对。

然后根据题设所给出的 m 个关系,建立一个 2*n 的有向图,即根据题设在图中构造有向边:若图中存在边 <i,j>,则表示若选了 i 则必须选 j,可以发现,上述的 9 种限制关系中,前 2 种一元关系可通过连一条边来实现,后 7 种二元关系可通过连两条边来实现。

对于一元关系:对于 Xi 为真,可以通过连边 <i+n,i> 实现,Xi 为假,可以通过连边 <i,i+n> 来实现

对于二元关系:以 “Xi 为假或者 Xj 为假“ 为例,其可以表述为:若 Xi 为真,则 Xj 为假,若 Xj 为真,则 Xi 为假,因此需要连两条边:<i,j+n>、<j,i+n>

最后根据建出的图跑一遍 Tarjan 来求出所有的强连通分量,然后根据拓扑序来决定每个点是选还是不选,由于 Tarjan 给出的是反拓扑序,因此只要找强连通分量编号小的即可。

关于拓扑序:以 x->x' 为例,如果选了 x 那么 x' 也要选,但这条边的意思是 x 这个点一定不选,于是就构成矛盾,只能选择拓扑序大的

这样在跑完 Tarjan 后,如果同一物品的两种状态在同一个边双连通分量中,则说明无解,若不在同一边双连通分量中,则可以输出选择方案,即每个点选择缩成的超级点中编号最小的那个

【模版】

1.Tarjan

以以下问题为例:

有 n 个布尔变量 m 个需满足的条件,每个条件的形式都是 " xi 为 true/false 或 xj 为 true/false ",2-SAT 问题的目标是给每个变量赋值,使得所有条件满足,现在给出 m 个条件,形式为:i a j b,表示 xi 为 a 或 xj 为 b,其中 a、b∈{0,1}

若无解,则输出 IMPOSSIBLE,若有解,输出 POSSIBLE 与构造的 n 个变量的解

struct Edge{
    int to,next;
}edge[N*2];
int head[N],tot;
int n,m;
int dfn[N],low[N];
bool vis[N];//标记数组
int scc[N];//记录结点i属于哪个强连通分量
int block_cnt;//时间戳
int sig;//记录强连通分量个数
stack<int> S;
void init(){
    tot=0;
    sig=0;
    block_cnt=0;
    memset(head,-1,sizeof(head));
    memset(vis,0,sizeof(vis));
    memset(dfn,0,sizeof(dfn));
    memset(low,0,sizeof(low));
    memset(scc,0,sizeof(scc));
}
void addEdge(int from,int to){
    edge[++tot].to=to;
    edge[tot].next=head[from];
    head[from]=tot;
}
void Tarjan(int x) {
    vis[x]=true;
    dfn[x]=low[x]=++block_cnt;//每找到一个新点,纪录当前节点的时间戳
    S.push(x);//当前结点入栈

    for(int i=head[x]; i!=-1; i=edge[i].next) { //遍历整个栈
        int y=edge[i].to;//当前结点的下一结点
        if(!dfn[y]) {
            Tarjan(y);
            low[x]=min(low[x],low[y]);
        }
        else if(vis[y])
            low[x]=min(low[x],dfn[y]);
    }

    if(dfn[x]==low[x]) { //满足强连通分量要求
        sig++;//记录强连通分量个数

        while(true) { //记录元素属于第几个强连通分量
            int temp=S.top();
            S.pop();
            vis[temp]=false;
            scc[temp]=sig;
            if(temp==x)
                break;
        }
    }
}
bool twoSAT(){
    for(int i=1;i<=2*n;i++)//找强连通分量
        if(!dfn[i])
            Tarjan(i);
    for(int i=1;i<=n;i++)
        if(scc[i]==scc[i+n])//条件a与!a属于同一连通分量,无解
            return false;
    return true;
}
int main() {
    init();

    scanf("%d%d",&n,&m);
    while(m--) {
        int x,y,xVal,yVal;
        scanf("%d%d%d%d",&x,&xVal,&y,&yVal);

        if(xVal==0&&yVal==0){//x为0或y为0
            addEdge(x+n,y);//x为0,y为1
            addEdge(y+n,x);//y为0,x为1
        }
        else if(xVal==0&&yVal==1){//x为0或y为1
            addEdge(x+n,y+n);//x为0,y为0
            addEdge(y,x);//y为1,x为1
        }
        else if(xVal==1&&yVal==0){//x为1或y为0
            addEdge(x,y);//x为1,y为1
            addEdge(y+n,x+n);//y为0,x为0
        }
        else if(xVal==1&&yVal==1){//x为1或y为1
            addEdge(x,y+n);//x为1,y为0
            addEdge(y,x+n);//y为1,x为0
        }
    }

    bool flag=twoSAT();
    if(!flag)
        printf("IMPOSSIBLE\n");
    else{
        printf("POSSIBLE\n");
        for(int i=1;i<=n;i++){
            if(scc[i]>scc[i+n])
                printf("1 ");
            else
                printf("0 ");
        }
        printf("\n");
    }

    return 0;
}

2.多次 dfs

将初始的 n 个物品变成 2n 个节点,然后从 0 开始编号到 2*n-1,其中原始第 i 个物品对应节点 i*2 和 i*2+1,如果标记 vis[i*2] 节点,那么表示 i 节点设为假,如果标记 vis[i*2+1] 节点,那么 i 节点设为真,同一个节点只能标记一种结果,即对于原始 i 来说,只能标记 vis[i*2] 或 vis[i*2+1] 其中之一

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

如果存在 i 为真的论述,那么直接标记 vis[i*2+1]

最终判断整个问题是否有解,就是做多次dfs来设置每个节点可能的值(真或假),看看是否所有可能取值情况都会冲突,如果不冲突,那么有解

vector<int> G[N*2];//G[i]==j表示如果vis[i]=true,那么vis[j]也要=true
bool vis[N*2];//标记数组
int Stack[N*2],tot;//用来记录一次dfs遍历的所有节点编号
void init(int n) {
    for(int i=0; i<2*n; i++)
        G[i].clear();
    memset(vis,0,sizeof(vis));
}
//加入(x,xval)或(y,yval)条件,xval=0表示假,yval=1表示真
void addEdge(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 dfs(int x) {//从x执行dfs遍历,途径的所有点都标记,如果不能标记,那么返回false
    if(vis[x^1])
        return false;
    if(vis[x])
        return true;
    vis[x]=true;
    Stack[tot++]=x;
    for(int i=0; i<G[x].size(); i++)
        if(!dfs(G[x][i]))
            return false;
    return true;
}
bool towSAT(int n) {//判断当前2-SAT问题是否有解
    for(int i=0; i<2*n; i+=2){
        if(!vis[i] && !vis[i+1]) {
            tot=0;
            if(!dfs(i)) {
                while(tot>0)
                    vis[Stack[--tot]]=false;
                if(!dfs(i+1))
                    return false;
            }
        }
    }
    return true;
}
int main(){
    int n,m;
    scanf("%d%d",&n,&m);
    init(n);
    while(m--){
        int a,b,aVal,bVal;
        scanf("%d%d%d%d",&a,&b,&aVal,&bVal);
        if(a==0&&b==0){
            addEdge(...);
            addEdge(...);
        }
        else if(a==0&&b==1){
            addEdge(...);
            addEdge(...);
        }
        else if(a==1&&b==0){
            addEdge(...);
            addEdge(...);
        }
        else if(a==1&&b==1){
            addEdge(...);
            addEdge(...);
        }
    }
    bool flag=twoSAT(n);
    if(flag)
        printf("YES\n");
    else
        printf("NO\n");

    return 0;
}

【例题】

  1. Let's go home(HDU-1824)(多次dfs)点击这里
  2. Wedding(POJ-3648)(多次dfs)点击这里
  3. Perfect Election(POJ-3905)(多次dfs)点击这里
  4. Katu Puzzle(POJ-3678)(多次dfs)点击这里
  5. Go Deeper(HDU-3715)(多次dfs+二分)点击这里
  6. Building roads(POJ-2749)(多次dfs+二分)点击这里
  7. Ikki's Story IV - Panda's Trick(POJ-3207)(多次dfs+范围判断)点击这里
  8. Priest John's Busiest Day(POJ-3683)(多次dfs+或关系的应用)点击这里
  9. Eliminate the Conflict(HDU-4115)(多次dfs+三元且关系)点击这里
  10. Get Luffy Out(POJ-2723)(多次dfs+且关系的枚举)点击这里
  11. Peaceful Commission(HDU-1814)(多次dfs+最小字典序)点击这里
  12. 2-SAT 问题(洛谷-P4782)(Tarjan)点击这里
  13. Party(HDU-3062)(Tarjan)点击这里
  14. 满汉全席(洛谷-P4171)(Tarjan)点击这里
  15. 处女座与宝藏(2019牛客寒假算法基础集训营 Day2-F)(Tarjan)点击这里
  • 12
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值