[JSOI2010] 满汉全席
题目描述
满汉全席是中国最丰盛的宴客菜肴,有许多种不同的材料透过满族或是汉族的料理方式,呈现在数量繁多的菜色之中。由于菜色众多而繁杂,只有极少数博学多闻技艺高超的厨师能够做出满汉全席,而能够烹饪出经过专家认证的满汉全席,也是中国厨师最大的荣誉之一。世界满汉全席协会是由能够料理满汉全席的专家厨师们所组成,而他们之间还细分为许多不同等级的厨师。
为了招收新进的厨师进入世界满汉全席协会,将于近日举办满汉全席大赛,协会派遣许多会员当作评审员,为的就是要在参赛的厨师之中,找到满汉界的明日之星。
大会的规则如下:每位参赛的选手可以得到 n n n 种材料,选手可以自由选择用满式或是汉式料理将材料当成菜肴。
大会的评审制度是:共有 m m m 位评审员分别把关。每一位评审员对于满汉全席有各自独特的见解,但基本见解是,要有两样菜色作为满汉全席的标志。如某评审认为,如果没有汉式东坡肉跟满式的涮羊肉锅,就不能算是满汉全席。但避免过于有主见的审核,大会规定一个评审员除非是在认为必备的两样菜色都没有做出来的状况下,才能淘汰一位选手,否则不能淘汰一位选手。
换句话说,只要参赛者能在这两种材料的做法中,其中一个符合评审的喜好即可通过该评审的审查。如材料有猪肉,羊肉和牛肉时,有四位评审员的喜好如下表:
评审一 评审二 评审三 评审四
满式牛肉 满式猪肉 汉式牛肉 汉式牛肉
汉式猪肉 满式羊肉 汉式猪肉 满式羊肉
如参赛者甲做出满式猪肉,满式羊肉和满式牛肉料理,他将无法满足评审三的要求,无法通过评审。而参赛者乙做出汉式猪肉,满式羊肉和满式牛肉料理,就可以满足所有评审的要求。
但大会后来发现,在这样的制度下如果材料选择跟派出的评审员没有特别安排好的话,所有的参赛者最多只能通过部分评审员的审查而不是全部,所以可能会发生没有人通过考核的情形。
如有四个评审员喜好如下表时,则不论参赛者采取什么样的做法,都不可能通过所有评审的考核:
评审一 评审二 评审三 评审四
满式羊肉 满式猪肉 汉式羊肉 汉式羊肉
汉式猪肉 满式羊肉 汉式猪肉 满式猪肉
所以大会希望有人能写一个程序来判断,所选出的 m m m 位评审,会不会发生没有人能通过考核的窘境,以便协会组织合适的评审团。
输入格式
第一行包含一个数字 K K K( 1 ≤ K ≤ 50 1\le K \le 50 1≤K≤50),代表测试文件包含了 K K K 组数据。
每一组测试数据的第一行包含两个数字 n n n 跟 m m m( n ≤ 100 n≤100 n≤100, m ≤ 1000 m≤1000 m≤1000),代表有 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
(均为大写字母)。
样例 #1
样例输入 #1
2
3 4
m3 h1
m1 m2
h1 h3
h3 m2
2 4
h1 m2
m2 m1
h1 h2
m1 h2
样例输出 #1
GOOD
BAD
题解思路
本题主要在于使用 tarjan
算法解决 2-SAT
问题。如果对 tarjan
算法还不清楚的,我个人推荐 这位博主 的这篇博客,配上他的图讲的还是非常清晰的。
首先,我们需要理解题目意思进行建图。当前需要判断,会不会无论参赛选手选择怎么搭配菜品,都无法满足评委的条件。换个角度就是,是否存在一个做法,能够满足所有评委要求。每个评委都有两个限制条件,参赛选手只需要满足每一个评委的一个要求,即如果不做该评委的第一道菜,那就必须要做评委的第二道菜,这是根据条件建图的关键。
建完图之后,就是根据图去寻找连通分量。如果一个菜的汉式做法和满式做法在同一个连通分量中,那么就不存在做菜方案能够满足所有评委的需求。
题解代码
#include <bits/stdc++.h>
using namespace std;
const int N = 211;
// dfn[i] 数组表示i在dfs中的搜索顺序;
// low[i] 数组表示i所属的强连通的数组,能回溯到哪;
// instack[i] 表示某结点是否在栈中
// belongs 为属于第 i 个强连通的结点
// st 数组模拟栈实现对图的dfs遍历
// scc[i]记录结点i属于哪个强连通分量
int n, m, dfn[N], cnt = 0, low[N], tot = 0, instack[N], st[N], top = 0, scc[N];
vector<int> edges[N];
// vector<int> belongs[N];
void tarjan(int x)
{
dfn[x] = low[x] = ++cnt;
st[++top] = x;
instack[x] = 1;
for (auto y : edges[x])
{
if (!dfn[y])
{
tarjan(y);
low[x] = min(low[x], low[y]);
}
else if (instack[y])
{
low[x] = min(low[x], dfn[y]);
}
}
if (low[x] == dfn[x])
{
++tot;
while (1)
{
int y = st[top--];
instack[y] = 0;
// belongs[tot].push_back(y); //此题不需要记录每个连通分量的元素,只需要知道每个点所在联通分量
scc[y] = tot;
if (x == y)
break;
}
}
}
void clean()
{
cnt = top = tot = 0;
memset(dfn, 0, sizeof(dfn));
memset(low, 0, sizeof(low));
memset(instack, 0, sizeof(instack));
memset(scc, 0, sizeof(scc));
for (register int i(0); i <= n * 2; i++)
{
edges[i].clear(); // 清空本轮所需 vector<int> 对象的元素
}
// for (register int i(0); i <= n * 2; i++)
// {
// belongs[i].clear(); // 清空本轮所需 vector<int> 对象的元素
// }
}
int main()
{
int k;
scanf("%d", &k);
while (k--)
{
// 对于多个测试样例,每次需要清楚上一个样例的数据,以免干扰
clean();
scanf("%d%d", &n, &m);
for (register int i(1); i <= m; ++i)
{
string str1, str2;
cin >> str1 >> str2;
char c1 = str1[0], c2 = str2[0];
int s1 = 0, s2 = 0;
int j = 1;
while (str1[j] >= '0' && str1[j] <= '9')
s1 = s1 * 10 + (str1[j++] - '0');
j = 1;
while (str2[j] >= '0' && str2[j] <= '9')
s2 = s2 * 10 + (str2[j++] - '0');
// printf("%c %d %c %d\n", c1, s1, c2, s2);
// 如果做的是汉式做法,就是前n项,满式做法为后n项
if (c1 == 'h' && c2 == 'h')
{
edges[s1 + n].push_back(s2);
edges[s2 + n].push_back(s1);
}
if (c1 == 'h' && c2 == 'm')
{
edges[s1 + n].push_back(s2 + n);
edges[s2].push_back(s1);
}
if (c1 == 'm' && c2 == 'h')
{
edges[s1].push_back(s2);
edges[s2 + n].push_back(s1 + n);
}
if (c1 == 'm' && c2 == 'm')
{
edges[s1].push_back(s2 + n);
edges[s2].push_back(s1 + n);
}
}
for (register int i(1); i <= n * 2; ++i)
if (!dfn[i])
tarjan(i);
int flag = 1;
for (register int i(1); i <= n; ++i)
{
if (scc[i] == scc[i + n])
{
flag = 0;
printf("BAD\n");
break;
}
}
if (flag)
{
printf("GOOD\n");
}
}
return 0;
}