【洛谷P4171】满汉全席【2-SAT学习小记】

[JSOI2010] 满汉全席

题目描述

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

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

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

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

参赛者做出汉式猪肉,满式羊肉和满式牛肉料理,就可以满足所有评审的要求。

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

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

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

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

输入格式

第一行包含一个数字 K K K 1 ≤ K ≤ 50 1\le K \le 50 1K50),代表测试文件包含了 K K K 组数据。

每一组测试数据的第一行包含两个数字 n n n m m m n ≤ 100 n≤100 n100 m ≤ 1000 m≤1000 m1000),代表有 n n n 种材料, m m m 位评审员。

为方便起见,舍弃做法的中文名称而给予编号,编号分别从 1 1 1 n n n

接下来的 m m m 行,每行都代表对应的评审员所拥有的两个喜好,每个喜好由一个英文字母跟一个数字代表,如 m 1 m1 m1 代表这个评审喜欢第 1 1 1 个材料透过满式料理做出来的菜,而 h 2 h2 h2 代表这个评审员喜欢第 2 2 2 个材料透过汉式料理做出来的菜。

输出格式

每组测试数据输出一行,如果不会发生没有人能通过考核的窘境,输出 GOOD;否则输出 BAD(均为大写字母)。

样例输入

2
3 4
m3 h1
m1 m2
h1 h3
h3 m2
2 4
h1 m2
m2 m1
h1 h2
m1 h2

样例输出

GOOD
BAD

分析

2-SAT

SAT 是适定性(Satisfiability)问题的简称。一般形式为 k - 适定性问题,简称 k-SAT。而当 k>2 时该问题为 NP 完全的。所以我们只研究 k=2 的情况。

在这里插入图片描述

2-SAT可以通过约束条件来建立bool方程,也就是说不选什么能够推出一定要选什么,然后根据“推出关系”,链接有向边,转化为图论问题。

举个栗子:

在这里插入图片描述

2-SAT常用解决方法

Tarjan SCC 缩点

算法考究在建图这点,我们举个例子来讲:
假设有 {a1,a2} 和 {b1,b2} 两对,已知 a1 和 b2 间有矛盾,于是为了方案自洽,由于两者中必须选一个,所以我们就要拉两条有向边 (a1,b1) 和 (b2,a2) 表示选了 a1 则必须选 b1,选了 b2 则必须选 a2 才能够自洽。

然后通过这样子建边我们跑一遍 Tarjan SCC 判断是否有一个集合中的两个元素在同一个 SCC 中,若有则输出不可能,否则输出方案。构造方案只需要把几个不矛盾的 SCC 拼起来就好了。

输出方案时可以通过变量在图中的拓扑序确定该变量的取值。如果变量 x 的拓扑序在 ¬ x \neg x ¬x 之后,那么取 x 值为真。应用到 Tarjan 算法的缩点,即 x 所在 SCC 编号在 ¬ x \neg x ¬x 之前时,取 x 为真。因为 Tarjan 算法求强连通分量时使用了栈,所以 Tarjan 求得的 SCC 编号相当于反拓扑序。

显然地,时间复杂度为 O ( n + m ) O(n+m) O(n+m)

第二种处理方法:爆搜
就是沿着图上一条路径,如果一个点被选择了,那么这条路径以后的点都将被选择,那么,出现不可行的情况就是,存在一个集合中两者都被选择了。

这种一般对于复杂度要求不高,但是输出的构造方案一般有要求,比如字典序最小等。

模板【洛谷P4782】

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<stack>
using namespace std;

struct node
{
	int to,next;
}e[4000010];

int n,m;
int tot,hd[4000010];
int dfn[2000010],col[2000010],low[2000010],timi,cnt;

void add(int x,int y)
{
	e[++tot]=(node){y,hd[x]};
	hd[x]=tot;
}

stack<int> st;
void tarjan(int x)
{
	dfn[x]=low[x]=++timi;
	st.push(x);
	for(int i=hd[x];i;i=e[i].next)
	{
		int v=e[i].to;
		if(!dfn[v])
		{
			tarjan(v);
			low[x]=min(low[x],low[v]);
		}
		else if(!col[v]) low[x]=min(low[x],low[v]);
	}
	if(dfn[x]==low[x])
	{
		col[x]=++cnt;
		while(st.top()!=x)
		{
			col[st.top()]=cnt;
			st.pop();
		}
		st.pop();
	}
}

