图论 2-SAT 学习笔记

问题

\(n\) 个布尔变量 \(x_1,\ldots,x_n\),另有 \(m\) 个需要满足的条件,每个条件的形式都是「\(x_i=a\)\(x_j=b\)」(\(a,b\) 为布尔变量)。给每个变量赋值使得所有条件得到满足。

转化

根据《离散数学及其应用》我们知道“蕴含”的定义:\(p \to q\) 表示如果 \(p\),那么 \(q\)。我们考虑用“蕴含”来表示条件,于是条件「p 或 q」可表示为 \(\neg p \to q\)\(\neg q \to p\),其中 \(p,q\) 可分别代表命题 \(x_i=a\)\(x_j=b\)

有了上面的转化,我们不难联想到图论建模。把“蕴含”看成有向边,并把每个布尔变量拆成两个命题 \([x_i=a]\)\([x_i \ne a]\) 两个点,则得到了一个有向图。

求解

根据“蕴含”的定义可以发现在同一强连通分量里的命题的真值是一样的,所以如果 \([x_i=a]\)\([x_i \ne a]\) 在同一强连通分量里那么无解。否则,缩点后我们可以得到一个 DAG,考虑如何构造解。根据“蕴含”的性质我们可以贪心的让拓扑序更大的点为真,而这样一定可以构造出合法的解。而我们发现如果在缩点的时候给强连通分量依次编号,这个编号其实就是逆拓扑序,于是可以用这个直接求解,具体可参考代码。

模板题:P4782 【模板】2-SAT

代码
#include <bits/stdc++.h>

#define eb emplace_back

using namespace std;

const int maxn = 1e6 + 5, maxm = 2e6 + 5;

int n, m;
vector <int> e[maxm];
int tim, tot, dfn[maxm], low[maxm], scc[maxm];
bool vis[maxm];
stack <int> st;

void tarjan(int x) {
	dfn[x] = low[x] = ++tim;
	st.push(x), vis[x] = 1;
	for(int y : e[x]) {
		if(!dfn[y]) {
			tarjan(y);
			low[x] = min(low[x], low[y]);
		}
		else if(vis[y]) low[x] = min(low[x], dfn[y]);
	}
	if(low[x] == dfn[x]) {
		tot++;
		while(!st.empty()) {
			int t = st.top(); st.pop();
			vis[t] = 0, scc[t] = tot;
			if(t == x) break;
		}
	}
}

int main() {
	scanf("%d%d", &n, &m);
	int a, x, b, y;
	for(int i = 1; i <= m; i++) {
		scanf("%d%d%d%d", &a, &x, &b, &y);
		e[a + (!x) * n].eb(b + y * n);
		e[b + (!y) * n].eb(a + x * n);
	}
	for(int i = 1; i <= (n << 1); i++) if(!dfn[i]) tarjan(i);
	for(int i = 1; i <= n; i++) if(scc[i] == scc[i + n])
		{ puts("IMPOSSIBLE"); return 0; }
	puts("POSSIBLE");
	for(int i = 1; i <= n; i++) printf("%d ", (scc[i] >= scc[i + n]));
	return 0;
}

例题

P6378 [PA 2010] Riddle

\(a_u\) 表示 \(u\) 是否为关键点,则边 \(u-v\) 可转化 \(\neg a_u \to a_v\)\(\neg a_v \to a_u\),而一部分 \(S\) 则可转化为 \(\forall u \in s,u \to \neg v,v \in S \land v \ne u\)。但直接建边边数可能达到 \(n^2\) 所以考虑优化建图。朴素优化建图有线段树优化建图和前后缀优化建图两种,本题采用前后缀优化建图即可。

代码
#include <bits/stdc++.h>

#define eb emplace_back

using namespace std;

const int maxn = 1e6 + 5, maxm = 6e6 + 5, maxe = 6e7 + 5;

int n, m, k, w, s[maxn];
int tim, tot, dfn[maxm], low[maxm], scc[maxm];
bool vis[maxm];
int top, st[maxm];

int ecnt, head[maxm];

struct Edge {
	int nxt, to;
} edge[maxe];

inline void add(int from, int to) {
	edge[++ecnt] = {head[from], to};
	head[from] = ecnt;
}

void tarjan(int x) {
	dfn[x] = low[x] = ++tim;
	vis[x] = 1, st[++top] = x;
	for(int i = head[x]; i; i = edge[i].nxt) {
		int y = edge[i].to;
		if(!dfn[y]) {
			tarjan(y);
			low[x] = min(low[x], low[y]);
		}
		else if(vis[y]) low[x] = min(low[x], dfn[y]);
	}
	if(low[x] == dfn[x]) {
		tot++;
		while(top) {
			int t = st[top--];
			vis[t] = 0, scc[t] = tot;
			if(t == x) break;
		}
	}
}

int main() {
	scanf("%d%d%d", &n, &m, &k);
	int u, v;
	for(int i = 1; i <= m; i++) {
		scanf("%d%d", &u, &v);
		add(u, v + n), add(v, u + n);
	}
	for(int i = 1; i <= k; i++) {
		scanf("%d", &w);
		for(int j = 1; j <= w; j++) {
			scanf("%d", &s[j]);
			add(s[j] + n * 2, s[j]), add(s[j] + n * 4, s[j]);
		}
		for(int j = 2; j <= w; j++) {
			add(s[j] + n * 2, s[j - 1] + n * 2);
			add(s[j] + n, s[j - 1] + n * 2);
		}
		for(int j = 1; j < w; j++) {
			add(s[j] + n * 4, s[j + 1] + n * 4);
			add(s[j] + n, s[j + 1] + n * 4);
		}
	}
	for(int i = 1; i <= n * 6; i++) if(!dfn[i]) tarjan(i);
	for(int i = 1; i <= n; i++)
		if(scc[i] == scc[i + n]) { puts("NIE"); return 0;}
	puts("TAK");
	return 0;
}

我在模拟赛遇到了这个题,但是模拟赛原题要求输出方案,也写一下。

首先我们想到按照正常 2-SAT 的求解方式,直接输出 scc[i] > scc[i + n] 的点,然后 WA 了。仔细思考一下你就会发现我们刚才的建图并没有要求每个部分恰好有一个!也就是说有的部分可能一个都没选。那么我们对于每个部分分开考虑,在一个部分中显然满足 scc[i] > scc[i + n] 的点至多一个(建图要求),对于没选的部分直接随便输出一个即可,因为多选一个点时,边的条件仍然满足,而选这个部分的点也可使部分的条件满足。

posted @ 2026-09-03 16:17  PrinceEvan  阅读(3)  评论(0)    收藏  举报