blog里没有特别详细的讲解,所以献上dalao的讲解
不过2_SAT的模型还是很典型的:
形式有点想网络流
给出若干集合,集合大小一般为2
一般每个集合中必选一个元素,非此即彼
元素之间有一些奇怪的限制
我们通过这些限制条件建图,永远记住:沿着图上的边前进代表must choose
一个限制条件可能需要多条边去表示,所以记录端点和边的空间一定要计算清楚
2_SAT的精髓还是在于如何建图(简单总结一下,详细证明移步dalao的blog)
模型一:两者(A,B)不能同时取
那么选择了
A
A
就只能选择,选择了
B
B
就只能选择
连边
A→B′,B→A′
A
→
B
′
,
B
→
A
′
模型二:两者(A,B)不能同时不取
那么选择了
A′
A
′
就只能选择
B
B
,选择了就只能选择
A
A
连边
模型三:两者(A,B)要么都取,要么都不取
那么选择了
A
A
,就只能选择,选择了
B
B
就只能选择,选择了
A′
A
′
就只能选择
B′
B
′
,选择了
B′
B
′
就只能选择
A′
A
′
连边
A→B,B→A,A′→B′,B′→A′
A
→
B
,
B
→
A
,
A
′
→
B
′
,
B
′
→
A
′
模型四:两者(A,A’)必取A
连边 A′→A A ′ → A
至于2_SAT问题的求解,主要有一些几种
最简单的,判断是否有解,
我们只需要一遍tarjan缩点,保证同一集合中的元素位于不同SCC中即可证明有解
不过需要注意的是,tarjan缩点只能起到判定的作用
而且blog后面介绍的方法也只针对原图(全图)有解时才能够构造方案
对于一些最大/最小值问题,我们可以将2_SAT与二分结合
tip
tarjan一定要维护出入栈的状态
int st[N],tot=0,n;
int S[N],top;
int dfn[N],low[N],clo,cnt,belong[N];
bool in[N];
void tarjan(int now) {
dfn[now]=low[now]=++clo;
S[++top]=now;
in[now]=1; //入栈
for (int i=st[now];i;i=way[i].nxt) {
int y=way[i].y;
if (!dfn[y]) {
tarjan(y);
low[now]=min(low[now],low[y]);
}
else if (in[y]) //判断是否在栈内
low[now]=min(low[now],dfn[y]);
}
if (low[now]==dfn[now]) {
int x=-1;
cnt++;
while (x!=now) {
x=S[top--];
belong[x]=cnt;
in[x]=0; //出栈
}
}
}
void init() {
memset(belong,0,sizeof(belong));
memset(dfn,0,sizeof(dfn));
memset(low,0,sizeof(low));
memset(in,0,sizeof(in));
clo=0; cnt=0; top=0;
}
bool solve() {
for (int i=1;i<=2*n;i++) //图中i与i+n是对立结点
if (!dfn[i])
tarjan(i);
for (int i=1;i<=n;i++)
if (belong[i]==belong[i+n])
return 0;
return 1;
}
continue
已经确定当前图有解,如果需要输出任意一组解怎么破
注意我们再tarjan的时候求出乐意一个
belong
b
e
l
o
n
g
数组
记录每个结点属于哪个SCC
我们把tarjan缩点后形成的DAG重新建出来,不过需要注意:DAG中的边是反向的
在DAG上拓扑排序(染色)
遇到一个未染色的结点,我们就将其标记为选中
同时ta的对立结点标记为未选中,在DAG上沿边传递未选中标记
tip
因为我们需要在对立结点上传递标记
而这个对立结点是SCC层面上的,所以我们需要记录一下每个SCC的对立SCC
struct node{
int y,nxt;
}e[M];
int ste[N],tet=0;
int du[N],S[N],col[N],opp[N];
void _add(int u,int w) {
tet++;e[tet].y=w;e[tet].nxt=ste[u];ste[u]=tet;
du[w]++;
}
void dfs(int now) { //传递未选中标记
if (col[now]) return;
col[now]=-1;
for (int i=ste[now];i;i=e[i].nxt)
dfs(e[i].y);
}
void TOP() { //拓扑
int top=0;
for (int i=1;i<=cnt;i++) //枚举SCC
if (!du[i])
S[++top]=i;
while (top) {
int now=S[top--];
if (col[now]) continue;
col[now]=1;
dfs(opp[now]);
for (int i=ste[now];i;i=e[i].nxt)
{
du[e[i].y]--;
if (!du[e[i].y]) S[++top]=e[i].y;
}
}
}
void rebuild() {
for (int i=1;i<=2*n;i++)
for (int j=st[i];j;j=way[j].nxt)
if (belong[i]!=belong[way[j].y])
_add(belong[way[i].y],belong[i]); //反向连边建DAG
for (int i=1;i<=n;i++) { //记录对立SCC
opp[belong[i]]=belong[i+n];
opp[belong[i+n]]=belong[i];
}
}
//solve(); 判定有解
还有一部分题要求输出最小字典序解
这种情况,我们只能暴力构造解
我们对图进行染色,
首先找到编号最小的未染色的点,标记为选中,沿着边传递选中标记
如果反馈回来的信息表示染色失败(出现冲突)
我们把此次的染色信息清空,选中ta的对立结点
如果两方面染色都失败了,说明原2_SAT无解
tip
然而的时候一定要记录此次的染色结点
任何一个后继结点出现了矛盾,则此染色方式不可行
int S[N],top=0;
int col[N];
int fan(int now) {
if (now<=n) return n+now;
else return now-n;
}
bool print(int now) {
if (col[now]) return 1;
if (col[opp(now)]) return 0;
col[now]=1;
S[++top]=now; //记录染色结点
for (int i=st[now];i;i=way[i].nxt)
if (!print[way[i].y]) return 0; //任何一个后继结点出现了矛盾,返回
return 1;
}
bool solve() {
memset(col,0,sizeof(col));
for (int i=1;i<=n;i++)
if (!col[i]&&!col[i+n]) //此集合没有染色
{
top=0;
if (!print(i)) {
while (top) col[S[top--]]=0;
if (!print(i+n)) return 0;
}
}
return 1;
}