int main()
{
	cin>>n>>m;
	for(int i=1;i<=m;i++)
	{
		int x,a,y,b;
		scanf("%d%d%d%d",&x,&a,&y,&b);
		if(a==1&&b==1)
		{
			add(x+n,y);
			add(y+n,x);
		}
		else if(a==1&&b==0)
		{
			add(x+n,y+n);
			add(y,x);
		}
		else if(a==0&&b==1)
		{
			add(x,y);
			add(y+n,x+n);
		}
		else if(a==0&&b==0)
		{
			add(x,y+n);
			add(y,x+n);
		}
	}
	for(int i=1;i<=n*2;i++)
	{
		if(!dfn[i]) tarjan(i);
	}
	for(int i=1;i<=n;i++)
	{
		if(col[i]==col[i+n])
		{
			cout<<"IMPOSSIBLE";
			return 0;
		} 
	}
	cout<<"POSSIBLE"<<endl;
	for(int i=1;i<=n;i++)
	{
		if(col[i]<col[i+n]) cout<<1<<' ';//这里是反拓扑序
		else cout<<0<<' ';
	}
	return 0;
}

回到这道题

明显有2-SAT的模型,于是我们根据满和汉的做法来建图(注意字符串的处理),然后tarjan就行,跟上一题神似,注意反拓扑序。

关键在于识别模型,不要误以为是二分图!!

上代码

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<stack>
using namespace std;

struct node
{
	int to,next;
}e[4010];

int t,n,m,ff;
int dfn[1010],low[1010],col[1010],timi,cnt;
int hd[4010],tot;

void add(int x,int y)
{
	e[++tot]=(node){y,hd[x]};
	hd[x]=tot;
}

stack<int> st;
void tarjan(int x)
{
    dfn[x]=low[x]=++timi;
    st.push(x);
    for(int i=hd[x];i;i=e[i].next)
    {
    	int v=e[i].to;
    	if(!dfn[v])
    	{
    		tarjan(v);
    		low[x]=min(low[x],low[v]);
		}
		else if(!col[v]) low[x]=min(low[x],low[v]);
	}
	if(dfn[x]==low[x])
	{
		col[x]=++cnt;
		while(st.top()!=x)
		{
			col[st.top()]=cnt;
			st.pop();
		}
		st.pop();
	}
}

int main()
{
	cin>>t;
	while(t--)
	{
		cin>>n>>m;
		char c1[10],c2[10];
		for(int i=1;i<=m;i++)
		{
			scanf("%s%s",&c1,&c2);
            int a=0,b=0,k=1;
            while(c1[k]>='0'&&c1[k]<='9') a=a*10+c1[k++]-48;
            k=1; 
			while(c2[k]>='0'&&c2[k]<='9') b=b*10+c2[k++]-48;
			if(c1[0]=='m'&&c2[0]=='m')
			{
				add(a,b+n);
				add(b,a+n);
			}
			if(c1[0]=='m'&&c2[0]=='h')
			{
				add(a,b);
				add(b+n,a+n);
			}
			if(c1[0]=='h'&&c2[0]=='m')
			{
				add(a+n,b+n);
				add(b,a);
			}
			if(c1[0]=='h'&&c2[0]=='h')
			{
				add(a+n,b);
				add(b+n,a);
			} 
		}
		for(int i=1;i<=2*n;i++)
		{
			if(!dfn[i]) tarjan(i);
		}
		for(int i=1;i<=n;i++)
		{
			if(col[i]==col[i+n])
			{
				cout<<"BAD"<<endl;
				ff=1;
				break;
			}
		}
		if(!ff) cout<<"GOOD"<<endl;
		
		memset(dfn,0,sizeof(dfn));
		memset(col,0,sizeof(col));
		memset(low,0,sizeof(low));
		timi=0;cnt=0;tot=0;
		memset(hd,0,sizeof(hd));
		ff=0;
	}
	return 0;
}

最后要请jq吃满汉全席,让她拒绝不开心

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值