2-SAT

如果限制条件只是“两者相同”或“两者不同”,问是否有合法方案,那么我们直接用扩展域并查集即可 (O(nlogn)) 维护(常数极小,配上按秩合并复杂度还会降低)。甚至我们还可以查询从第几个限制条件开始就没有合法方案了。但是如果限制条件为单向的:若 1 选,则 2 必不选;若 2 不选,则 1 不一定要选。那么我们就只能用2-SAT了。

前置技能:tarjan

图的建立

首先定义:(X) 表示选择 (X)(X') 表示不选 (X)(X_1 -> X_2) 表示选择 (X_1) 就必须选 (X_2)

我们发现,每一个限制都存在一个逆限制,我们称之为逆否命题,如:(A->B) 的逆否命题为 (B'->A')(A->B') 的逆否命题为 (B->A')。这是符合逻辑的。并且我们发现,这是对称的(类似中心对称?反正不像轴对称)。命题与逆否命题的对称性在解决2-SAT问题方面有着重要作用。特别地,(A -> A') 的逆否命题是 (A -> A')(就是它本身)。

我们将所有限制及其逆否命题用有向边来表示出来。

判定是否有解

我们可以认为,对于任意的 (x)(x'),如果都不在同一个 (SCC) 中,那么就一定存在一种解。

由于命题与逆否命题的对称性,如果 (x) 能到 (x'),那么 (x') 就一定能到 (x),使得他们在同一 SCC 里面。

找到解的一种方案

一种简单粗暴容易理解的方法是:先 tarjan 缩点,判断是否有解。如果有解,那么我们在缩点后的 DAG 上跑拓扑序(当然是反图的拓扑)。由于对称性,如果一个点 (x) 所在的 SCC 为 1,那么其对称点的 SCC 为 0。可以发现,SCC 实际上也是对称的。因此如果一个点未标记,则将其标记成 (0),将其对称点标记成 (1)。 否则不管。最后 标记为 0 的点为 “真”,标记为 1 的点为“假”

另一种改进方法是:因为 tarjan 本质是 dfs,其搞出的 SCC 的编号实际上正是拓扑序。因此我们可以根据 SCC 的编号来决定点为 真 还是 假。


好吧,我已经看不懂上面的东西了,再搞另一种思路吧。

就是tarjan缩点后,对于 (blong[i][0],blong[i][1]) ,我们显然要选取拓扑序较大的那一个,因为如果拓扑序较大,那么它就比较靠后,影响的范围更小,更容易构造出方案。

然后tarjan出来的编号其实是拓扑序的逆序,因此我们应该选择编号较小的那一个。

我们规定:(istrue[cur] = col[cur] > col[opp[cur]]),其中 (opp[cur] = cur + n)

证明还是根据对称性。

另一种实现方法

如果不小心在考场上忘记了 tarjan 算法怎么写,或者忘记了方案到底怎么找,还有一种比较暴力的 DFS 做法。不过容易证明这个做法的复杂度也是 (O(n)) 的。

随便找一个 (x)(x') 都没有遍历过的点 (x),沿着它遍历一遍,将遍历过的点都打上 vis 标记,并加入栈中。如果某个时候不合法了,那就退出,把栈中的点的标记都清空,再沿着 (x') 遍历一遍。如果还是冲突,就无解。否则,(x)vis 标记就说明选 (x)(x')vis 标记就说明选 (x')

代码:

bool vis[N];
int stk[N], stop;
bool dfs(int cur) {
	if (vis[oth[cur]])	return false;
	if (vis[cur])	return true;
	vis[cur] = true; stk[++stop] = cur;
	for (int i = head[cur]; i; i = e[i].nxt) {
		int to = e[i].to;
		if (!dfs(to))	return false;
	}
	return true;
}
inline bool work() {
	memset(vis, 0, sizeof(vis));
	for (int i = 1; i <= n + n; ++i)	if (!vis[i] && !vis[oth[i]]) {
		stop = 0;
		if (!dfs(i)) {
			while (stop)	vis[i] = false, --stop;
			if (!dfs(oth[i]))	return false;
		}
	}
	return true;
}

例题

P4782 【模板】2-SAT 问题

(a || b == true) 转化为 (a' -> b)(b' -> a)

吐槽:强制转化是真的慢,去掉(int)istrue[i]后效率高了一倍

当作模板题,贴个板子吧:

for (register int i = 1; i <= n; ++i)
	bl[i][0] = i, opp[i] = i + n, bl[i][1] = i + n, opp[i + n] = i;
register int aa, bb, cc, dd;
for (register int i = 1; i <= m; ++i) {
	read(aa); read(bb); read(cc); read(dd);
	addedge(opp[bl[aa][bb]], bl[cc][dd]);
	addedge(opp[bl[cc][dd]], bl[aa][bb]);
}
for (register int i = 1; i <= (n << 1); ++i)
	if (!dfn[i])	tarjan(i);
for (register int i = 1; i <= n; ++i) {
	if (col[bl[i][0]] == col[bl[i][1]]) {
		puts("IMPOSSIBLE");
		return 0;
	}
	istrue[i] = col[bl[i][0]] > col[bl[i][1]];
}
puts("POSSIBLE");
for (register int i = 1; i <= n; ++i) {
	printf("%d ", istrue[i]);
}
puts("");

P3209 [HNOI2010]平面图判定

给一个环和一堆边,问是否可能是平面图。

n <= 200, m <= 10000

平面图的边数最多是 (3 * n - 6)

因此 (n)(m) 规模相同,支持 (n^2) 做法。

当然,除去连边操作,剩下的可以 (O(m)) 完成。即将边在环外记作 (1),在环内记作 (0)。然后跑 2-SAT 即可。

P3825 [NOI2017]游戏

题意:

n个地图,有三种赛车 A, B, C,地图分 x, a, b, c 四种场地,其中 a, b, c 分别 不适合 A, B, C 车。每种场地只能使用 A, B, C其中的一种赛车。

现有 m 个限制,为:如果 p 场地使用 P 车,那么 q 场地必须使用 Q 车。

询问是否有解。如果有,输出任意一种方案。

(n <= 5e4, m <= 1e5, d = sum[x] <= 8)

题解:

建立 4n 个点,即将每个场地拆成两个点,然后再给每个点设置个“对称点”。对于 a, b, c 场地以及 m 个限制,使用 2-SAT 模型很容易搞定。但是不好搞的是 x 场地。

发现 x 的数量及其小。那么我们可以枚举 x 场地到底用什么车。但是这样做是 (O((n + m) * 3^d))) 的,会超时,并且由于其它场地是“不符合”,而这些场地偏偏是“必须”,实现起来不是很容易。因此我们直接枚举 x 不适合 哪种车,就是说适合其它两种车。这样只用枚举 a, b 场地即可满足所有情况。复杂度: (O((n + m) * 2^d))

然后就是代码能力+代码能力了。

习题

注意:

开大数组!!!!

附:

tarjan求强连通分量

void tarjan(int cur) {
    dfn[cur] = low[cur] = ++dcnt;
    stk[++stop] = cur; vis[cur] = true;
    for (register int i = head[cur]; i; i = e[i].nxt) {
        int to = e[i].to;
        if (!dfn[to]) {
            tarjan(to);
            low[cur] = min(low[cur], low[to]);
        } else if (vis[to]) {
            low[cur] = min(low[cur], low[to]);
        }
    }
    if (low[cur] == dfn[cur]) {
        int tmp; ctot++;
        do {
            tmp = stk[stop--];
            col[tmp] = ctot;
            vis[tmp] = false;
        } while (tmp != cur);
    }
}

2-SAT 基本操作

for (register int i = 1; i <= n; ++i)
	bl[i][0] = i, bl[i][1] = i + n;
//...(addedge)
for (register int i = 1; i <= (n << 1); ++i)
	if (!dfn[i])	tarjan(i);
for (register int i = 1; i <= n; ++i) {
	if (col[bl[i][0]] == col[bl[i][1]]) {
		puts("IMPOSSIBLE");
		return 0;
	}
	istrue[i] = col[bl[i][0]] > col[bl[i][1]];
}
原文地址:https://www.cnblogs.com/JiaZP/p/13731072.html