2-SAT 好题分享

写在前面

结尾语使用了 deepseek AI。


P4782 【模板】2-SAT

想题时间:4 min。

模板很重要。

概括:给你 \(n\) 个变量,有 \(m\) 个约束条件。每个约束条件形如 \(x_i\)true/false\(x_j\)true/false,问最后有没有解。若有解,给出方案。

点评

bool 方程的思想+图论。

  • 考虑两个变量的情况。

  • \(a \vee b\) 可以写作 \((\neg a \to b) \wedge (\neg b \to a)\)

\(x \to y\) 相当于建立一条图上的有向边。那么可以使用强连通分量来判断是否无解。

  • 如果 \(a\)\(\neg a\) 在同一个强连通分量内,表示 \((\neg a \to a) \wedge (a \to \neg a)\)。矛盾,所以无解。

  • 否则肯定要选其中一个,如果选拓扑序靠前的,那么它有可能会沿着缩点后的 DAG 走到拓扑序靠后的,本来有解被判断成了无解,肯定是不行的。证明后面会说。

#include<bits/stdc++.h>
#define int long long
#define ull unsigned long long
#define rep(i,l,r) for(int i=l;i<=r;++i)
using namespace std;

const int N=2e6+5;
int n,m,top=0,cnt=0,tot=0;
int dfn[N],low[N],bel[N],st[N],nxt[N],head[N],to[N];
int SCC=0;
int a[N][2];
bool vis[N];

void add(int x,int y){
	nxt[++tot]=head[x];
	head[x]=tot;
	to[tot]=y;
}

void Tarjan(int x){
	dfn[x]=low[x]=++cnt;
	vis[st[++top]=x]=1;
	for(int i=head[x];i;i=nxt[i]){
		int y=to[i];
		if(!dfn[y]){
			Tarjan(y);
			low[x]=min(low[x],low[y]);
		}else if(vis[y]){
			low[x]=min(low[x],dfn[y]);
		}
	}
	if(dfn[x]==low[x]){
		int v;
		++SCC;
		do{
			v=st[top--];
			vis[v]=0;
			bel[v]=SCC;
		}while(x!=v);
	}
}

signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cin>>n>>m;
	for(int i=1,s=0;i<=n;++i){
		a[i][0]=++s;
		a[i][1]=++s;
	}
	rep(i,1,m){
		int x,y,c,d;
		cin>>x>>c>>y>>d;
		add(a[x][!c],a[y][d]);
		add(a[y][!d],a[x][c]);
	}
	rep(i,1,(n<<1)){
		if(!dfn[i])Tarjan(i);
	}
	rep(i,1,n){
		if(bel[a[i][0]]==bel[a[i][1]]){
			cout<<"IMPOSSIBLE\n";
			return 0;
		}
	}
	cout<<"POSSIBLE\n";
	rep(i,1,n){
		if(bel[a[i][0]]<bel[a[i][1]]){
			cout<<"0 ";
		}else cout<<"1 ";
	}
	cout<<"\n";
	return 0;
}

PS:

P4171 [JSOI2010] 满汉全席

想题时间:6 min。

先看原题:

题目描述

满汉全席是中国最丰盛的宴客菜肴,有许多种不同的材料透过满族或是汉族的料理方式,呈现在数量繁多的菜色之中。由于菜色众多而繁杂,只有极少数博学多闻技艺高超的厨师能够做出满汉全席,而能够烹饪出经过专家认证的满汉全席,也是中国厨师最大的荣誉之一。世界满汉全席协会是由能够料理满汉全席的专家厨师们所组成,而他们之间还细分为许多不同等级的厨师。

为了招收新进的厨师进入世界满汉全席协会,将于近日举办满汉全席大赛,协会派遣许多会员当作评审员,为的就是要在参赛的厨师之中,找到满汉界的明日之星。

大会的规则如下:每位参赛的选手可以得到 \(n\) 种材料,选手可以自由选择用满式或是汉式料理将材料做成菜肴。

大会的评审制度是:共有 \(m\) 位评审员分别把关。每一位评审员对于满汉全席有各自独特的见解,但基本见解是,要有两样菜色作为满汉全席的标志。如某评审认为,如果没有汉式东坡肉跟满式的涮羊肉锅,就不能算是满汉全席。但避免过于有主见的审核,大会规定一个评审员除非是在认为必备的两样菜色都没有做出来的状况下,才能淘汰一位选手,否则不能淘汰一位选手。

换句话说,只要参赛者能在这两种材料的做法中,其中一个符合评审的喜好即可通过该评审的审查。如材料有猪肉,羊肉和牛肉时,有四位评审员的喜好如下表:

评审一 评审二 评审三 评审四 
满式牛肉 满式猪肉 汉式牛肉 汉式牛肉 
汉式猪肉 满式羊肉 汉式猪肉 满式羊肉 

