2-SAT问题

一.2-SAT学习

2-SAT建图

模板题

  • 问题描述: 有 n 个 bool 变量,每个布尔变量都有 0,1 两种可能,给定 m 条限制,例如 (u=0/1)||(v=0/1),或者 (u=0/1)&&(v=0/1),求满足限制的一种方案?
  • 2-SAT: 以图论的形式做,将每个点抽象成两个点,表示等于 0 或等于 1。其中: [ 0 , 2 , 4 … … ] [0,2,4……] [0,2,4] 表示等于 0 , [ 1 , 3 , 5 … … ] [1,3,5……] [1,3,5] 表示等于 1。然后对限定条件,转化为连边即可。例如:u 为 0 时,v 必须为1: 2 u − > 2 v + 1 2u->2v+1 2u>2v+1

限定条件连边补充:

若且关系:
u去v必去      :2u+1->2v+1
u去v就不去    :2u+1->2v

强制限定关系:
u必须去       :2u->2u+1
u不可去       :2u+1->2u 

位运算
u or v = 1    :2u->2v+1,2v->2u+1
u & v = 0     :2u+1->2v,2v+1->2u
u xor v = 1   :2u->2v+1,2v+1->2u;2v->2u+1,2u+1->2v
u xor v = 0   :2u->2v,2v->2u;2u+1->2v+1,2v+1->2u+1

tarjan O(n) 求 2-SAT 的一组解(不可限定特征)

  • 原理: 若 u 的 0 和 1 在同一个强连通分量中,则题意无法满足。否则存在若干解
  • 特点: t a r j a n tarjan tarjan 只能输出一个随机的解,无法输出有特征的解。并且只支持有向图
  • 输出一个解: 拓扑序越大 -> 越优,即若 u 的 1 拓扑序较 u 的 0 大,就输出 1 ,否则输出 0 。事实上,在缩点染色的时候我们就已经有个一个倒序拓排序。那么:颜色编号越小 -> 拓扑序越大 -> 越优,即 c o l o r [ 2 u ] > c o l o r [ 2 u + 1 ] color[2u]>color[2u+1] color[2u]>color[2u+1] 时输出 1 ;否则输出 0 。

Code

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+20;
struct ppp {
	int u,v,next;
} e[N*2];
int n,k,vex[N*2];
int dfn[N*2],low[N*2],cnt;
int co,color[N*2],stk[N*2],top,instk[N*2];

void add(int u,int v) {
	k++;
	e[k].u=u;
	e[k].v=v;
	e[k].next=vex[u];
	vex[u]=k;
}
void tarjan(int u) {
    dfn[u]=low[u]=++cnt;
    instk[u]=1;
    stk[++top]=u;
    for(int i=vex[u];i;i=e[i].next){
    	int v=e[i].v;
    	if(!dfn[v]){
    		tarjan(v);
    		low[u]=min(low[v],low[u]);
		}
		else if(instk[v])low[u]=min(low[u],dfn[v]);
	}
	if(dfn[u]==low[u]){
		co++;
		while(1){
			int t=stk[top--];
			color[t]=co;
			instk[t]=0;
			if(t==u)break;
		}
	}
}
void solve(){
	for(int i=2;i<=2*n+1;i++)if(!dfn[i])tarjan(i);
	for(int i=1;i<=n;i++){
		if(color[2*i]==color[2*i+1]){
			cout<<"IMPOSSIBLE"; 
			return ;
		}
	}
	cout<<"POSSIBLE"<<endl;
	for(int i=1; i<=n; i++)cout<<(coler[2*i]>color[2*i+1])<<" ";
}
int main() {
	int m,u,v,x,y;
	cin>>n>>m;
	for(int i=1; i<=m; i++) {
		cin>>u>>x>>v>>y;
		add();//限定关系连边
	}
	solve();
}

dfs 求解 2-SAT 的一组解(可以限定特征)

  • 原理: 通过 d f s dfs dfs 染色,可以得到 2-SAT 问题的一组特殊解,其支持:按照位开始,优先染色。例如:字典序最小的解
  • 特点: d f s dfs dfs 染色可以求出有特征的解,并且支持无向图求解。缺点是时间复杂度偏高。
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+20;
struct ppp {
	int u,v,next;
} e[N*2];
int n,k,vex[N*2];
int dfn[N*2],low[N*2],cnt;
int co,color[N*2],stk[N*2],top,instk[N*2];

