P4782 【模板】2-SAT 问题

CSDN博客

原题链接

前置知识:强连通分量模板

简要题意:

给出若干条件,每个条件形如 “\(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;
}

参考资料:

posted @ 2020-04-18 16:18  bifanwen  阅读(203)  评论(0编辑  收藏  举报