图论 —— 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;
}
【例题】
- Let's go home(HDU-1824)(多次dfs):点击这里
- Wedding(POJ-3648)(多次dfs):点击这里
- Perfect Election(POJ-3905)(多次dfs):点击这里
- Katu Puzzle(POJ-3678)(多次dfs):点击这里
- Go Deeper(HDU-3715)(多次dfs+二分):点击这里
- Building roads(POJ-2749)(多次dfs+二分):点击这里
- Ikki's Story IV - Panda's Trick(POJ-3207)(多次dfs+范围判断):点击这里
- Priest John's Busiest Day(POJ-3683)(多次dfs+或关系的应用):点击这里
- Eliminate the Conflict(HDU-4115)(多次dfs+三元且关系):点击这里
- Get Luffy Out(POJ-2723)(多次dfs+且关系的枚举):点击这里
- Peaceful Commission(HDU-1814)(多次dfs+最小字典序):点击这里
- 2-SAT 问题(洛谷-P4782)(Tarjan):点击这里
- Party(HDU-3062)(Tarjan):点击这里
- 满汉全席(洛谷-P4171)(Tarjan):点击这里
- 处女座与宝藏(2019牛客寒假算法基础集训营 Day2-F)(Tarjan):点击这里
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!