void add(int u,int v) {
	k++;
	e[k].u=u;
	e[k].v=v;
	e[k].next=vex[u];
	vex[u]=k;
}
int dfs(int u){
	if(vis[u^1])return 0;
	if(vis[u])return 1;
	vis[u]=1;
	stk[++top]=u;
	for(int i=vex[u];i;i=e[i].next){
		int v=e[i].v;
		if(dfs(v)==0)return 0;
	}
	return 1;
}
void solve() {//最小字典序版
    for(int i=1;i<=n;i++){
    	if(vis[2*i]==0&&vis[2*i+1]==0){
    		top=0;
    		if(dfs(2*i)==0){//优先考虑不去 
    			while(top)vis[stk[top--]]=0;
    			if(dfs(2*i+1)==0){//其次考虑去 
    				cout<<"无解";
    				return;
				}
			}
		}
	}
	for(int i=1;i<=n;i++){
		if(vis[2*i])cout<<"0 ";
		else cout<<"1 ";
	}
}
int main() {
	int m,u,v;
	cin>>n>>m;
	for(int i=1; i<=m; i++) {
		add();//限定关系连边
	}
	solve();
}

二.2-SAT 例题

tarjan 求解

例题1:建图,判断是否有解

  • 题目描述: n 个队伍,每个队伍有三个人 a i , b i , c i a_i,b_i,c_i ai,bi,ci 。其中 a i a_i ai 为队长。要求要么队长去,要么两个队员都去。同时还有 m 个限定条件 a i , b i a_i,b_i ai,bi,表示 a i , b i a_i,b_i ai,bi 必须只去一个。求是否有可行解。
  • 限定条件 a i , b i , c i a_i,b_i,c_i ai,bi,ci
  • 2 a i + 1 − > 2 b i , 2 a i + 1 − > 2 c i 2a_i+1->2b_i,2a_i+1->2c_i 2ai+1>2bi,2ai+1>2ci
  • 2 a i − > 2 b i + 1 , 2 a i − > 2 c i + 1 2a_i->2b_i+1,2a_i->2c_i+1 2ai>2bi+1,2ai>2ci+1
  • 限定条件 a i , b i a_i,b_i ai,bi
  • 2 a i + 1 − > 2 b i , 2 a i − > 2 b i + 1 2a_i+1->2b_i,2a_i->2b_i+1 2ai+1>2bi,2ai>2bi+1
  • 使用 t a r j a n tarjan tarjan 判断是否有可行解即可。时间复杂度 O ( n + m ) O(n+m) O(n+m)

例题2:建图,判断是否有解

  • 题目描述: 1 对夫妇结婚,n-1 对夫妇去参加婚礼,有一个很长的桌子,新郎新娘坐在桌子两边,然后 n-1 对夫妇就座,要求任何一对夫妇都不能坐在同一边。同时有 m 对人有奸情,有奸情的两个人不能同时坐在新娘对面,只能分开做或都做到新娘的那边,判断是否有解。如果有解,要求输出坐在新娘一边的人。
  • 问题分析: 将新娘所在桌子的那边记为 “1”。将 2n 个人拆分成 2n 个 0 和 2n 个 1 。
  • 对于新娘(x)新郎(y)位置:
  • x − > x + 2 n , y + 2 n − > y x->x+2n,y+2n->y x>x+2n,y+2n>y
  • 对于夫妇关系 x , y x,y x,y
  • x − > y + 2 n , y − > x + 2 n ; y + 2 n − > x , x + 2 n − > y x->y+2n,y->x+2n;y+2n->x,x+2n->y x>y+2n,y>x+2n;y+2n>x,x+2n>y
  • 对于奸情关系 x , y x,y x,y
  • x + 2 n − > y , y + 2 n − > x x+2n->y,y+2n->x x+2n>y,y+2n>x

二.开始刷题
例题1

将满式和汉式分别记为0,1即可,其他相当于模板题

#include<bits/stdc++.h>
using namespace std;
const int N=1e4+20;
const int M=5e4;
struct ppp {
	int u,v,next;
} e[M];
int n,k,vex[N];
int dfn[N],low[N],cnt;
int co,color[N],stk[N],top,instk[N];

void add(int u,int v) {
	k++;
	e[k].u=u;
	e[k].v=v;
	e[k].next=vex[u];
	vex[u]=k;
}
void tarjan(int u) {
	dfn[u]=low[u]=++cnt;
	instk[u]=1;
	stk[++top]=u;
	for(int i=vex[u]; i; i=e[i].next) {
		int v=e[i].v;
		if(!dfn[v]) {
			tarjan(v);
			low[u]=min(low[v],low[u]);
		} else if(instk[v])low[u]=min(low[u],dfn[v]);
	}
	if(dfn[u]==low[u]) {
		co++;
		while(1) {
			int t=stk[top--];
			color[t]=co;
			instk[t]=0;
			if(t==u)break;
		}
	}
}
void init(){
	memset(vex,0,sizeof(vex));
	memset(dfn,0,sizeof(dfn));
	memset(low,0,sizeof(low));
	memset(e,0,sizeof(e));
	memset(color,0,sizeof(color));
	k=0;
	cnt=0;
	co=0; 
}
void solve() {
	for(int i=1; i<=2*n; i++)if(!dfn[i])tarjan(i);
	for(int i=1; i<=n; i++) {
		if(color[i]==color[i+n]) {
			cout<<"BAD"<<endl;
			return ;
		}
	}
	cout<<"GOOD"<<endl;
}
int main() {
	int T,m,u,v;
	int p1,p2;
	char x,y;
	cin>>T;
	while(T--) {
		init(); 
		cin>>n>>m;
		for(int i=1; i<=m; i++) {
			cin>>x>>u>>y>>v;
			if(x=='h')p1=1;
			else p1=0;
			if(y=='h')p2=1;
			else p2=0;
			add(u+(!p1)*n,v+p2*n);
			add(v+(!p2)*n,u+p1*n);
		}
		solve();
	}
	return 0;
}

