PS:如果读过题了可以跳过题目描述直接到题解部分
题目
题目描述
满汉全席是中国最丰盛的宴客菜肴,有许多种不同的材料透过满族或是汉族的料理方式,呈现在数量繁多的菜色之中。由于菜色众多而繁杂,只有极少数博学多闻技艺高超的厨师能够做出满汉全席,而能够烹饪出经过专家认证的满汉全席,也是中国厨师最大的荣誉之一。世界满汉全席协会是由能够料理满汉全席的专家厨师们所组成,而他们之间还细分为许多不同等级的厨师。
为了招收新进的厨师进入世界满汉全席协会,将于近日举办满汉全席大赛,协会派遣许多会员当作评审员,为的就是要在参赛的厨师之中,找到满汉界的明日之星。
大会的规则如下:每位参赛的选手可以得到 n 种材料,选手可以自由选择用满式或是汉式料理将材料当成菜肴。
大会的评审制度是:共有 m 位评审员分别把关。每一位评审员对于满汉全席有各自独特的见解,但基本见解是,要有两样菜色作为满汉全席的标志。如某评审认为,如果没有汉式东坡肉跟满式的涮羊肉锅,就不能算是满汉全席。但避免过于有主见的审核,大会规定一个评审员除非是在认为必备的两样菜色都没有做出来的状况下,才能淘汰一位选手,否则不能淘汰一位选手。
换句话说,只要参赛者能在这两种材料的做法中,其中一个符合评审的喜好即可通过该评审的审查。如材料有猪肉,羊肉和牛肉时,有四位评审员的喜好如下表:
如参赛者甲做出满式猪肉,满式羊肉和满式牛肉料理,他将无法满足评审三的要求,无法通过评审。而參赛者乙做出汉式猪肉,满式羊肉和满式牛肉料理,就可以满足所有评审的要求。
但大会后来发现,在这样的制度下如果材料选择跟派出的评审员没有特别安排好的话,所有的参赛者最多只能通过部分评审员的审查而不是全部,所以可能会发生没有人通过考核的情形。
如有四个评审员喜好如下表时,则不论参赛者采取什么样的做法,都不可能通过所有评审的考核:
评审一 | 评审二 | 评审三 | 评审四 |
---|---|---|---|
满式羊肉 | 满式猪肉 | 汉式羊肉 | 汉式羊肉 |
汉式猪肉 | 满式羊肉 | 汉式猪肉 | 满式猪肉 |
所以大会希望有人能写一个程序来判断,所选出的 m 位评审,会不会发生没有人能通过考核的窘境,以便协会组织合适的评审团。
输入格式
第一行包含一个数字 K(1≤K≤50),代表测试文件包含了 K 组数据。
每一组测试数据的第一行包含两个数字 n 跟 m(n≤100,m≤1000),代表有 n 种材料,m 位评审员。
为方便起见,舍弃做法的中文名称而给予编号,编号分别从 1 到 n。
接下来的 m 行,每行都代表对应的评审员所拥有的两个喜好,每个喜好由一个英文字母跟一个数字代表,如 m1 代表这个评审喜欢第 1 个材料透过满式料理做出来的菜,而 h2 代表这个评审员喜欢第 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
看这道题的题解之前,我建议没有学过tarjan的人先大致学习一下强连通分量的 tarjan 算法。
我们仔细阅读题目会发现,虽然评委很多,但每个评委的要求都只有两道菜,也就是说,如果其中一道没有满足,就必须要满足另一道,而正好每种材料又只有两种做法,所以我们可以反过来想,假如一个评委喜欢 h1 和 m2 ,如果我们已经确定 1 的做法为 m,那 2 的做法只有为 m 时才能满足这个评委,反之,当我们已经确定 2 的做法为 h 时,那么 1 的做法就必须为 h。
这个想明白了以后,我们就可以开始建边了。对于刚才那个例子,我们假设第 i 种材料的满式做法为 i ,汉式做法为 i+n ,那么我们需要建两条边,一条从 1 指向 2,一条从 2+n 指向 1+n。建这个边的时候一定要非常注意逻辑顺序,因为建的是单向边,如果建反了,就会影响后面的搜索。
然后直接用 tarjan 求 i 和 i+n 有没有在同一个强联通里面,如果在的话,输出“BAD”,都不在的话就输出“GOOD”。
这个看起来其实和并查集很像,但实践证明,这道题不能用并查集做,主要是因为并查集相当于是个无向图,或者说,并查集的复杂度太高了。(反正我是没能用并查集做出来)
最后,记得每个数据跑完以后要把数组归零!!!
代码实现
//洛谷 P4171 [JSOI2010] 满汉全席
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int k;
int n,m;
int x1,x2;
char c1,c2;
int cnt;
int head[220];
int dfn[220];
int low[220];
int s[220];
int instack[220];
int scc[220];
int dfncnt;
int sc;
int tp;
bool b;
struct tree{
int v,nex;
}a[2200];
void build(int u,int v){//建图
a[++cnt].v=v;
a[cnt].nex=head[u];
head[u]=cnt;
}
int tarjan(int u){//求强联通分量
dfn[u]=low[u]=++dfncnt;
s[++tp]=u;
instack[u]=1;
for(int i=head[u];i;i=a[i].nex){
int v=a[i].v;
if(!dfn[v]){
if(!tarjan(v)){
return 0;
}
low[u]=min(low[v],low[u]);
}
else if(instack[v]){
low[u]=min(dfn[v],low[u]);
}
}
if(dfn[u]==low[u]){
while(s[tp]!=u){
if(s[tp]==u+n||s[tp]==u-n){//这个地方我没有保存强联通的编号而是直接返回一个值
return 0;//效果和先保存后面再单独判断是一样的
}
instack[s[tp]]=0;
--tp;
}
instack[s[tp]]=0;
--tp;
}
return 1;
}
int main(){
scanf("%d",&k);
while(k--){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;++i){
cin>>c1>>x1>>c2>>x2;
if(c1=='m'){
if(c2=='m'){
build(x1+n,x2);
build(x2+n,x1);
}
else{
build(x1+n,x2+n);
build(x2,x1);
}
}
else{
if(c2=='m'){
build(x1,x2);
build(x2+n,x1+n);
}
else{
build(x1,x2+n);
build(x2,x1+n);
}
}
}
for(int i=1;i<=(n<<1);++i){
if(!dfn[i]){
if(!tarjan(i)){
b=1;
break;
}
}
}
if(b){
printf("BAD\n");
}
else{
printf("GOOD\n");
}
memset(head,0,sizeof(head));
memset(dfn,0,sizeof(dfn));
memset(low,0,sizeof(low));
memset(instack,0,sizeof(instack));
cnt=0;
tp=0;
dfncnt=0;
b=0;
}
}