• 博客园logo
  • 会员
  • 众包
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • HarmonyOS
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录

llwwll

  • 博客园
  • 联系
  • 订阅
  • 管理

公告

View Post

2-sat问题

什么是2-sat问题

学习资料

如给定\(n\)个变量,可以赋值为\(A{_{0} }\)或\(A{_{1} }\),现给出\(m\)个限制,询问是否存在合法赋值使得\(m\)个询问均成立
很抽象吧

举个例子

现在有\(n\)个水果,有\(m\)个人,他们每一个人会有两种需求,他们可能会需要第\(i\)种,也可能讨厌第\(j\)种

小A 讨厌\(a\) 喜欢\(b\)
小B 喜欢\(a\) 喜欢\(b\)
小C 讨厌\(a\) 讨厌\(b\)

显然选择\(b\)满足他们

我们不妨把两种要求设为 a,b,,变量前加 ¬ 表示「不」,即「假」。上述条件翻译成布尔方程即:\((¬ a∨b)\quad(a∨b)\quad(¬a∨¬b)\)

怎么求解 2-SAT 问题?

使用强连通分量

对于每个变量 \(x\),我们建立两个点:\(x\quad¬x\), 分别表示变量 \(x\) 取 \(true\) 和取 \(false\)。所以,图的节点个数是两倍的变量个数。在存储方式上,可以给第 \(i\) 个变量标号为 \(i\),其对应的反值标号为 \(i+n\)。对于每个同学的要求 \((a∨b)\),转换为 \(¬a→b∧¬b→a\)。对于这个式子,可以理解为:「若 \(a\)不选则 \(b\) 必选,若 \(b\) 不选则 \(a\) 必选」然后按照箭头的方向建有向边就好了。综上,我们这样对上面的方程建图:

原式 \(\quad\quad\)建图
\((¬ a∨b)\quad a→b∧¬b→¬a\)
\((a∨b)\quad ¬a→b∧¬b→a\)
\((¬a∨¬b)\quad a→¬b∧b→¬a\)
说白了就是其中一个若不满足则另一个必须满足要求
建出来就长这样
image
那么我们通过一个强连通分量就可以判断矛盾情况是否可以相互到达

有了前置芝士,我们就可以看一道例题了

满汉全席

显然,\(n\)种肉相当于分别赋值为 满 或 汉。
对于\(j\) 评委(如汉式牛肉或满式羊肉)
就可以转化为 满式牛肉--->满式羊肉与汉式羊肉--->汉式牛肉
( 类比上述\((¬ a∨b)\) )

建好图后,开始\(Tarjan\)求强连通分量。可知,同一强联通分量中的菜不能包括同种肉的两种做法,否则,答案为\(BAD\)。所以求出强连通分量后,求可以立刻得出答案。
image
(样例一的建图)

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define maxn 100010 
using namespace std;
int head[maxn],low[maxn],dfs[maxn],Gro[maxn];
int vis[maxn];
int f,tot,cnt,group,num1,num2;
stack<int> q;
struct node{
	int v,nex;}re[maxn];
void add(int u,int v)
{
	++tot;
	re[tot].v=v; re[tot].nex=head[u];
	head[u]=tot;
}
void init()
{
	memset(head,0,sizeof head);
	memset(Gro,0,sizeof Gro);
	memset(dfs,0,sizeof dfs);
	memset(vis,0,sizeof vis);
	memset(low,0,sizeof low);
	f=tot=cnt=group=0;
	num1=num2=0;
}
void tarjian(int x)
{
	low[x]=dfs[x]=++cnt;
	q.push(x);  vis[x]=1;
	for(int i=head[x];i;i=re[i].nex)
	{
		int v=re[i].v;
		if(!dfs[v])
		{
			tarjian(v);
			low[x]=min(low[x],low[v]);
		}
		else if(vis[v])
		low[x]=min(low[x],dfs[v]);
	}
	if(low[x]==dfs[x])
	{
		int y;
		group++;
		do{
			y=q.top();
			q.pop(); vis[y]=0;
			Gro[y]=group;
		}
		while(y!=x);
	}
}

