zoukankan      html  css  js  c++  java
  • 2-SAT问题简述

    前置知识

    强连通分量

    k-SAT问题

    k-SAT问题中的SAT意思就是(stability),也就是适应性问题。本意是给出n个变量,每一个变量有k个状态,并且也给出一些约束条件,要求你求出是否存在每一个变量的取值方案(状态分配方案)。

    很可惜,k-SAT(k>2)已经被证明是NP完全的问题了,也就是说我们无法用一些多项式时间复杂度的算法来解决这个问题,但是我们可以发现。当k<=2的时候,我们可以用一些多项式时间复杂度的算法来解决这个问题。

    2-SAT问题

    先来一道模板题:

    Luogu P4782

    下面是对2-sat问题的一个例子

    假如你是一个厨师,现在要给两个人做菜,每个人的口味都不同,而你至少要满足每个人的一个口味。这些人会提一些要求,比方说:

    A:

    • 我不喜欢吃辣椒(-a)
    • 我喜欢吃肥肉有(b)

    B:

    • 我不喜欢吃辣椒(-a)
    • 我不喜欢吃肥肉(-b)

    那么我们可以用这种形式来简化表示约束条件:

    A: -a or B: -a

    A: b or B -b

    对于每一个(x_i) or (x_j)的约束条件,我们可以将其变化成:

    (x_i)为假则(x_j)为真

    (x_i)为真则(x_j)为假

    就比如说 A:-a or B:-b 可以变成

    若 A:-a为假,则B: -b为真

    若 B:-b为假,则A:-a为真

    对上面的例子进行缩写,可以得到:

    若A为a,则B为-b

    若B为b,则A为-a
    (因为-a为假的话证明肯定就是a了,反之亦然,同时也适用于b的情况)

    于是,对于每一种约束条件的格式,我们都可以像上面这样分析,然后发现一些传递的关系:

    1

    我们把每一个变量抽象成图上的两个点,两个点分别代表了原来一个变量的两个不同的状态。也就是说变成a 和 -a (就是a真和a假)。

    我们可以针对每一种约束条件把相对应的关系传递用有向边连接起来,表示从A的某一个状态可以推测出B的值(或者是b的某一个状态可以推得a的状态),也就是说这会形成一些链,在链上的每一个变量的值都是确定了的。但是根据数据的不同,实际上会出现强连通分量。并且如果一个强连通分量中包括某一个和原来a取值相反的-a的取值,则代表其矛盾,也就是说无解

    我们这里使用tarjan求SCC(强连通分量)的算法来检测环的出现与否。

    那么当问题有解的时候,我们该如何显示每一个变量的值呢?难道我们还需要对原图进行拓扑+染色吗?实际上我们并不需要这么做。因为我们在tarjan的过程中实际上就已经相当于求了一遍拓扑排序了。可以这么想,因为最后我们赋值scc的时候是从栈内弹出赋值的,也就是说越靠近叶子的节点越先被赋值,总的来看这不就是拓扑序的逆序吗?我们可以直接对比a和-a的拓扑序,哪个小就表明哪个是在越靠经根节点处被选中的,那么我们就优先选择哪个比较小的节点。

    当然,因为这里我们存储的是拓扑的逆序,所以我们会优先选择哪个比较大的节点

    一些代码处理的细节

    虽然我们分析问题的时候经常会用a 或-a来表示某个变量的两种状态,但是数组的下标不能是负数。这里我们可以简单地表示-a为a+n,n就是点数。

    如果完全按照每一种xi和xj的取值来写代码的话,就会是这个样子的:

    for (int i=1;i<=m;i++) {
        int a,va,b,vb;
    	scanf("%d %d %d %d",&a,&va,&b,&vb);
        if (va && vb) { // a, b 都真,-a -> b, -b -> a
            gpe[a+n].push_back(b);
            gpe[b+n].push_back(a);
        } else if (!va && vb) { // a 假 b 真,a -> b, -b -> -a
            gpe[a].push_back(b);
            gpe[b+n].push_back(a+n);
        } else if (va && !vb) { // a 真 b 假,-a -> -b, b -> a
            gpe[a+n].push_back(b+n);
            gpe[b].push_back(a);
        } else if (!va && !vb) { // a, b 都假,a -> -b, b -> -a
            gpe[a].push_back(b+n);
            gpe[b].push_back(a+n);
        }
    }
    

    当然这么写是正确的,但是代码不可避免地有点冗长,这里我们可以使用位运算的写法可以缩短代码长度:

    for(int i=1;i<=m;i++){
    	int a,va,b,vb;
    	scanf("%d %d %d %d",&a,&va,&b,&vb);
        	gpe[a+n*(va&1)].push_back(b+n*(vb^1));
        	gpe[b+n*(vb&1)].push_back(a+n*(va^1));
    }
    

    模板题AC代码:

    //luogu p4782
    #include <bits/stdc++.h>
    using namespace std;
    const int maxn=2000050;
    struct edge{
    	int to;
    	edge(int to_){
    		to=to_;
    	}
    };
    vector<edge> gpe[maxn];
    int dfn[maxn],low[maxn],ins[maxn],scc[maxn],size[maxn],cnt=0,sccn=0;
    stack<int> s;
    void tarjan(int u){
    	dfn[u]=low[u]=++cnt;
    	s.push(u);
    	ins[u]=1;
    	for(int i=0;i<gpe[u].size();i++){
    		int v=gpe[u][i].to;
    		if(!dfn[v]){
    			tarjan(v);
    			low[u]=min(low[u],low[v]);
    		}else if(ins[v]){
    			low[u]=min(low[u],dfn[v]);
    		}
    	}
    	if(low[u]==dfn[u]){
    		ins[u]=0;
    		scc[u]=++sccn;
    		size[sccn]=1;
    		while(s.top()!=u){
    			scc[s.top()]=sccn;
    			ins[s.top()]=0;
    			size[sccn]+=1;//这里的size实际上是不需要的
    			s.pop();
    		}
    		s.pop();
    	}
    	return;
    }
    int n,m,oud[maxn];
    int main(void){
    	scanf("%d %d",&n,&m);
    	memset(low,0x3f,sizeof(low));
    	memset(ins,0,sizeof(ins));
    	for(int i=1;i<=m;i++){
    	      int a,va,b,vb;
    	      scanf("%d %d %d %d",&a,&va,&b,&vb);
        	      gpe[a+n*(va&1)].push_back(b+n*(vb^1));
        	      gpe[b+n*(vb&1)].push_back(a+n*(va^1));
    	}
    	for(int i=1;i<=n*2;i++){
    		if(!dfn[i]){
    			tarjan(i);
    		}
    	}
    	for(int i=1;i<=n;i++){
    		if(scc[i]==scc[i+n]){
    			printf("IMPOSSIBLE");
    			return 0;
    		}
    	}
    	printf("POSSIBLE
    ");
    	for(int i=1;i<=n;i++){
    		printf("%d ",scc[i]<scc[i+n]);
    	}
    	return 0;
    }
    

    个人的xbb

    其实这个问题感觉和差分约束有一点神似,因为都是根据关系来转换到图上解决的问题

    看到自己以前写的代码感觉还是太蠢了qaq,但是反正也懒得改,就直接贴上去罢了

  • 相关阅读:
    Linux-进程描述(1)—进程控制块
    C++中的继承(2)类的默认成员
    Linux系统date命令的参数及获取时间戳的方法
    new/new[]和delete/delete[]是如何分配空间以及释放空间的
    golang垃圾回收
    golang内存分配
    go中的关键字-reflect 反射
    go中的关键字-go(下)
    go中的关键字-go(上)
    go中的关键字-defer
  • 原文地址:https://www.cnblogs.com/jrdxy/p/12926305.html
Copyright © 2011-2022 走看看