2-SAT 學習筆記

Cyan_wind發表於2024-08-10

2-SAT 用於求解布林方程組,其中每個方程最多含有兩個變數,方程的形式為 \((a∨b)=1\) ,即式子 \(a\) 為真或式子 \(b\) 為真。求解的方法是根據邏輯關係式建圖,然後求強聯通子圖,每一個強聯通子圖的答案都是一樣的。

建圖:

這裡以模版題為例:

題意:給定若干個需要滿足的條件,其形式為 \(a,1/0,b,1/0\) ,表示要求 \(a\) 為真/假 或 \(b\) 為真/假。

我們將一個點拆成兩個 \(a\)\(\neg a\) ,分別表示 \(a\) 取真和取假。然後根據方程式,建一個有向圖,其中有向邊 \(a\to b\) 的定義為:若 \(a\) 被滿足,則 \(b\) 也要被滿足。一共有四種可能的情況:

  • \(a\) 為真或 \(b\) 為真:則當 \(a\) 為假時,\(b\) 一定為真,將 \(\neg a\)\(b\) 連一條有向邊;當 \(b\) 為假時,\(a\) 一定為真,將 \(\neg b\)\(a\) 連一條有向邊。
  • \(a\) 為真或 \(b\) 為假:則當 \(a\) 為假時,\(b\) 一定為假,將 \(\neg a\)\(\neg b\) 連一條有向邊;當 \(b\) 為真時,\(a\) 一定為假,將 \(b\)\(\neg a\) 連一條有向邊。
  • \(a\) 為假或 \(b\) 為真:則當 \(a\) 為真時,\(b\) 一定為真,將 \(a\)\(b\) 連一條有向邊;當 \(b\) 為假時,\(a\) 一定為假,將 \(\neg b\)\(\neg a\) 連一條有向邊。
  • \(a\) 為假或 \(b\) 為假:則當 \(a\) 為真時,\(b\) 一定為假,將 \(a\)\(\neg b\) 連一條有向邊;當 \(b\) 為真時,\(a\) 一定為假,將 \(b\)\(\neg a\) 連一條有向邊。

注意連邊建圖時,一定要將所有的情況全部連起來,並且一定要滿足 若 \(a\) 滿足,則一定可以推倒 \(b\) 滿足。

求強聯通子圖:

這裡介紹 Kosaraju 演算法。

該演算法分為兩步:

  1. 先對原圖中任意一個點開始進行 dfs ,直到所有點都被訪問過一遍。在 dfs 時,當我們每退出一個點時,就將該點放入一個棧內。
  2. 在原圖的反圖(即將所有 \(A\to B\) 的有向邊變成 \(B\to A\) )上進行 dfs 。從上一步中得到的棧的棧頂開始,每次遇到一個沒有被遍歷過的點,就遍歷該點,此時該點所能到達的所有點即為一個強聯通子圖。

求答案:

由於每個點只能選 真或假 ,而同一個聯通塊中的所有點都是同樣的值。所以當某個點的真和假都出現在同一個聯通塊中,則無解。隨便選一個聯通塊,並將其中的點都作為答案就可以了。

但是這樣還有一個問題,如下圖:

image

此時我們無論選第一個聯通塊還是第二個聯通塊都是可行的,但是當我們再加上一條 \(B\to !C\) 的邊,同時也會加上新的一條 \(C\to !B\) 的邊。這時我們就不能取聯通塊 \(A,B,C,D\) 了。

image

這時根據上面我們求聯通塊的演算法,若對於兩個點之間只存在一條有向邊 \(a\to b\) ,我們在遍歷反圖時會先遍歷 \(a\) ,此時邊反向變成了 \(b\to a\) ,然後 \(a\) 就無法達到 \(b\) ,所以 \(b\) 所在的聯通塊的編號會大於 \(a\) 所在的聯通塊,這樣我們透過取編號最大的那個聯通塊裡的點就可以避免上面這種問題。

Code:

#include<cstdio>
using namespace std;
const int N=1e6+5;
int n,m,tot,he[N*2],ne[N*2],to[N*2],he1[N*2],ne1[N*2],to1[N*2],d[N*2],ti,co[N*2];bool bj[N*2];
void add(int x,int y)
{
	tot++;ne[tot]=he[x];to[tot]=y;he[x]=tot;
	ne1[tot]=he1[y];to1[tot]=x;he1[y]=tot;
}
void dfs1(int x)
{
	int i,y;
	bj[x]=1;
	for(i=he[x];i!=0;i=ne[i])
	{
		y=to[i];
		if(bj[y]==0)
		dfs1(y);
	}
	d[++ti]=x;
}
void dfs2(int x)
{
	int i,y;
	co[x]=ti;bj[x]=1;
	for(i=he1[x];i!=0;i=ne1[i])
	{
		y=to1[i];
		if(bj[y]==0)
		dfs2(y);
	}
}
void Kosaraju()
{
	int i;
	for(i=1;i<=2*n;i++)
	{
		if(bj[i]==0)
		dfs1(i);
	}
	for(i=1;i<=n*2;i++)
	bj[i]=0;
	ti=0;
	for(i=n*2;i>=1;i--)
	{
		if(bj[d[i]]==0)
		{
			ti++;dfs2(d[i]);
		}
	}
}
int main()
{
	int i,x,y,bj1,bj2;
	scanf("%d%d",&n,&m);
	for(i=1;i<=m;i++)
	{
		scanf("%d%d%d%d",&x,&bj1,&y,&bj2);
		add(x+n*bj1,y+n*(bj2^1))
		;add(y+n*bj2,x+n*(bj1^1));
	}
	Kosaraju();
	for(i=1;i<=n;i++)
	{
		if(co[i]==co[i+n])
		{
			printf("IMPOSSIBLE");
			return 0;
		}
	}
	printf("POSSIBLE\n");
	for(i=1;i<=n;i++)
	{
		if(co[i]>co[i+n])
		printf("1 ");
		else
		printf("0 ");
	}
	return 0;
}