图论--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]
进一步,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: 我要求代码当中满足下列条件之一:
- 不写
using namespace std;
( ¬a) - 使用读入优化 (b)
- 大括号不换行 ( ¬c)
- 不写
- Anfangen: 我要求代码当中满足下条件之一:
- 写
using namespace std;
(a) - 使用读入优化 (b)
- 大括号不换行 ()
- 写
- Zachary_260325:我要求代码当中满足下条件之一:
- 不写
using namespace std;
() - 使用
scanf
() - 大括号换行 (c)
- 不写
我们不妨把三种要求设为 a,b,ca,b,c,变量前加 \neg¬ 表示「不」,即「假」。上述条件翻译成布尔方程即:。其中,表示或,表示与。
现在要做的是,为 ABC 三个变量赋值,满足三位学生的要求。
Q: 这可怎么赋值啊?暴力?
A: 对,这是 SAT 问题,已被证明为 NP 完全 的,只能暴力。
Q: 那么 2-SAT 是什么呢?
A: 2-SAT,即每位同学 只有两个条件(比如三位同学都对大括号是否换行不做要求,这就少了一个条件)不过,仍要使所有同学得到满足。于是,以上布尔方程当中的 c,,¬c 没了,变成了这个样子:
公式杀招:
怎么求解 2-SAT 问题?
使用强连通分量。 对于每个变量 xx,我们建立两个点:x ,¬x 分别表示变量 xx 取 true
和取 false
。所以,图的节点个数是两倍的变量个数。在存储方式上,可以给第 ii 个变量标号为 ii,其对应的反值标号为 i + ni+n。对于每个同学的要求 (a∨b),转换为 ¬a→b∧¬b→a。对于这个式子,可以理解为:「若 aa 假则 bb 必真,若 bb 假则 aa 必真」然后按照箭头的方向建有向边就好了。综上,我们这样对上面的方程建图:
原式 | 建图 |
---|---|
a→b∧¬b→¬a | |
¬a→b∧¬b→a | |
a→¬b∧b→¬a |
于是我们得到了这么一张图:
可以看到,¬a 与 b 在同一强连通分量内,a与 ¬b 在同一强连通分量内。同一强连通分量内的变量值一定是相等的。也就是说,如果 x 与 ¬x 在同一强连通分量内部,一定无解。反之,就一定有解了。
但是,对于一组布尔方程,可能会有多组解同时成立。要怎样判断给每个布尔变量赋的值是否恰好构成一组解呢?
这个很简单,只需要 当 xx 所在的强连通分量的拓扑序在 \neg 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\n");
return ;
}
}
printf("YES\n");
}
int main()
{
while(scanf("%d%d", &N, &M) != EOF)
{
init();
getMap();
find_cut(0, 2*N-1);
solve();
}
return 0;
}
图论--2-SAT--详解的更多相关文章
- ACM(图论)——tarjan算法详解
---恢复内容开始--- tarjan算法介绍: 一种由Robert Tarjan提出的求解有向图强连通分量的线性时间的算法.通过变形,其亦可以求解无向图问题 桥: 割点: 连通分量: 适用问题: 求 ...
- 图论中DFS与BFS的区别、用法、详解…
DFS与BFS的区别.用法.详解? 写在最前的三点: 1.所谓图的遍历就是按照某种次序访问图的每一顶点一次仅且一次. 2.实现bfs和dfs都需要解决的一个问题就是如何存储图.一般有两种方法:邻接矩阵 ...
- 图论中DFS与BFS的区别、用法、详解?
DFS与BFS的区别.用法.详解? 写在最前的三点: 1.所谓图的遍历就是按照某种次序访问图的每一顶点一次仅且一次. 2.实现bfs和dfs都需要解决的一个问题就是如何存储图.一般有两种方法:邻接矩阵 ...
- quartz.net 时间表达式----- Cron表达式详解
序言 Cron表达式:就是用简单的xxoo符号按照一定的规则,就能把各种时间维度表达的淋漓尽致,无所不在其中,然后在用来做任务调度(定时服务)的quart.net中所认知执行,可想而知这是多么的天衣无 ...
- spring定时任务详解(@Scheduled注解)( 转 李秀才的博客 )
在springMVC里使用spring的定时任务非常的简单,如下: (一)在xml里加入task的命名空间 xmlns:task="http://www.springframework.or ...
- spring quartz 配置实现定时任务 详解
一. 编写定时任务JAVA类 比如: public class QuartzJob { public QuartzJob(){ System.out.println(" ...
- Quartz 入门详解
Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,它可以与J2EE与J2SE应用程序相结合也可以单独使用.Quartz可以用来创建简单或为运行十个,百个, ...
- cron表达式详解
@Scheduled(cron = "* * * * * *") cron表达式详解 1.cron表达式格式: {秒数} {分钟} {小时} {日期} {月份} {星期} {年份( ...
- HTTP 协议详解
相关文章:HTTP 协议之压缩 当今web程序的开发技术真是百家争鸣,ASP.NET, PHP, JSP,Perl, AJAX 等等. 无论Web技术在未来如何发展,理解Web程序之间通信的基本协议相 ...
- 【转】jqGrid 各种参数 详解
[原文]http://www.cnblogs.com/younggun/archive/2012/08/27/2657922.htmljqGrid 各种参数 详解 JQGrid JQGrid是一个 ...
随机推荐
- IDEA永久激活码获取
废话少说,直接上地址:http://idea.medeming.com/jet/ *小编还是建议,有家底的程序猿还是直接买正版的号,非正版的很多不便的地方~
- 算法:模拟退火(基于c++程序)
一 什么是模拟退火算法? 所谓退火,其实是金属冶炼的一个名词.比如加工一把刀,我们通常是把材料加工到很高的一个温度,加以锤炼.之后慢慢的将温度降下来,如果我们降温的控制比较好的话,那么金属里面的原子就 ...
- alg-链表中有环
typedef struct ListNode { int val; ListNode *next; ListNode(int x) : val(x), next(nullptr) {} }ListN ...
- Thinking in Java,Fourth Edition(Java 编程思想,第四版)学习笔记(三)之Everything Is an Object
---恢复内容开始--- Both C++ and Java are hybird languages. A hybird language allow multiple programming st ...
- C#中分布式事务的超时处理问题
事务是个很精妙的存在,我们在数据层.服务层.业务逻辑层等多处地方都会使用到. 在这里我只说下TransactionScope这个微软推荐使用的隐式事务.它是从Framework 2.0开始引入的一个事 ...
- 用Python爬取大众点评数据,推荐火锅店里最受欢迎的食品
前言 文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 作者:有趣的Python PS:如有需要Python学习资料的小伙伴可以加点 ...
- C# 基础知识系列- 11 委托和事件
0. 前言 事件和委托是C#中的高级特性,也是C#中很有意思的一部分.出现事件的地方,必然有委托出现:而委托则不一定会有事件出现.那为什么会出现这样的关系呢?这就需要从事件和委托的定义出发,了解其中的 ...
- HashMap之KeySet分析
本篇涵盖 1.HashMap并不是用keySet来存储key的原因及证明 2.keySet方法返回后的remove.add操作原理 一.方法作用 概括一下 1.keySet方法返回map中包含的键的集 ...
- 从hfctf学习JWT伪造
本文作者:Ch3ng easy_login 简单介绍一下什么是JWT Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519) ...
- tensorflow1.0 数据队列FIFOQueue的使用
import tensorflow as tf #模拟一下同步先处理数据,然后才能取数据训练 #tensorflow当中,运行操作有依赖性 #1.首先定义队列 Q = tf.FIFOQueue(3,t ...