如参赛者甲做出满式猪肉,满式羊肉和满式牛肉料理,他将无法满足评审三的要求,无法通过评审。而参赛者乙做出汉式猪肉,满式羊肉和满式牛肉料理,就可以满足所有评审的要求。

但大会后来发现,在这样的制度下如果材料选择跟派出的评审员没有特别安排好的话,所有的参赛者最多只能通过部分评审员的审查而不是全部,所以可能会发生没有人通过考核的情形。

如有四个评审员喜好如下表时,则不论参赛者采取什么样的做法,都不可能通过所有评审的考核:

评审一 评审二 评审三 评审四 
满式羊肉 满式猪肉 汉式羊肉 汉式羊肉 
汉式猪肉 满式羊肉 汉式猪肉 满式猪肉 

所以大会希望有人能写一个程序来判断,所选出的 \(m\)位评审,会不会发生没有人能通过考核的窘境,以便协会组织合适的评审团。

概括:一道菜有满汉两种料理方式,只能恰用一种料理方式做一道菜。给出若干个约束,每一个形如一个菜用满/汉料理 \(\vee\) 另一个菜用满/汉料理,问是否有解。

  • 概括的核心在于抓不变量对立面,在本题中对立双方显然是满/汉料理, 把它们看成 true/false 来解题。

点评

看概括你就会做了。关键在于转化。

一部分人编号 \(i\)\(i+n\),作为对立的双方,但是需要分类讨论 4 种,容易写挂。

不妨考虑记一个 \(x_{0/1}\),给它赋值一个编号(任意,但不能重复,推荐 ++idx)。

这其实是建立一个类似 STL map 的映射。帮助我们快速访问一个变量的真/假。

接下来就是 2-SAT 板子。

#include<bits/stdc++.h>
#define ll long long
#define rep(i,l,r) for(int i=(l);i<=(r);++i)
using namespace std;

const int N=510,M=4010;
int T,idx=1,cnt=0;
int n,m,ts;
int a[N][2];
int head[N<<1];
int bel[N<<1];
int dfn[N<<1],low[N<<1];
int stk[N<<1],top=0;
bool ins[N<<1];

struct edge{
	int to,nxt;
}e[M<<2];

void add_edge(int u,int v){
	++idx;
	e[idx].to=v;
	e[idx].nxt=head[u];
	head[u]=idx;
}

void Tarjan(int x){
	dfn[x]=low[x]=++ts;
	stk[++top]=x;
	ins[x]=1;
	for(int i=head[x];i!=0;i=e[i].nxt){
		int y=e[i].to;
		if(!dfn[y]){
			Tarjan(y);
			low[x]=min(low[x],low[y]);
		}else if(ins[y]){
			low[x]=min(low[x],dfn[y]);
		}
	}
	if(low[x]==dfn[x]){
		++cnt;
		int v;
		do{
			v=stk[top--];
			ins[v]=0;
			bel[v]=cnt;
		}while(v!=x);
	}
}

signed main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	cin>>T;
	while(T--){
		cin>>n>>m;
		int id=0;
		rep(i,1,n){
			a[i][0]=++id;
			a[i][1]=++id;
		}
		rep(i,1,(n<<1)){
			dfn[i]=low[i]=0;
			head[i]=0;
			ins[i]=0;
			bel[i]=0;
		}
		idx=1,top=0,cnt=0,ts=0;
		rep(i,1,m){
			char c,d;
			int e,f;
			cin>>c>>e>>d>>f;
			int c2=((c=='m')?1:0);
			int d2=((d=='m')?1:0);
			int x=e,y=f;
			add_edge(a[x][!c2],a[y][d2]);
			add_edge(a[y][!d2],a[x][c2]);
		}
		rep(i,1,2*n){
			if(!dfn[i]){
				Tarjan(i);
			}
		}
		bool flag=1;		
		rep(i,1,n){
			if(bel[a[i][0]]==bel[a[i][1]]){
				flag=0;
				break;
			}
		}
		if(flag){
			cout<<"GOOD\n";
		}else{
			cout<<"BAD\n";
		}
	}
	return 0;
}

P5782 [POI 2001] 和平委员会

想题时间:4 min。

概括:有 \(n\) 个党派,每个党派有 \(2\) 个代表,同时有 \(m\) 对矛盾关系。代表从 \(1\) 编号到 \(2n\)。 编号为 \(2i−1\)\(2i\) 的代表属于第 \(i\) 个党派。和平委员会的组建规则满足:

  1. 一个党派的两个代表只能恰好去一个。
  2. 对于一对矛盾关系而言,他们不能同时出现。

问是否有解。如果有解,输出方案。

点评