int T,n,m,k;
char s1[10],s2[10];
signed main()
{
	scanf("%lld",&T);
	while(T--)
	{
		scanf("%lld%lld",&n,&m);
		init();
		for(int i=1;i<=m;i++)// m  1~n     h  n+1~2n
		{
			cin>>s1>>s2;
			k=1,num1=0;
			while(s1[k]>='0'&&s1[k]<='9') num1=num1*10+s1[k++]-'0';
			k=1,num2=0;
			while(s2[k]>='0'&&s2[k]<='9') num2=num2*10+s2[k++]-'0';
			if(s1[0]=='m')
			{
				if(s2[0]=='m') add(num1+n,num2),add(num2+n,num1);//分别是将h1连m2  同时将h2连m1 
				if(s2[0]=='h') add(num1+n,num2+n),add(num2,num1);//将h1连h2   同时将m2连m1 
			}
			if(s1[0]=='h')
			{
				if(s2[0]=='m') add(num1,num2),add(num2+n,num1+n);
				if(s2[0]=='h') add(num1,num2+n),add(num2,num1+n);
			}
		}
		
		for(int i=1;i<=2*n;i++)
		{
			if(!dfs[i]) tarjian(i);
		}
		for(int i=1;i<=n;i++)
		{
			if(Gro[i]==Gro[i+n]) 
			{
				f=1; break;
			}
		}
		
		if(f==1) printf("BAD\n");
		else printf("GOOD\n");
	}
	return 0;
}
/*
2
3 4 
m3 h1 
m1 m2 
h1 h3 
h3 m2 
2 4 
h1 m2 
m2 m1 
h1 h2 
m1 h2

*/

现在我们有了判断是否有解的方法
那么如何解决方案情况呢

这就需要认识到\(Tarjan\)与\(DFS\)序的性质了;
显然,在\(Tarjan\)进行\(DFS\)时,若遇到一个强连通分量(未到达根节点记为\(BIG\quad scc\))
则会继续下去只到最后一个\(scc\)(记为\(lil\quad scc\))
这时候\(Tarjan\)才将最新进入栈的\(scc\)进行标号;
也就是说\(DFS\)序越靠后,\(Tajan\)的缩点标号就越靠前
由\(DFS\)的性质可得,原先点(\(BIG\))可以通过某条路径到达(\(lil\))
而\(lil\)无法到达\(BIG\)
那么在解决方案中,就说明选择\(lil\)方案就不会发生\(BIG\)情况
反之选择\(BIG\) 就可能发生\(lil\)
由此可得\(\quad\) 选择缩点序小的点会更优。
举个例子 和平委员会

点击查看代码
#include<bits/stdc++.h>
#define maxn 100010
#define int long long
using namespace std; 
int head[maxn],low[maxn],dfs[maxn],Gro[maxn];
int ver[maxn],nex[maxn];
int vis[maxn];
int f,tot,cnt,group;
stack<int> q;
void add(int u,int v)
{
	ver[++tot]=v; nex[tot]=head[u];head[u]=tot;
}
int one(int x)
{
	if(x&1) return x+1;
	else return x-1;
}
void tarjan(int x)
{
	dfs[x]=low[x]=++cnt;
	q.push(x); vis[x]=1;
	for(int i=head[x];i;i=nex[i])
	{
		int v=ver[i];
		if(!dfs[v])
		{
			cout<<x<<"----"<<v<<endl;
			tarjan(v);
			low[x]=min(low[x],low[v]);
		}
		else
		{
			if(vis[v])
			{
				cout<<"find "<<v<<"---"<<x<<endl;
				low[x]=min(low[x],dfs[v]);
			}
		}
	}
	if(dfs[x]==low[x])
	{
		cout<<"degin!!!  "<<group+1<<"     "<<x<<"--";
		int y;group++;
		do
		{
			y=q.top();Gro[y]=group;
			q.pop(); vis[y]=0;
			cout<<y<<"--";
		}while(x!=y);
	}
}
int n,m,u,v;
signed main()
{
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=m;i++)
	{
		scanf("%lld%lld",&u,&v);
		add(u,one(v));
		add(v,one(u));
	}
	/*
	建边的本质是有u就一定去选择v 
	*/
	for(int i=1;i<=2*n;i++)
	{
		if(!dfs[i]) tarjan(i);
	}
	for(int i=1;i<=2*n;i++)
	{
		cout<<Gro[i]<<" ";
	}
	for(int i=1;i<=2*n;i+=2)
	{
		if(Gro[i]==Gro[i+1]) 
		{
			printf("NIE\n");
			return 0;
		}
	}
	
//	cout<<endl<<endl;;
	for(int i=1;i<=2*n;i+=2)
	{
		if(Gro[i]<Gro[i+1]) 
		{
			/*
			所以这句话:if(Gro[i]<Gro[i+1])	,意思就是:如果i+1到i有单向的连边,那么我们肯定是选i的呀,因为如果选了i+1,就要选i,不就自己把自己坑死了吗
			*/
			printf("%lld\n",i);
		}
		else printf("%lld\n",i+1);
		/*由于 Tarjan 强连通的 DFS 特性,给每个强连通分量的标号其实就是反向的拓扑序(标号为 k 的强连通分量会比 k+1 更早退栈,即先进入栈中)。
所以选标号小的强连通分量会影响到更少的点。 所以就有了输出答案的代码:
		*/
		/*显然Tarjan是以DFS序进行的,在DFS过程中遇到第一个强连通分量说明一定是从之前的点到达的,直到最后一个scc,这时候在本题中,如果出现后来的scc可以到达
自己同类型委员会,此时就是矛盾的,因为DFS序就一定可以从原先点到此点,scc又保证此点一定能到原先点,故矛盾;
		那么答案输出也是同理,DFS越深,拓扑序越大,而scc编号越小。 如果我们选择scc编号大的作为答案,编号大说明此scc(记为大scc)之后还有其他的scc(记为小scc),
就可能出现 大scc中有同组委员,且小scc也有同组委员,那么大scc就可以通过DFS序到达小scc,相当于选了大scc就会依赖小scc。同组委员就都选择了,故矛盾; 
 
		*/
	}
	return 0;
}
/*
2 4
1 3
1 4
2 3
2 4

2 3
1 4
2 3

5 6
1 3
1 4
3 5
2 6
7 9 
6 8

*/

小练习

建边技巧

很好判断的建边方式,很好判断的可行性
然后就T飞了
显然建边时\(n^2\)的效率是无法接受的
那么就有了前后缀建边
以空间换时间
众所周知 2-sat 可以 以\(i,i+n\)表示选或不选
但是会有\(n^2\)的效率
引入\(i+2n,i+3n\)
我们先建立若干个辅助点,让每一个辅助点都和最终点相连。然后,按照从前往后的顺序连接辅助点。这样,只需要连一条边,就得到了一个点的 后缀。
image

(如上图,点 22 一条边就连到了 22 之后的所有对应点)

反之,如果按照从后往前的顺序连接辅助点,就得到了前缀。必须要建两排辅助点。

最终建的图如下:
image

点击查看代码
#include<bits/stdc++.h>
#define maxn 4000010
using namespace std;
int ver[maxn<<5],nex[maxn<<5],head[maxn<<5],tot;
int cnt,dfs[maxn],low[maxn],vis[maxn],Gro[maxn],group;
int st[maxn];
int n,m,k,u,v,a[maxn],num,top;
inline int read() { int x = 0, f = 0; char c = getchar(); while (!isdigit(c)) f |= c == '-', c = getchar(); while (isdigit(c)) x = (x << 1) + (x << 3) + (c ^ 48), c = getchar(); return f ? -x : x; }
void add(int u,int v)
{
	ver[++tot]=v,nex[tot]=head[u];head[u]=tot;
}
void tarjan(int x)
{
	dfs[x]=low[x]=++cnt;
	st[++top]=x; vis[x]=1;
	for(int i=head[x];i;i=nex[i])
	{
		int v=ver[i];
		if(!dfs[v])
		{
			tarjan(v);
			low[x]=min(low[x],low[v]);
		}
		else
		if(vis[v])
		{
			low[x]=min(low[x],dfs[v]);
		}
	}
	if(dfs[x]==low[x])
	{
		int y; group++;
		do
		{
			y=st[top]; top--;
			Gro[y]=group; vis[y]=0;
		}while(x!=y);
	}
}
int main()
{
	n=read();
	m=read();
	k=read();
	for(int i=1;i<=m;i++)
	{
		u=read();
		v=read();
		add(u+n,v);
		add(v+n,u);
	}
	int nn=(n<<1),nnn=3*n;
	for(int i=1;i<=k;i++)
	{
		num=read();
		
		for(int j=1;j<=num;j++)
		{
			a[j]=read();
			add(a[j]+nn,a[j]+n);
			add(a[j]+nnn,a[j]+n);
		}
		for(int j=1;j<num;j++)
		{
			add(a[j]+nnn,a[j+1]+nnn);
			add(a[j+1]+nn,a[j]+nn);
			add(a[j],a[j+1]+nnn);
			add(a[j+1],a[j]+nn);
		}
	}
	for(int i=1;i<=2*n;i++)
		if(!dfs[i]) tarjan(i);
	for(int i=1;i<=n;i++)
	{
		if(Gro[i]==Gro[i+n])
		{
			puts("NIE");
			return 0;
		}
	}
	puts("TAK");
	return 0;
}

posted on 2022-09-21 18:58  llwwll  阅读(51)  评论(0)    收藏  举报

刷新页面返回顶部
 
博客园  ©  2004-2025
浙公网安备 33010602011771号 浙ICP备2021040463号-3