dfs暴搜

例题1:dfs染色,优先染和上一位置一样的颜色

  • 题目描述: n 个元素的 01 串,其元素未知。有 m 个已知条件 [ l i , r i ] , k i [l_i,r_i],k_i [li,ri],ki,表示区间 [ l i , r i ] [l_i,r_i] [li,ri] 中的 1 的个数为奇数或偶数。求一个最小字典序解。 n , m ∈ [ 1 , 1000 ] n,m\in[1,1000] n,m[1,1000]
  • 问题分析: [ l i , r i ] , s i [l_i,r_i],s_i [li,ri],si 可以转化为 ( l i ⨁ l i + 1 ⨁ … … ⨁ r i = s i ) (l_i⨁l_{i+1}⨁……⨁r_i=s_i) (lili+1ri=si) ,再转化为 ( p r e r i ⨁ p r e l i − 1 = k i ) (pre_{r_i}⨁pre_{l_{i-1}}=k_i) (preripreli1=ki) 。这样就可以转化为 2-SAT 问题了。考虑在 dfs 的时候,优先考虑与上一次解一样的解,即可。注意 p r e [ 0 ] = 0 pre[0]=0 pre[0]=0
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+20;
struct ppp {
	int u,v,next;
} e[N*2];
int n,k,vex[N*2];
int dfn[N*2],low[N*2],cnt;
int co,color[N*2],stk[N*2],top,instk[N*2],vis[N*2];

void add(int u,int v) {
	k++;
	e[k].u=u;
	e[k].v=v;
	e[k].next=vex[u];
	vex[u]=k;
}
int dfs(int u){
	if(vis[u^1])return 0;
	if(vis[u])return 1;
	vis[u]=1;
	stk[++top]=u;
	for(int i=vex[u];i;i=e[i].next){
		int v=e[i].v;
		if(dfs(v)==0)return 0;
	}
	return 1;
}
void solve() {
    int last=0;
    for(int i=0;i<=n;i++){
    	if(vis[2*i]==0&&vis[2*i+1]==0){
    		top=0;
    		if(last==0){
    			int t=dfs(2*i);
    			if(t==0){
    				while(top)vis[stk[top--]]=0;
    				dfs(2*i+1);
    				last=1;
				}else last=0;
			}else{
				int t=dfs(2*i+1);
    			if(t==0){
    				while(top)vis[stk[top--]]=0;
    				dfs(2*i);
    				last=0;
				}else last=1;
			}
		}else last=(vis[2*i+1]==1);
	}
	for(int i=1;i<=n;i++){
		if(vis[2*i+1]==vis[2*(i-1)+1])cout<<"0";
		else cout<<"1";
		if(i!=n)cout<<" ";
	}
}
int main() {
	int m,u,v,s;
	cin>>n>>m;
	for(int i=1; i<=m; i++) {
		cin>>u>>v>>s;
		u--;
		if(s==1){
			add(2*u+1,2*v);
			add(2*v,2*u+1);
			add(2*v+1,2*u);
			add(2*u,2*v+1);
		}else{
			add(2*u,2*v);
			add(2*v,2*u);
			add(2*u+1,2*v+1);
			add(2*v+1,2*u+1);
		}
	}
	add(2*0+1,2*0+0);
	solve();
}

例题2:dfs染色,求 1 的数量最多为多少

  • 题目描述: n 个人,每个人只有两个身份,要么骗子,要么同伴。骗子只会说假话,同伴只会说真话。m 个评论,表示 u i u_i ui v i v_i vi w i w_i wi,其中 w i = 1 / 0 w_i=1/0 wi=1/0 表示是骗子/同伴。 n ∈ [ 1 , 2 e 5 ] , m ∈ [ 0 , 2 e 5 ] n\in[1,2e5],m\in[0,2e5] n[1,2e5],m[0,2e5]
  • 问题分析: 仔细想想,其实并不是单向的,而是双向的。限定条件为 u ⨁ v = w u⨁v=w uv=w
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值