2-SAT问题

2-SAT问题是一种解决具有两种状态变量的逻辑问题的算法,常与Tarjan或并查集配合使用。题目描述了一个旅馆中的人需要通过开关控制房间状态逃出的场景,每个房间由两个开关控制,且每个房间必须同时打开。文章提供了输入输出格式,并指出问题可以转化为无向图的2-SAT问题,使用并查集求解。另外,还提到了另一道较难的2-SAT题目,该题为单向边,需要使用Tarjan算法,判断状态是否在同一强联通分量中。

what is 2-SAT?

2-SAT问题是一种求解对于多个变量都只有0,1两种取值的问题的算法,对于题目中所给的两个相互依存的条件之间连一条边,通常配合tarjan来使用(或者并查集)

来看一道模板题中的模板题——比洛谷模板还水 感觉洛谷模板有点难

CF776D The Door Problem

【问题描述】

一家旅馆的 n 间不同的房间里困住了 n 个人,其中有些房间是被锁住了,有些房间是打开的,但是只有在所有房间同时打开的情况下,被困人员才能逃离。现在有 m 个开关,每个开关控制着一些房间的门,开关的作用是使得这些房间原来开着的关上,关上的打开,但每个门都被两个开关控制。

【输入格式】

第一行,有两个正整数 n 和 m(2 ≤  n, m ≤ 10^5) , n 表示房间的数量,m 表示开关的数量。 第二行 n 个数,表示每个房间的状态,0 表示房间是锁住的,1 表示房间是开着的。
再接下来 m 行,每行第一个数 x 表示第 i 把锁控制的房间数,再接着有 x 个数,分别表示所控制的房间编号(1~n) 。

数据保证每个房间是被两个开关控制的。

【输出格式】

如果房间都能被打开则输出“YES”,否则输出“NO”

这道题是典型的2-SAT问题

如果这个门是开着的,意味着控制这个门的两个开关都需要按一遍,那么我们把开关1的开状态和开关2的开状态连边,把他们的关状态连边

如果这个门是关着的,意味着控制这个门的两个开关有一个需要按,那么我们把开关1的开状态和开关2的关状态两边,把开关1的关状态和开关2的开状态连边。

因为这道题是一个无向图,所以我们可以用并查集来求解

如果一个开关的开状态和关状态相连,那么就不能做

下面上代码:

# include <cstdio>
# include <algorithm>
# include <cstring>
# include <cmath>
# include <climits>
# define Rep(i,a,b) for(int i=a;i<=b;i++)
# define _Rep(i,a,b) for(int i=a;i>=b;i--)
# pragma-GCC-optimize("-Ofast") 
using namespace std;

typedef unsigned long long ull;
typedef long long ll;
const int N=2e5+5;

inline bool isdigit(char c){
	return c<='9'&&c>='0';
}

template<typename T>void read(T &x){
	x=0;int f=1;
	char c=getchar();
	for(;!isdigit(c);c=getchar())if(f=='-')f=-1;
	for(;isdigit(c);c=getchar())x=(x<<1)+(x<<3)+c-'0';
	x*=f;
}

int n,m;
bool on[N];
int con[N][2],cnt[N],fa[N];

inline int find(int x){
	if(fa[x]==x)return x;
	return fa[x]=find(fa[x]);
}

inline void merge(int x,int y){
	fa[find(x)]=find(y);
}

int main()
{
	read(n),read(m);
	Rep(i,1,2*m)fa[i]=i;
	Rep(i,1,n)read(on[i]);
	Rep(i,1,m){
		int t;
		read(t);
		while(t--){
			int ttt;
			read(ttt);
			con[ttt][cnt[ttt]++]=i;
		}
	}
	Rep(i,1,n){
		if(!on[i])merge(con[i][0],con[i][1]+m),merge(con[i][1],con[i][0]+m);
		else merge(con[i][0],con[i][1]),merge(con[i][0]+m,con[i][1]+m);
	}
	Rep(i,1,m)if(find(i)==find(i+m))return puts("NO"),0;
	puts("YES");
	return 0;
}

另一道比较难的模板题

P4782 【模板】2-SAT 问题

就是普通的做然后简单的做一下答案

因为这道题是单向边,所以我们要用tarjan来做,如果0,1状态在同一个强联通分量里面,那么就是IMPOSSIBLE

在统计答案的时候,我们注意到,强联通分量的color序是逆拓扑排序的,所以我们只要判断color[i]和color[i+n]的大小即可

上代码:

# include <cstdio>
# include <algorithm>
# include <cstring>
# include <cmath>
# include <climits>
# define Rep(i,a,b) for(int i=a;i<=b;i++)
# define _Rep(i,a,b) for(int i=a;i>=b;i--)

using namespace std;

const int N=2e6+5;

typedef long long ll;

inline bool isdigit(char c){
	return c<='9'&&c>='0';
}

template<typename T>void read(T &x){
	x=0;int f=1;
	char c=getchar();
	for(;!isdigit(c);c=getchar())if(c=='-')f=-1;
	for(;isdigit(c);c=getchar())x=(x<<1)+(x<<3)+c-'0';
	x*=f;
}

int n,m;
int head[N],cnt;
int dfn[N],low[N],dfsxu,stack[N],top,color[N],sum;
bool vis[N];

struct Edge{
	int to,next;
}e[N<<1];

inline void add(int x,int y){
	e[++cnt]=(Edge){y,head[x]},head[x]=cnt;
}

inline void tarjan(int u){
	dfn[u]=++dfsxu;
	low[u]=dfsxu;
	stack[++top]=u;
	vis[u]=true;
	for(int i=head[u];~i;i=e[i].next){
		int v=e[i].to;
		if(!dfn[v]){
			tarjan(v);
			low[u]=min(low[u],low[v]);
		}
		else if(vis[v])low[u]=min(low[u],low[v]);
	}
	if(low[u]==dfn[u]){
		color[u]=++sum;
		vis[u]=false;
		while(stack[top]!=u){
			color[stack[top]]=sum;
			vis[stack[top--]]=false;
		}
		top--;
	}
}

int main()
{
	read(n),read(m);
	memset(head,-1,sizeof(head));
	Rep(i,1,m){
		int x,y,z,a;
		read(x),read(y),read(z),read(a);
		add(x+(1^y)*n,z+a*n);
		add(z+(1^a)*n,x+y*n);
	}
	Rep(i,1,2*n)if(!dfn[i])tarjan(i);
	Rep(i,1,n)if(color[i]==color[i+n])return puts("IMPOSSIBLE"),0;
	puts("POSSIBLE");
	Rep(i,1,n)printf("%d ",color[i]>color[i+n]);
	puts("");
	return 0;
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值