P4782 【模板】2-SAT 问题
前置知识:强连通分量模板
简要题意:
给出若干条件,每个条件形如 “\(x_i\) 为真 或 \(x_j\) 为假”,求使得所有 \(x_i\) 赋值为 真或假 且满足每个条件。(\(x_i\) 为真 或 \(x_j\) 为假,只要满足一个就满足了整个)
不可避免的是,本人第一次想到的思路竟然和 \(\text{2-SAT}\) 算法背道而驰!
如果您不想看本人的思考过程,或者想直奔正解,可以直接去算法二看看。
算法一
是作者第一想法,口胡的算法,后来因为觉得不太对放弃了。
第一眼想到的是 二分图匹配.
毕竟很类似的一点,那么怎样建图?怎样二分图?都是问题。
想了想,对于每个 \(x_i , true_{x_i} , y_i , true_{y_i}\) ,建两条边,\(x_i \rightarrow y_i (true_{x_i}) , y_i \rightarrow x_i (true_{y_i})\). 并把所有 \(x_i\) 和 \(y_i\) 分开二分图。
可是,这样显然不对啊?因为 \(y_i \rightarrow x_i\) 在二分图中没有用啊,那么 \(true_{y_i}\) 就没用了,然而根本不是这样的。
很快我想到,可以建一条 \(x_i \rightarrow y_i (true_{x_i} , true_{y_i})\) 的边,表示 \(x_i \rightarrow y_i\) 可以是 \(true_{x_i}\) 也可以是 \(true_{y_i}\).
那么二分图的过程就是,把 “协商” 的过程改一改,分作两次协商:先取 \(true_{x_i}\) 进行下一步协商,再取 \(true_{y_i}\) 进行下一步协商,并将结果取 \(\max\) 返回。
但是,二分图究竟求出来什么呢?也就是,你求出这个最大匹配以后,它代表什么?
仔细想了想,可以代表一个最多能满足的条件数量,可以判断无解情况。
那么如果有解,如何确定这个解呢?只需要把最后的匹配结果输出即可。
但是这个匹配过程绝对不简单。因为这不是简单的一步回溯,也不是说只有 “选” “不选” 两种操作,而是 都满足,满足第一个,满足第二个,不满足 \(4\) 种情况,如果不满足也不一定无解,回溯情况可能还有满足情况。
当时想到这里心里冷了一下,\(\text{2-SAT}\) 总不会就是这样子的吧?那不就成了码力题了,那么多细节自己不一定处理的过来。
又想了一下,不对,既然二分图模板是绿题,那这题码力再大也只是细节罢了,怎么到紫题了呢?当时不太明白,有点愕然。
仔细一想,然后推翻了之前所有的想法。
首先,如果你这样建图的话,那么可能存在一个 \(x_i\) 对应多个 \(y_i\) 的情况,二分图模板中是 只要选一个就行了,而本题的要求是 全部满足,这肯定是不对的。
既然第一个思路死了,那就来看正解。
算法二
\(\text{SAT}\) 算法是一种用于解决 布尔方程 的一类算法,其本质在于图的建立。
\(\text{CNT-SAT}\) 算法,则是每个条件分为 \(\text{CNT}\) 个分支(比方说本题就是 \(\text{2-SAT}\) ) 的 布尔方程 的解决算法。
那么,我们根据 \(\text{CNT}\) 值的不同来讨论问题。
Case 1.
\(\text{CNT} =1\)
即 \(\text{1-SAT}\).(没有这个说法,为了让新手理解才这么说而已)
这时每个条件只有 \(1\) 种方案,直接哈希判断一波结束。
Case 2.
正宗 \(\text{SAT}\) 来了!
考虑 建图。
假设 \(m\) 个条件都形如 \(x_i = p , y_i = q\) 的形式,那么我们建图,如果 \(p\) 为真那么 \(x_i \rightarrow x_{i+n}\),否则 \(x_{i+n} \rightarrow x\). \(q\) 同理,那么这样的处理会让边数 \(\text{*}2\),用链式前向星的同学要小心啊~
建成图后,对原图跑一个强连通分量即可完成本题。
你可能不知所云了,那么举个例子吧:
\(3\) 个 \(\text{AK IOIers}\) 是好朋友,他们经常互相借鉴参考代码,却由于码风的不同很有矛盾。他们各自对 大括号是否换行(\(a\) 条件),多余大括号是否需要(\(b\) 条件),缩进是 \(4\) 格还是 \(2\) 格(这里认为 \(4\) 格是标准)(\(c\) 条件),是否需要格式化代码(即多余空格)(\(d\) 条件) 发表了自己的看法:
\(\text{1. wxq}\):
- 神仙发话:大括号不必换行。(\(\neg a\))
- 神仙发话:多余大括号当然需要!( \(\neg b\))
- 神仙发话:缩进是 \(2\) 格比较好点。。。(\(c\))
- 神仙发话:多余空格是码风更加清新!(\(d\))
\(\text{2. fzx}\):
- 佛家禅语:大括号不必换行~。 (\(\neg a\))
- 佛家禅语:去掉没用的大括号~。 (\(b\))
- 佛家禅语:缩进日常 \(2\) 格~。(\(c\))
- 佛家禅语:不太喜欢多余空格哦~。(\(\neg d\))
\(\text{3. gyx}\):
- 道破神机:大括号换行快乐!(\(a\))
- 道破神机:大括号没用就算了哦!(\(b\))
- 道破神机:缩进 \(2\) 格有利于女装吖!(\(c\))
- 道破神机:多余空格用来装 \(\text{*}\) 挺好的!(\(d\))
\(\text{wxq}\) 为所有 \(\text{AKIOI}\) 的神仙代言辩论,\(\text{fzx}\) 为所有 \(\text{AKIOI}\) 的佛祖代言辩论,\(\text{gyx}\) 为所有 \(\text{AKIOI}\) 的女装大佬辩论。
经过精彩无比而言冗长的辩论,仍然没有结果。因为,\(\text{fzx}\) 的佛风克制了 \(\text{gyx}\) 的女装气息,而这种气息恰好把 \(\text{wxq}\) 的神仙风格压制,\(\text{wxq}\) 的神仙风格却让 \(\text{fzx}\) 的佛法难以施展。 \(\text{3 years later} \cdots \cdots \cdots \cdots\)
他们觉得,不同一码风是不可以的,那样代码看得 不!舒!服! 但是他们降低了苛刻的要求,认为 自己的所有要求中,只要满足一个就是自己能接受的码风了。
他们找到你想问问:哪种码风可以适应所有人的要求呢?
显然机智的你发现,大括号不换行,有多余大括号,缩进 \(2\) 格,多余空格 是其中的一种方案。当然,程序中如何解决呢?
电脑如果不能形象地转换实际问题,那就和猴子没什么区别。——\(\text{bfw}\)
(开个玩笑而已)
首先你发现,他们都觉得 缩进 \(2\) 格 较好,即同时存在 \(\neg b\),那么这个条件答案有了分支:\(\text{(1).}\) 直接满足 \(\neg b\) ,其余条件任意。 \(\text{(2).}\) 直接满足 \(b\),然后递归分支其余条件。
那么你就得到答案了?(好笑)
这样子能做你还不早得图灵奖了? ——\(\text{lxl}\)
(嗯,那图领奖早被人抢了吧)
嗯是的,\(\text{SAT}\) 就是这样一类 \(\text{NP}\) 完全问题。
—— 那 \(\text{2 - SAT}\) 是啥?
—— 嗯就是把上面 码风的要求 改成两项即可!(当然 \(\text{3-SAT}\) 和 \(\text{2-SAT}\) 解决方法类似)
—— 那为什么我没听说过 \(\text{3-SAT}\)?
—— 因为,\(CNT \geq 3\) 的情况只能用暴力解决,它已经废弃啦!
假设 \(m\) 个条件都形如 \(x_i = p , y_i = q\) 的形式,那么我们建图,如果 \(p\) 为真那么 \(x_i \rightarrow x_{i+n}\),否则 \(x_{i+n} \rightarrow x\). \(q\) 同理,那么这样的处理会让边数 \(\text{*}2\),用链式前向星的同学要小心啊~
(怎么又说了一遍吖)所以,这样你会发现,一个两两可达的点集上,所有条件要么一起满足,要么一起不满足。
所以把原图 \(\text{Tarjan}\) 求出所有强连通分量,及其所在并查集编号。如果 \(f_i \not = f_{i+n}\) 就无解喽!(\(f\) 是并查集)
时间复杂度:\(O(n+m)\).
实际得分:\(100pts\).
#pragma GCC optimize(2)
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+1;
inline int read(){char ch=getchar();int f=1;while(ch<'0' || ch>'9') {if(ch=='-') f=-f; ch=getchar();}
int x=0;while(ch>='0' && ch<='9') x=(x<<3)+(x<<1)+ch-'0',ch=getchar();return x*f;}
bool h[N]; int cnt=0;
int times,dfn[N];
int low[N],n,m,f[N];
vector<int>G[N];
stack<int>s;
inline void dfs(int u) {
dfn[u]=low[u]=++times;
s.push(u); h[u]=1;
for(int i=0,t;i<G[u].size();i++) {
t=G[u][i];
if(!dfn[t]) {
dfs(t); low[u]=min(low[u],low[t]);
} else if(h[t]) low[u]=min(low[u],dfn[t]);
}
if(low[u]==dfn[u]) {
cnt++; int k;
do {
// a[cnt].push_back(s.top());
k=s.top();
h[k]=0; f[k]=cnt;
s.pop();
} while(k!=u) ;
}
}
int main(){
int n=read(),m=read(); while(m--) {
int a=read(),va=read(),b=read(),vb=read();
G[a+n*(va&1)].push_back(b+n*(vb^1));
G[b+n*(vb&1)].push_back(a+n*(va^1));
} for(int i=1;i<=(n<<1);i++)
if(!dfn[i]) dfs(i);
for(int i=1;i<=n;i++)
if(f[i]==f[i+n]) {
puts("IMPOSSIBLE");
return 0;
} puts("POSSIBLE");
for(int i=1;i<=n;i++) printf("%d ",f[i]<f[i+n]);
putchar('\n');
return 0;
}
参考资料: