2-SAT问题浅显入门指北
写在前面
这只菜菜的黄鸭最近自学了
另外,
问题的大概形式(定义)
我们把
综合来看,大概 就是给定一串布尔变量,每个变量只可以为真
例题也可以看这里:P4782 【模板】2-SAT 问题
我们举个例子来看一下这个问题:(这个是
Tops里,LCJ老师正在为一个模板写代码,写的代码要符合所有同学的码风要求。假设课堂有四位学生:wht,sx,hjl,wjx,他们的要求如下:
wht:我要求代码满足下列条件之一:
- 要加
ios::sync_with_stdio(0);
- 要开 O2
- 大括号换行
sx:我要求代码满足下列条件之一:
- 要加
ios::sync_with_stdio(0);
- 不开 O2
- 大括号不换行
hjl:我要求代码满足下列条件之一:
- 不加
ios::sync_with_stdio(0);
- 不开 O2
- 大括号不换行
wjx:我要求代码满足下列条件之一:
- 不加
ios::sync_with_stdio(0);
- 开 O2
- 大括号换行
我们不妨把“加 ios::sync_with_stdio(0)
”定义为
则四位同学的要求如下:
同学名称 | 条件 |
条件 |
条件 |
---|---|---|---|
wht | |||
sx | |||
hjl | |||
wjx |
满足的布尔方程即为
而LCJ老师所要做的就是为
这就是
问题的解法
朴素思想(暴力)
那这怎么赋值呢?
我们首先来考虑最简单的方法:暴力
枚举每一个变量的每一种可能,如果符合要求LCJ老师就会按这种方案写
很显然,对于每个变量只有两种可能的话,时间复杂度为
如果我们就这样写,那妥妥会
那怎么优化呢?
……
没错,对于 常规
但是,我们说了,对于 常规 只可以暴力,但是
针对 的特殊解法
因为此类问题涉及变量之间的关系,考虑用图论来解决
首先假设有
我们考虑建这样一个图:对于每一个布尔变量
接下来我们考虑如何建边:
首先我们考虑一个关系:
那这说明
与 中 必然至少 有一个是 所以我们由
可以推出 , 可以推出 那可不可以由
推出 呢? 不行,因为
的话 必然为 ,与 无关
不可推出 同理 所以我们就可以由
连向 ,由 连向 ![]()
要注意只有关系确切才可以建边
通过上述推理,我们总结出一般的建边规则:
若满足
也可以总结为 若其中一个变量不成立,则另一个变量必须成立
附:若

图建好了,那接下来我们首先应该判断有没有解。
首先我们考虑 无解:对于一个
因为既要
也就是说,如果
而判断强连通分量的算法常用
例如 ,此问题就无解
否则就是有解。对于有解的情况,题目一般会叫你输出一组合法的解
那怎么对每一个
我们继续来看一个例子(画图技术很差,轻喷)

我们推理一下:
因为由
因为由
如果再看不懂请看下面详细(口胡)解释:
反证法:
如果我们把
,会发生什么呢? 由图知若
,则 ,但又因 可以推出 ,与 矛盾,所以 证毕。
由此我们可以推出一个推论:在有解的情况下,一个布尔变量的两种取值是具有 前后推导关系 的,也就是一个值间接影响了另一个取值。
而我们所要赋值的那个值,并不会产生矛盾
在Tarjan缩点的拓扑上,表现为我们要在两种取值中选择拓扑序较大的那个值(因为它后遍历到,不会产生矛盾)
所以我们接下来要三部曲:缩点、拓扑、染色(bushi
只不过我们在
所以就转变为求拓扑序较小的那个值了
其实以我的理解是拓扑序越大的点在一棵树上是越靠近叶节点的,然后越靠近叶节点的那些节点在 Tarjan 的时候是越早被缩点的,所以拓扑序越大的点其所在强联通分量编号越小,那么我们只要取两个取值中强联通分量编号较小的所对应的值就可以了(这是保证不会错的,因为有时候两个值取哪个都行)
的代码实现
哇哈哈哈哈哈,代码来喽
本小节将分部分讨论,以 这道题 为例(请先仔细读题,不要当
输入
cin>>n>>m;//输入变量数与约束关系数量 for(int i=1;i<=m;i++) { int x,xv,y,yv; cin>>x>>xv>>y>>yv;//输入对应约束条件 //接下来按2.2特殊解法的建边规则进行建边,x表示第x个变量,x+n表示第x个变量的相反值 if(!xv&&!yv) gra[x].push_back(y+n),gra[y].push_back(x+n); else if(xv&&!yv) gra[x+n].push_back(y+n),gra[y].push_back(x); else if(!xv&&yv) gra[y+n].push_back(x+n),gra[x].push_back(y); else if(xv&&yv) gra[x+n].push_back(y),gra[y+n].push_back(x); }
当然,这是笔者的写法。由于笔者太菜,分分钟被大佬吊打, 便发现了另一种使用 位运算 建边的方法,代码如下:
cin>>n>>m; for(int i=1;i<=m;i++) { int x,xv,y,yv; cin>>x>>xv>>y>>yv; gra[x+n*(xv&1)].push_back(y+n*(yv^1)); gra[y+n*(yv&1)].push_back(x+n*(xv^1)); }
这里用分类讨论的方法解释一下这样建边正确的原因:(其实不难,自己手推也可以)
综上,四种情况全部考虑完,所以该做法成立
Tarjan求强连通分量
这个板子不用多讲了吧(bushi
int dfn[2000009],low[2000009],col[2000009],cnt,colc; int st[2000009],top; bool vis[2000009];//数组空间一定要开够 inline void tarjan(int u)//Tanjan板子,不用多讲了,一模一样 { dfn[u]=low[u]=++cnt,st[++top]=u,vis[u]=1; for(int i=0;i<gra[u].size();i++) { int v=gra[u][i]; 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]) { colc++; while(st[top]!=u) col[st[top]]=colc,vis[st[top--]]=0; col[st[top]]=colc,vis[st[top--]]=0; } return; } int main() { ios::sync_with_stdio(0); cin.tie(0);cout.tie(0);//玄学读入优化 //do something... //以下是Tarjan板子,不会的自行百度 for(int i=1;i<=n*2;i++) if(!dfn[i]) tarjan(i); //do something... return 0; }
此部分还算比较简单,记住板子就可以
判定无解与有解的方案输出
无解的判定(见上文):如果一个变量的两个值都在同一个强连通分量内,那此题无解
除无解以外都是有解(这个不用多说了吧)
关于有解的输出也请见上问,懒得搬运
int main() { ios::sync_with_stdio(0); cin.tie(0);cout.tie(0); //do something for(int i=1;i<=n;i++) if(col[i]==col[i+n]) { cout<<"IMPOSSIBLE";//如果一个变量的两个值都在同一个强连通分量内,那此题无解 return 0; } cout<<"POSSIBLE"<<endl;//先输出有解 for(int i=1;i<=n;i++) if(col[i]>col[i+n]) cout<<"1 ";//详见2.2,如果不是Tarjan求强连通分量要改成小于号 else cout<<"0 "; return 0; }
这样,一道
附上
高清无注释代码:
//#pragma GCC optimize(3) //#pragma GCC optimize(2) #include<bits/stdc++.h> using namespace std; int n,m; vector<int>gra[2000009]; int dfn[2000009],low[2000009],col[2000009],cnt,colc; int st[2000009],top; bool vis[2000009]; inline void tarjan(int u) { dfn[u]=low[u]=++cnt,st[++top]=u,vis[u]=1; for(int i=0;i<gra[u].size();i++) { int v=gra[u][i]; 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]) { colc++; while(st[top]!=u) col[st[top]]=colc,vis[st[top--]]=0; col[st[top]]=colc,vis[st[top--]]=0; } return; } int main() { ios::sync_with_stdio(0); cin.tie(0);cout.tie(0); cin>>n>>m; for(int i=1;i<=m;i++) { int x,xv,y,yv; cin>>x>>xv>>y>>yv; if(!xv&&!yv) gra[x].push_back(y+n),gra[y].push_back(x+n); else if(xv&&!yv) gra[x+n].push_back(y+n),gra[y].push_back(x); else if(!xv&&yv) gra[y+n].push_back(x+n),gra[x].push_back(y); else if(xv&&yv) gra[x+n].push_back(y),gra[y+n].push_back(x); } for(int i=1;i<=n*2;i++) if(!dfn[i]) tarjan(i); for(int i=1;i<=n;i++) if(col[i]==col[i+n]) { cout<<"IMPOSSIBLE"; return 0; } cout<<"POSSIBLE"<<endl; for(int i=1;i<=n;i++) if(col[i]>col[i+n]) cout<<"1 "; else cout<<"0 "; return 0; }
总结
此题重点就在建边与输出方案上,要理解边与边、点与点之间的练习,加以推理就可以写出了
预祝各位同学:
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!