二元关系自然想到 2-SAT 来解决。

一个党派的两个代表只能去一个,当作变量 \(x_i\)true/false 使用。

对于矛盾关系,设 \(a,b\),考虑如何利用建立逻辑关系。

  • 当两人无法同时出现时,他们分别的对立面肯定要出现一个,写成符号是 \(\neg a \vee \neg b\)
  • 等价于 \((b \to \neg a)\wedge (a \to \neg b)\)
  • 据此直接跑 2-SAT 即可。
#include<bits/stdc++.h>
#define ll long long
#define rep(i,l,r) for(int i=(l);i<=(r);++i)
using namespace std;

const int N=1e5+10,M=2e4+10;
int id=0,idx=0,ts,n,m;
int SCC;
int dfn[N<<1];
int low[N<<1],head[N<<1];
bool ins[N<<1];
int stk[N<<1],top;
int a[N][2];
int bel[N<<1];

struct edge{
	int to,nxt;
}e[M<<1];

void add(int u,int v){
	++idx;
	e[idx].to=v;
	e[idx].nxt=head[u];
	head[u]=idx;
}

void Tarjan(int x){
	dfn[x]=low[x]=++ts;
	stk[++top]=x;
	ins[x]=1;
	for(int i=head[x];i!=0;i=e[i].nxt){
		int y=e[i].to;
		if(!dfn[y]){
			Tarjan(y);
			low[x]=min(low[x],low[y]);
		}else if(ins[y]){
			low[x]=min(low[x],dfn[y]);
		}
	}
	if(low[x]==dfn[x]){
		int v;
		++SCC;
		do{
			v=stk[top--];
			ins[v]=0;
			bel[v]=SCC;
		}while(v!=x);
	}
}

signed main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	cin>>n>>m;
	rep(i,1,n){
		a[i][0]=++id;
		a[i][1]=++id;
	}
	rep(i,1,m){
		int a,b;
		cin>>a>>b;
		int c=((a&1)?a+1:a-1);
		int d=((b&1)?b+1:b-1);
		add(a,d);
		add(b,c);
	}
	rep(i,1,(n<<1)){
		if(!dfn[i])Tarjan(i);
	}
	rep(i,1,n){
		if(bel[a[i][0]]==bel[a[i][1]]){
			cout<<"NIE\n";
			return 0;
		}
	}
	rep(i,1,n){
		if(bel[a[i][0]]<bel[a[i][1]]){
			cout<<a[i][0]<<'\n';
		}else{
			cout<<a[i][1]<<'\n';
		}
	}
	return 0;
}

P3513 [POI 2011] KON-Conspiracy

想题时间:7 min。

概括

\(n\) 个人,要把他们分成两个组别。

  • 第一个组别,要求两两互相认识。不妨叫它后勤组。
  • 第二个组别,要求两两互相不认识。不妨叫它阴谋组。
  • 注意:这里的认识不具有传递性,即上面的认识代表两人直接认识。

现在给出与 \(n\) 个人直接认识的人的编号,并保证对称,即若 \(x\) 认识 \(y\)\(y\) 也认识 \(x\)

要你求分组的方案数。若无解输出 \(0\)

\(2 \le n \le 5000\)

点评

把两个组别视为对立面做 2-SAT。记 \(x_{i,0/1}\) 表示把 \(i\) 分到阴谋组/后勤组的情况。这时候是很难直接写出带 \(\vee\) 的式子的,不妨分类讨论。

  • \(i\) 认识 \(j\),那么两人不能同时在阴谋组,那么 \(\neg x_{i,0} \wedge \neg x_{j,0}\)。在图中建模对应为 \(x_{j,0} \to x_{i,1}\)\(x_{i,1} \to x_{j,1}\)
  • \(i\) 不认识 \(j\),那么两人不能同时在后勤组,那么 \(\neg x_{i,1} \wedge \neg x_{j,1}\)。在图中建模对应为 \(x_{j,1} \to x_{i,0}\)\(x_{i,1} \to x_{j,0}\)

直接跑 2-SAT 是否有解。考虑调整法。不存在 \(\ge 2\) 个人被调到对立组别。(想一想)

那么只有四种情况。

  1. 啥也不干,看看 2-SAT 跑出来的是否有解(可能存在一个组为空的情况,此时不合法)。
  2. 后勤组枚举一个人调到阴谋组,要满足这个人与阴谋组的人都不认识。
  3. 阴谋组枚举一个人调到后勤组,要满足这个人与后勤组的人都认识。
  4. 后勤组和阴谋组互换一个人。
    • 设后勤组要换的人是 \(x\),阴谋组要换的人是 \(y\)\(a_x\) 表示 \(x\) 在阴谋组中认识的人数,\(b_y\) 表示 \(y\) 在后勤组中不认识的人数。
    • 若要 \(x,y\) 互换合法,要满足 \(x\) 与除阴谋组 \(y\) 外都不认识,且 \(y\) 与后勤组除 \(x\) 外的人都互相认识。
      • \(x\)\(y\) 相互认识。则 \(a_x - 1 =0\)\(b_y - 0=0\)
      • \(x\)\(y\) 相互不认识,与上面情况相反,\(a_x - 0=0\)\(b_y -1=0\)

