2-SAT 问题
和网络流一样,很多问题都可以建模为 2-SAT 问题,只要会求解 2-SAT,就可以解决许多问题。
洛谷博客
github
vercel
 k-SAT
首先引入 k-SAT 问题。
k-SAT 问题说的就是有 n 个布尔值 ai 和 m 组约束条件,其中每组约束都是 a 中 k 个值进行逻辑运算后等于一个给定值 (0 或 1) 的形式,求一组满足约束的 ai(或者输出无解)。
比如
⎩⎪⎨⎪⎧(a0anda1)or(nota2)anda3=1(a0xor(nota1))anda2=0(a0ora1)xor(nota2)=1
是一个 3-SAT 问题,它的一个解为
⎩⎪⎪⎪⎨⎪⎪⎪⎧a0=0a1=0a2=0a3=1
可以证明 k-SAT 在 k≥3 时是没有多项式解法的,但是 k≤2 时有。
 2-SAT
2-SAT 问题是 k-SAT 问题在 k=2 时的特例。
每一个约束条件都可以表示为 「ai 为 x 或 aj 为 y」的形式。和网络流一样,很多问题都可以建模为 2-SAT 问题,只要会求解 2-SAT,就可以解决许多问题。建模当然比原算法难哈哈哈
接下来是重点:如何求解 2-SAT 问题。
 1. 图论建膜
先建立膜型 stO CYB Orz
我们把每个变量拆成两个点,一个表示该变量为真,另一个表示该变量为假。我们约定 ⟨i,x⟩ 为 ai=x 代表的节点,如 ⟨10,1⟩ 表示 a10=1 代表的节点。因此一个节点代表了一个命题。既然这样,我们就可以自然地定义节点 ⟨i,x⟩ 的非节点 ¬⟨i,x⟩ 定义为 ⟨i,¬x⟩,表示 ai=x 也就是 ai=¬x。
边表示蕴含关系,即 ⟨i,x⟩ 到 ⟨j,y⟩ 间有一条有向边表示如果 ai=x 则 aj=y。注意连边时要把这条边的逆否边连起来,⟨i,x⟩→⟨j,y⟩ 则 ⟨j,¬y⟩→⟨i,¬x⟩。为了方便,我们把 ⟨i,x⟩ 能走到 ⟨j,y⟩ 记为 ⟨i,x⟩⇒⟨j,y⟩,走不到记为 ⟨i,x⟩⇒⟨j,y⟩。
则原问题可化为:在约束对应的图上,选择 n 个点,使表示某变量为真与为假的点有且只有一个被选择,每个点若被选择,它能走到的节点也被选择。
 2. 求解
怎么解呢?当然要请出我们的 爆搜 Tarjan 大佬。
首先我们发现,如果一个节点与它的非节点互相连通,那么问题肯定无解,因为如果 ai=1⇒ai=0 且 ai=0⇒ai=1,那么就形成了逻辑矛盾。
然后我们发现,如果 ⟨i,x⟩⇒⟨i,¬x⟩ 且 ⟨i,¬x⟩⇒⟨i,x⟩ 则 ai=¬x。(其实也就是反证法)
如果 ⟨i,x⟩ 与 ⟨i,¬x⟩ 没有直接或间接关系,则 ai 可以随便取值。
因此我们考虑将图用 tarjan 把强连通分量缩点,得到一张 DAG。如果一个点和它的非节点在同一个强连通分量中,则问题无解,否则,ai 的取值就是 ⟨i,1⟩ 和 ⟨i,0⟩ 中缩点后拓扑序较小的节点,因为拓扑序较大的节点不可能走回拓扑序小的节点。
于是,我们得到了一组可行解。
 code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
   |  #include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #include<vector>
  using namespace std;
  namespace sat{
  	const int maxn=1000006; 	vector<int> e[maxn*2];
  	int n; 	inline int rho(int i,bool x){	 		return i*2-x; 	} 	void add(int i,bool a,int j,bool b){	 		e[rho(i,a)].push_back(rho(j,b)); 		e[rho(j,b^1)].push_back(rho(i,a^1)); 	}
  	int dfn[maxn*2],low[maxn*2],vis[maxn*2],cnt=0; 	int scc[maxn*2],sccn=0,stk[maxn*2],tp=0; 	void dfs(int u){ 		low[u]=dfn[u]=++cnt; 		stk[++tp]=u; 		vis[u]=1; 		for(int i=e[u].size()-1;i>=0;i--){ 			int v=e[u][i]; 			if(!dfn[v]){ 				dfs(v); 				low[u]=min(low[u],low[v]); 			}else if(vis[v]){ 				low[u]=min(low[u],dfn[v]); 			} 		} 		if(low[u]==dfn[u]){ 			sccn++; 			while(stk[tp]!=u){ 				vis[stk[tp]]=0; 				scc[stk[tp]]=sccn; 				tp--; 			} 			vis[stk[tp]]=0; 			scc[stk[tp]]=sccn; 			tp--; 		} 	} 	 	bool x[maxn]; 	bool solve(){ 		for(int i=1;i<=2*n;i++){ 			if(!dfn[i]){ dfs(i); } 		} 		for(int i=1;i<=n;i++){ 			if(scc[rho(i,0)]==scc[rho(i,1)]){ 				return 0; 			}else{ 				x[i]=scc[rho(i,0)]>scc[rho(i,1)]; 			} 		} 		return 1; 	}
  	bool& get(int i){ 		return x[i]; 	}
  }
  using sat::e; using sat::rho;
  int n,m;
  int main(){
  	scanf("%d%d",&n,&m); 	 	sat::n=n; 	for(int i=1;i<=m;i++){ 		int x,a,y,b; 		scanf("%d%d%d%d",&x,&a,&y,&b); 		sat::add(x,a^1,y,b); 	}
  	if(sat::solve()){ 		puts("POSSIBLE"); 		for(int i=1;i<=n;i++){ 			printf("%d%c",sat::x[i]," \n"[i==n]); 		} 	}else{ 		puts("IMPOSSIBLE"); 	}
  	return 0;
  }
 
  | 
 
 例题
模板题
双倍经验
NOI2017 游戏
第三题有一些难度,我做这道题的时候一开始建模建错了,结果不能保证每个地图都能分配到赛车……