• 图论--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种:

    1.         A[x]
    2.         NOT A[x]   
    3.         A[x] AND A[y]
    4.         A[x]  AND NOT A[y]
    5.         A[x] OR A[y]
    6.         A[x] OR NOT A[y]
    7.         NOT (A[x] AND A[y])
    8.         NOT (A[x] OR A[y])
    9.         A[x] XOR A[y]
    10.         NOT (A[x] XOR A[y])
    11.         A[x] XOR NOT A[y]

    进一步,3可以用1表示:A[x],A[y],NOT(A[x] XOR A[y]) 相当于 NOT A[x] AND  NOT A[y]。狄摩根定律。只剩下了9中基本关系类型。

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

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


    2-SAT模型建立:

    1.我们利用一条有向边<i,j>,来表示选i的情况下,一定要选j;

    2.用i表示某个点是true,那么i'表示某个点是false

    3.因为限制的两两之间的关系,所以我们可以通过逻辑关系来建边:

              1)如果给出A和B的限制关系,A和B必须一起选,(A and B)||(!A and !B )==true 那么选A必须选B,建边<i,j>和<j,i>还有<i',j'>和<j',i'>

              2)如果给出A和B的限制关系,选A不能选B,那么(A && !B)||(!A && B )==true,建边<i,j'>和<j,i'>

              3)如果必须选A,那么A==true,建边<i',i>

              4)如果A一定不能选,那么!A==true.建边<i,i'>

    这么建图之后,会出现一个有向图,这个有向图会导致一个连通环,导致某个点一旦选取,那么这条链上的所有点都要被选中。如果我们找到一个强连通分量,那么这个强连通分量当中的点,如果选取必须全部选取,不选取的话一定是全部不选取,所以只要满足这个有向图中连通的点不会导致i和i'同时被选取,如果不存在矛盾,那么当前问题就是有解的。但是往往在求解过程中,我们要求的解会要求一些性质,所以提供以下几种解决方案。

    用离散的的知识解释的话就是下面这位大佬的讲解(别人发给我的)

    首先,把「2」和「SAT」拆开。SAT 是 Satisfiability 的缩写,意为可满足性。即一串布尔变量,每个变量只能为真或假。要求对这些变量进行赋值,满足布尔方程。

    举个例子:教练正在讲授一个算法,代码要给教室中的多位同学阅读,代码的码风要满足所有学生。假设教室当中有三位学生:Anguei、Anfangen、Zachary_260325。现在他们每人有如下要求:

    • Anguei: 我要求代码当中满足下列条件之一:
      1. 不写 using namespace std; ( ¬a)
      2. 使用读入优化 (b)
      3. 大括号不换行 ( ¬c)
    • Anfangen: 我要求代码当中满足下条件之一:
      1. 写 using namespace std; (a)
      2. 使用读入优化 (b)
      3. 大括号不换行 (
eg ¬c
    • Zachary_260325:我要求代码当中满足下条件之一:
      1. 不写 using namespace std; (
eg ¬a
      2. 使用 scanf (
eg ¬b
      3. 大括号换行 (c)

    我们不妨把三种要求设为 a,b,ca,b,c,变量前加  eg¬ 表示「不」,即「假」。上述条件翻译成布尔方程即:(
eg avee bvee
eg c) wedge (avee bvee
eg c) wedge (
eg avee
eg bvee c)(¬a∨b∨¬c)∧(a∨b∨¬c)∧(¬a∨¬b∨c)。其中,vee∨表示或,wedge∧表示与。 

    现在要做的是,为 ABC 三个变量赋值,满足三位学生的要求。

    Q: 这可怎么赋值啊?暴力?

    A: 对,这是 SAT 问题,已被证明为 NP 完全 的,只能暴力。

    Q: 那么 2-SAT 是什么呢?

    A: 2-SAT,即每位同学 只有两个条件(比如三位同学都对大括号是否换行不做要求,这就少了一个条件)不过,仍要使所有同学得到满足。于是,以上布尔方程当中的 c,
eg cc,¬c 没了,变成了这个样子:(
eg avee b) wedge (avee b) wedge (
eg avee
eg b)

    公式杀招:


     


     

    怎么求解 2-SAT 问题?

    使用强连通分量。 对于每个变量 xx,我们建立两个点:x ,¬x 分别表示变量 xx 取 true 和取 false。所以,图的节点个数是两倍的变量个数在存储方式上,可以给第 ii 个变量标号为 ii,其对应的反值标号为 i + ni+n。对于每个同学的要求  (a∨b),转换为  ¬a→b∧¬b→a。对于这个式子,可以理解为:「若 aa 假则 bb 必真,若 bb 假则 aa 必真」然后按照箭头的方向建有向边就好了。综上,我们这样对上面的方程建图:

    原式 建图
    
eg avee b  a→b∧¬b→¬a
    a vee b  ¬a→b∧¬b→a
    
eg avee
eg bspace space  a→¬b∧b→¬a

    于是我们得到了这么一张图:

    built

    可以看到,¬a 与 b 在同一强连通分量内,a与 ¬b 在同一强连通分量内。同一强连通分量内的变量值一定是相等的。也就是说,如果 x 与 ¬x 在同一强连通分量内部,一定无解。反之,就一定有解了。

    但是,对于一组布尔方程,可能会有多组解同时成立。要怎样判断给每个布尔变量赋的值是否恰好构成一组解呢?

    这个很简单,只需要 当 xx 所在的强连通分量的拓扑序在  eg x¬x 所在的强连通分量的拓扑序之后取 xx 为真 就可以了。在使用 Tarjan 算法缩点找强连通分量的过程中,已经为每组强连通分量标记好顺序了——不过是反着的拓扑序。所以一定要写成 color[x] < color[-x] 。

    代码实现:

    //暴力DFS,求字典序最小的解,也是求字典序唯一的方法
    #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 <queue>
    #include <vector>
    #include <stack>
    #include <algorithm>
    #define MAXN 2000+10
    #define MAXM 400000
    #define INF 1000000
    using namespace std;
    vector<int> G[MAXN];
    int low[MAXN], dfn[MAXN];
    int dfs_clock;
    int sccno[MAXN], scc_cnt;
    stack<int> S;
    bool Instack[MAXN];
    int N, M;
    void init()
    {
    	for(int i = 0; i < 2*N; i++) G[i].clear();
    }
    void getMap()
    {
    	int a, b, c;
    	char op[10];
    	while(M--)
    	{
    		scanf("%d%d%d%s", &a, &b, &c, op);
    		switch(op[0])
    		{
    			case 'A': 
    			if(c == 1)//a,b取1 
    			{
    				G[a + N].push_back(a);
    				G[b + N].push_back(b);
    			}
    			else//a,b至少一个不为真 
    			{
    				G[a].push_back(b + N);
    				G[b].push_back(a + N);
    			}
    			break;
    			case 'O':
    			if(c == 1)//a,b最少有一个为真 
    			{
    				G[b + N].push_back(a);
    				G[a + N].push_back(b);
    			}
    			else//a,b都为假 
    			{
    				G[a].push_back(a + N);
    				G[b].push_back(b + N);
    			}
    			break;
    			case 'X':
    			if(c == 1)//a b 不同值 
    			{
    				G[a + N].push_back(b);
    				G[a].push_back(b + N);
    				G[b].push_back(a + N);
    				G[b + N].push_back(a);
    			}
    			else//a b 同真同假 
    			{
    				G[a].push_back(b);
    				G[b].push_back(a);
    				G[a + N].push_back(b + N);
    				G[b + N].push_back(a + N);
    			}
    		}
    	}
    }
    void tarjan(int u, int fa)
    {
    	int v;
    	low[u] = dfn[u] = ++dfs_clock;
    	S.push(u);
    	Instack[u] = true;
    	for(int i = 0; i < G[u].size(); i++)
    	{
    		v = G[u][i];
    		if(!dfn[v])
    		{
    			tarjan(v, u);
    			low[u] = min(low[u], low[v]);
    		}
    		else if(Instack[v])
    		low[u] = min(low[u], dfn[v]);
    	}
    	if(low[u] == dfn[u])
    	{
    		scc_cnt++;
    		for(;;)
    		{
    			v = S.top(); S.pop();
    			Instack[v] = false;
    			sccno[v] = scc_cnt;
    			if(v == u) break; 
    		}
    	}
    }
    void find_cut(int l, int r)
    {
    	memset(low, 0, sizeof(low));
    	memset(dfn, 0, sizeof(dfn));
    	memset(sccno, 0, sizeof(sccno));
    	memset(Instack, false, sizeof(Instack));
    	dfs_clock = scc_cnt = 0;
    	for(int i = l; i <= r; i++)
    	if(!dfn[i]) tarjan(i, -1);
    }
    void solve()
    {
    	for(int i = 0; i < N; i++)
    	{
    		if(sccno[i] == sccno[i + N])
    		{
    			printf("NO
    ");
    			return ;
    		}
    	}
    	printf("YES
    ");
    }
    int main()
    {
    	while(scanf("%d%d", &N, &M) != EOF)
    	{
    		init();
    		getMap();
    		find_cut(0, 2*N-1);
    		solve();
    	}
    	return 0;
    }
     
     
     
  • 相关阅读:
    .NET 使用EF执行存储过程你知道几种?
    SQLserver 如何优雅的行转列
    SQLserver 如何获取近1月、近3个月、近6月数据
    三汇自动挂断问题:SIP兼容性,ACK检测,忽略ACK开启。
    几个flutter 开源项目测试
    Android versions for all users globally
    Using Flutter 2 on M1 MacOS Apple Silicon
    Educational Codeforces Round 111
    Wannafly挑战赛1
    摆烂记录
  • 原文地址:https://www.cnblogs.com/lunatic-talent/p/12798600.html
Copyright © 2020-2023  润新知