2-SAT 问题
和网络流一样,很多问题都可以建模为 2-SAT
问题,只要会求解 2-SAT
,就可以解决许多问题。
洛谷博客
github
vercel
k-SAT
首先引入 k-SAT
问题。
k-SAT
问题说的就是有 n 个布尔值 ai 和 m 组约束条件,其中每组约束都是 a 中 k 个值进行逻辑运算后等于一个给定值 (0 或 1) 的形式,求一组满足约束的 ai(或者输出无解)。
比如
⎩⎪⎨⎪⎧(a0anda1)or(nota2)anda3=1(a0xor(nota1))anda2=0(a0ora1)xor(nota2)=1
是一个 3-SAT
问题,它的一个解为
⎩⎪⎪⎪⎨⎪⎪⎪⎧a0=0a1=0a2=0a3=1
可以证明 k-SAT
在 k≥3 时是没有多项式解法的,但是 k≤2 时有。
2-SAT
2-SAT
问题是 k-SAT
问题在 k=2 时的特例。
每一个约束条件都可以表示为 「ai 为 x 或 aj 为 y」的形式。和网络流一样,很多问题都可以建模为 2-SAT
问题,只要会求解 2-SAT
,就可以解决许多问题。建模当然比原算法难哈哈哈
接下来是重点:如何求解 2-SAT
问题。
1. 图论建膜
先建立膜型 stO CYB Orz
我们把每个变量拆成两个点,一个表示该变量为真,另一个表示该变量为假。我们约定 ⟨i,x⟩ 为 ai=x 代表的节点,如 ⟨10,1⟩ 表示 a10=1 代表的节点。因此一个节点代表了一个命题。既然这样,我们就可以自然地定义节点 ⟨i,x⟩ 的非节点 ¬⟨i,x⟩ 定义为 ⟨i,¬x⟩,表示 ai=x 也就是 ai=¬x。
边表示蕴含关系,即 ⟨i,x⟩ 到 ⟨j,y⟩ 间有一条有向边表示如果 ai=x 则 aj=y。注意连边时要把这条边的逆否边连起来,⟨i,x⟩→⟨j,y⟩ 则 ⟨j,¬y⟩→⟨i,¬x⟩。为了方便,我们把 ⟨i,x⟩ 能走到 ⟨j,y⟩ 记为 ⟨i,x⟩⇒⟨j,y⟩,走不到记为 ⟨i,x⟩⇒⟨j,y⟩。
则原问题可化为:在约束对应的图上,选择 n 个点,使表示某变量为真与为假的点有且只有一个被选择,每个点若被选择,它能走到的节点也被选择。
2. 求解
怎么解呢?当然要请出我们的 爆搜 Tarjan 大佬。
首先我们发现,如果一个节点与它的非节点互相连通,那么问题肯定无解,因为如果 ai=1⇒ai=0 且 ai=0⇒ai=1,那么就形成了逻辑矛盾。
然后我们发现,如果 ⟨i,x⟩⇒⟨i,¬x⟩ 且 ⟨i,¬x⟩⇒⟨i,x⟩ 则 ai=¬x。(其实也就是反证法)
如果 ⟨i,x⟩ 与 ⟨i,¬x⟩ 没有直接或间接关系,则 ai 可以随便取值。
因此我们考虑将图用 tarjan 把强连通分量缩点,得到一张 DAG
。如果一个点和它的非节点在同一个强连通分量中,则问题无解,否则,ai 的取值就是 ⟨i,1⟩ 和 ⟨i,0⟩ 中缩点后拓扑序较小的节点,因为拓扑序较大的节点不可能走回拓扑序小的节点。
于是,我们得到了一组可行解。
code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
| #include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #include<vector>
using namespace std;
namespace sat{
const int maxn=1000006; vector<int> e[maxn*2];
int n; inline int rho(int i,bool x){ return i*2-x; } void add(int i,bool a,int j,bool b){ e[rho(i,a)].push_back(rho(j,b)); e[rho(j,b^1)].push_back(rho(i,a^1)); }
int dfn[maxn*2],low[maxn*2],vis[maxn*2],cnt=0; int scc[maxn*2],sccn=0,stk[maxn*2],tp=0; void dfs(int u){ low[u]=dfn[u]=++cnt; stk[++tp]=u; vis[u]=1; for(int i=e[u].size()-1;i>=0;i--){ int v=e[u][i]; if(!dfn[v]){ dfs(v); low[u]=min(low[u],low[v]); }else if(vis[v]){ low[u]=min(low[u],dfn[v]); } } if(low[u]==dfn[u]){ sccn++; while(stk[tp]!=u){ vis[stk[tp]]=0; scc[stk[tp]]=sccn; tp--; } vis[stk[tp]]=0; scc[stk[tp]]=sccn; tp--; } } bool x[maxn]; bool solve(){ for(int i=1;i<=2*n;i++){ if(!dfn[i]){ dfs(i); } } for(int i=1;i<=n;i++){ if(scc[rho(i,0)]==scc[rho(i,1)]){ return 0; }else{ x[i]=scc[rho(i,0)]>scc[rho(i,1)]; } } return 1; }
bool& get(int i){ return x[i]; }
}
using sat::e; using sat::rho;
int n,m;
int main(){
scanf("%d%d",&n,&m); sat::n=n; for(int i=1;i<=m;i++){ int x,a,y,b; scanf("%d%d%d%d",&x,&a,&y,&b); sat::add(x,a^1,y,b); }
if(sat::solve()){ puts("POSSIBLE"); for(int i=1;i<=n;i++){ printf("%d%c",sat::x[i]," \n"[i==n]); } }else{ puts("IMPOSSIBLE"); }
return 0;
}
|
例题
模板题
双倍经验
NOI2017 游戏
第三题有一些难度,我做这道题的时候一开始建模建错了,结果不能保证每个地图都能分配到赛车……