• 只有2-SAT不是NPC呢——2-SAT学习笔记


    [{Huge ext{2-SAT学习笔记}} ]


    什么是2-SAT?

    我们举一个简单的例子:

    机房里有三位大佬小s,小l,小m和一个蒟蒻tqr,他们刷题时有不同的要求(因为蒟蒻什么题都不会做,故不列举):

    大佬/要求 小s 小l 小m
    要求1 不包含数论知识(¬a) 包含数论知识(a) 不包含数论知识(¬a)
    要求2 包含几何知识(b) 包含几何知识(b) 不包含几何知识(¬b)
    要求3 不包含图论知识(¬c) 不包含图论知识(¬c) 包含图论知识(c)

    在此之前,复习一下前置数学知识:

    对于一个命题:如果a,那么b

    其逆命题为:如果b,那么a

    其否命题为:如果¬a,那么¬b

    其逆否命题为:如果¬b,那么¬a

    ∧表示与,∨表示或,¬表示非

    原命题与逆否命题等值。逆命题与否命题等值

    原命题与逆命题以及逆否命题与否命题之间没有关系

    我们继续上面的情景,如果我们要出一道题,使这道题能同时满足上面三位dalao的喜好,该如何做?

    经过数学分析,满足的题目一定符合以下条件(¬a∨b∨¬c)∧(a∨b∨¬c)∧(¬a∨¬b∨c)

    因此我们要做的就是给每个变量赋值,使上式值为true

    是的,这就是SAT问题,但需要注意的是,上面的情况中,每个同学对题目都有三个限制,因此是3-SAT问题

    可证明的,3(及以上)-SAT问题都是NPC问题(即不可使用算法解决,唯一的方法是暴力枚举)

    因此,2-SAT问题是算法能解决的极限,那么我们将限制改一改:

    经过一段时间的学习后,除了tqr,其他人都完全掌握了图论知识……

    一波操作过后,限制条件变成了(¬a∨b)∧(a∨b)∧(¬a∨¬b),这就是我们要学习的2-SAT了

    那我们就开始吧……

    如何解决2-SAT?

    首先,我们需要将几个互相有限制的点赋值,那就把它们丢到图里面!

    建立两个点,分别是a和¬a(储存时,可以存到编号分别为 (i)(i+maxn) 的节点上,就像并查集对称点一样)

    那么每个点之间的关系是什么?

    (a∨b)可以理解为若a为真,则b为假,反之b为真

    于是我们可以建出图形:

    ¬a→ b∧¬b →a

    由图形可以发现,a与¬b,以及¬a与b都在同一个强连通分量里,于是我们可以得出结论:

    2-SAT问题同一个强连通分量中的所有元素值相同

    很显然的,如果a与¬a(或者x与¬x,x代表任意条件)在同一个强连通分量里,即他们的值相等,那么出现了矛盾,可判断问题无解(就像1=-1一样,矛盾的等式)

    解决一个实际问题

    题目描述

    有n个布尔变量 (x_1)~(x_n),另有 (m) 个需要满足的条件,每个条件的形式都是 (x_i)true/false(x_j)true/false。你的mubiao给每个变量赋值使得所有条件得到满足。

    输入格式

    第一行两个整数(n)(m),意义如题面所述

    接下来(m)行每行(4)个整数(i,a,j,b),表示(x_i)(a)(x_j)(b)((a,b∈{0,1}))

    输出格式

    如无解,输出(IMPOSSIBLE);否则输出(POSSIBLE),

    下一行(n)个整数(x_1)~(x_n)((x_i∈{0,1})),表示构造出的解。

    很显然地,我们需要按照上述所说的来建边

    read(n);read(m);
    for(register int i=1;i<=m;++i)
    {
    	read(a),read(va),read(b),read(vb);
    	add_edge(a+!va*n,b+vb*n);
    	add_edge(b+!vb*n,a+va*n);
    }
    

    然后tarjan找强连通分量(color数组是拓扑序)

    void tarjan(int u)
    {
    	low[u]=dfn[u]=++idx;
    	sta.push(u);
    	vis[u]=1;
    	for(register int i=head[u];i;i=edge[i].next)
    	{
    		int v=edge[i].to;
    		if(!dfn[v])
    		{
    			tarjan(v);
    			low[u]=min(low[u],low[v]);
    		}
    		else if(vis[v]) low[u]=min(low[u],dfn[v]);
    	}
    	if(dfn[u]==low[u])
    	{
    		for(tms++;!sta.empty();)
    		{
    			int x=sta.top();
    			sta.pop();
    			vis[x]=0;
    			color[x]=tms;
    			if(x==u) break;
    		}
    	}
    }
    
    for(register int i=1;i<=2*n;++i) if(!dfn[i]) tarjan(i);
    

    如果强连通分量 (x_0), (x_1) 在缩点后 (DAG) 的底图中连通,那么我们选择的一定是拓扑序较大的

    (Tarjan) 求强连通分量得到的强连通分量编号是遵循拓扑序逆序的,所以如果 (x_0) 的编号更小,输出 (0),否则输出 (1)

    for(register int i=1;i<=n;++i)
        if(color[i]==color[i+n])
            {puts("IMPOSSIBLE");return 0;}	puts("POSSIBLE");
    for(register int i=1;i<=n;++i)
    	printf(color[i]>color[i+n]?"1 ":"0 ");
    

    这道题就很轻松地解决了,下面是完整代码

    #include<bits/stdc++.h>
    using namespace std;
    
    int n,m,a,va,b,vb;
    
    struct Edge
    {
    	int next,to;
    }edge[2000005];
    int cnt=0,head[2000005];
    
    inline void add_edge(int from,int to)
    {
    	edge[++cnt].next=head[from];
    	edge[cnt].to=to;
    	head[from]=cnt;
    }
    
    int low[2000005],dfn[2000005],color[2000005],tms,idx;
    bool vis[2000005];
    stack<int> sta;
    void tarjan(int u)
    {
    	low[u]=dfn[u]=++idx;
    	sta.push(u);
    	vis[u]=1;
    	for(register int i=head[u];i;i=edge[i].next)
    	{
    		int v=edge[i].to;
    		if(!dfn[v])
    		{
    			tarjan(v);
    			low[u]=min(low[u],low[v]);
    		}
    		else if(vis[v]) low[u]=min(low[u],dfn[v]);
    	}
    	if(dfn[u]==low[u])
    	{
    		for(tms++;!sta.empty();)
    		{
    			int x=sta.top();
    			sta.pop();
    			vis[x]=0;
    			color[x]=tms;
    			if(x==u) break;
    		}
    	}
    }
    
    template<class T>inline void read(T &res)
    {
    	T flag=1;char c;
    	while((c=getchar())<'0'||c>'9')if(c=='-')flag=-1;res=c-'0';
    	while((c=getchar())>='0'&&c<='9')res=(res<<1)+(res<<3)+c-'0';res*=flag;
    }
    
    int main()
    {
    	read(n);read(m);
    	for(register int i=1;i<=m;++i)
    	{
    		read(a),read(va),read(b),read(vb);
    		add_edge(a+!va*n,b+vb*n);
    		add_edge(b+!vb*n,a+va*n);
    	}
    	for(register int i=1;i<=2*n;++i) if(!dfn[i]) tarjan(i);
    	for(register int i=1;i<=n;++i)
    		if(color[i]==color[i+n]) {puts("IMPOSSIBLE");return 0;}
    	puts("POSSIBLE");
    	for(register int i=1;i<=n;++i)
    		printf(color[i]>color[i+n]?"1 ":"0 ");
    	return 0;
    }
    

    有什么不懂的可以发qq问我,没了

  • 相关阅读:
    Git-Runoob:Git 查看提交历史
    Git-Runoob:Git 分支管理
    Git-Runoob:Git 基本操作
    Git-Runoob:Git 创建仓库
    weblogic11g 安装集群 —— win2003 系统、单台主机
    关于条件宏的易错点
    关于端口号你知道多少!
    POJ 2114 Boatherds【Tree,点分治】
    Android应用开发学习笔记之Intent
    对于接收到的GPS信息详解
  • 原文地址:https://www.cnblogs.com/tqr06/p/11191174.html
Copyright © 2020-2023  润新知