依次判断即可。

#include<bits/stdc++.h>
#define ll long long
#define rep(i,l,r) for(int i=(l);i<=(r);++i)
using namespace std;

const int N=5e3+10;
int n,top,id,idx=1;
int ts=0,SCC;
int dfn[N<<1],low[N<<1],stk[N<<1];
int bel[N<<1],x[N<<1][2],head[N<<1];
bool mp[N][N],ins[N<<1];
int match_cnt[N<<1];
int lk[N<<1];
vector<int> tmp0,tmp1;

struct edge{
	int to,nxt;
}e[N*N*2];

void add_edge(int u,int v){
	++idx;
	e[idx].to=v;
	e[idx].nxt=head[u];
	head[u]=idx;
}

void Tarjan(int x){
	dfn[x]=low[x]=++ts;
	stk[++top]=x;
	ins[x]=1;
	for(int i=head[x];i!=0;i=e[i].nxt){
		int y=e[i].to;
		if(!dfn[y]){
			Tarjan(y);
			low[x]=min(low[x],low[y]);
		}else if(ins[y]){
			low[x]=min(low[x],dfn[y]);
		}
	}
	if(dfn[x]==low[x]){
		int v;
		++SCC;
		do{
			v=stk[top--];
			ins[v]=0;
			bel[v]=SCC;
		}while(v!=x);
	}
}

signed main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	cin>>n;
	rep(i,1,n){
		int k;
		cin>>k;
		rep(j,1,k){
			int a;
			cin>>a;
			mp[i][a]=mp[a][i]=1;
		}
		x[i][0]=++id;
		x[i][1]=++id;
	}
	for(int i=1;i<n;++i){
		for(int j=i+1;j<=n;++j){
			if(mp[i][j]){
				add_edge(x[i][0],x[j][1]);
				add_edge(x[j][0],x[i][1]);
			}else{
				add_edge(x[i][1],x[j][0]);
				add_edge(x[j][1],x[i][0]);
			}
		}
	}
	rep(i,1,id){
		if(!dfn[i]){
			Tarjan(i);
		}
	}
	rep(i,1,n){
		if(bel[x[i][0]]==bel[x[i][1]]){
			cout<<"0\n";
			return 0;
		}
		if(bel[x[i][0]]<bel[x[i][1]]){
			tmp0.push_back(i);
		}else{
			tmp1.push_back(i);
		}
	}
	ll ans=0;
	if((int)tmp0.size()>0 && (int)tmp1.size()>0)ans=1;
	for(int x:tmp0){
		for(int y:tmp1){
			if(!mp[x][y]){
				match_cnt[x]++;
				lk[x]=y;
			}
		}
	}
	for(int x:tmp1){
		for(int y:tmp0){
			if(mp[x][y]){
				match_cnt[x]++;
				lk[x]=y;
			}
		}
	}
	rep(x,1,n){
		if(match_cnt[x]==1){
			if(match_cnt[lk[x]]==0){
				++ans;
			}
		}
	}
	for(int x:tmp0){
		if((int)tmp0.size()>1 && match_cnt[x]==0)++ans;
	}
	for(int x:tmp1){
		if((int)tmp1.size()>1 && match_cnt[x]==0)++ans;
	}
	cout<<ans<<'\n';
	return 0;
}

结尾(deepseek)

其实,我们回过头来看 2-SAT,它解决的不仅仅是算法竞赛里的一道难题。它本质上是在教我们一件事:在一个充满约束的世界里,如何找到一组自洽的选择

我们常常觉得身不由己,是因为生活中的“约束条件”太多了。而 2-SAT 告诉我们的一个很酷的道理是:即便有再多的限制,只要这些限制是逻辑清晰的,我们总能在矛盾中找到一个可行的解。 更重要的是,它让我们明白,很多看似无解的两难境地,其实是因为我们没有找到那个能同时满足所有条件的“赋值”。

所以,希望大家在以后遇到纠结和两难时,不仅能想起今天这个算法,更能拥有 2-SAT 背后的那种思维力量:冷静地分析约束,勇敢地做出选择,然后在看似矛盾的现实里,找到那个属于自己的、逻辑自洽的答案。

今天的分享就到这里,谢谢大家!

posted @ 2026-04-02 14:20  lbh666  阅读(2)  评论(0)    收藏  举报