题目描述
地球上的血型系统由 222 个等位基因决定,有 444 种血型。现考虑一种外星生命,其每个个体有 NNN 位父母、NNN 个等位基因、NNN 种不同的抗原类型(编号为 111 到 NNN)。等位基因可以是这 NNN 种抗原类型中的任意一种,也可以是不表达任何抗原的 “O\\texttt{O}O” 类型(即无抗原)。一个个体的血型由其所有非 O\\texttt{O}O 等位基因对应的抗原集合决定,若所有等位基因均为 O\\texttt{O}O,则血型为空(O\\texttt{O}O 型)。
给定 NNN 位父母的血型(每位父母的血型给出其含有的抗原集合),以及 QQQ 个待查询的血型(每个查询也是一个抗原集合)。对于每个查询,需要判断:是否存在一种方式,从每位父母的等位基因组合中分别选出一个等位基因传递给后代,使得后代的血型恰好等于该查询集合。
输入格式
输入包含多个测试用例,每个测试用例格式如下:
- 第一行包含两个整数 NNN 和 QQQ,分别表示父母个数(也等于等位基因个数和抗原种类数)和查询个数,满足 1≤N≤1001 \\le N \\le 1001≤N≤100,1≤Q≤401 \\le Q \\le 401≤Q≤40。
- 接下来 NNN 行,每行描述一位父母的血型。每行以整数 BBB 开头(0≤B≤N0 \\le B \\le N0≤B≤N),表示该父母具有的抗原种类数,随后 BBB 个互不相同的整数 C1,C2,…,CBC_1, C_2, \\dots, C_BC1,C2,…,CB(1≤Ci≤N1 \\le C_i \\le N1≤Ci≤N),表示抗原类型编号。
- 接下来 QQQ 行,每行描述一个查询血型。格式与父母血型相同:先给出 BBB,再给出 BBB 个抗原编号。
输入以 EOF\\texttt{EOF}EOF 结束。
输出格式
对于每个测试用例,按照查询顺序输出 QQQ 行,每行一个字符 Y 或 N,表示该查询血型是否可能在给定父母的后代中出现。
样例
输入
2 1
2 2 1
1 2
0
3 4
1 1
2 2 3
0
1 3
3 2 1 3
2 1 2
2 3 2
4 3
4 2 1 3 4
4 2 1 3 4
1 1
1 2
1 3
2 2 1
0
输出
N
Y
N
Y
N
Y
N
N
题目分析
本题是一个遗传可行性判定问题。关键点在于,每位父母的等位基因组合中,除了明确的抗原类型外,还可以存在 “O\\texttt{O}O” 类型(即不表达抗原)。因此,一位父母可以传递给后代的非 O\\texttt{O}O 抗原类型,必须属于其血型集合;同时,该父母也可以选择传递 O\\texttt{O}O 类型,相当于不贡献任何抗原。
对于后代来说,它从 NNN 位父母各继承一个等位基因,共 NNN 个等位基因。后代的血型是这些等位基因中所有非 O\\texttt{O}O 类型的集合。因此,查询血型 TTT 可行的充要条件是:存在一种从每位父母中选择一个等位基因的方式,使得所有被选中的非 O\\texttt{O}O 抗原恰好组成集合 TTT。
由于每位父母至多贡献一个非 O\\texttt{O}O 抗原(因为每位父母只传递一个等位基因),且每个抗原只要在集合 TTT 中,就必须被至少一位父母贡献。至于那些没有贡献抗原的父母,他们只需传递 O\\texttt{O}O 类型即可,不会引入额外抗原。因此,问题转化为:能否用 NNN 位父母(每人最多匹配一个抗原)去覆盖查询集合 TTT 中的所有抗原,且每位父母只能匹配其血型中含有的抗原。
这正是二分图匹配问题:左侧是 NNN 位父母,右侧是查询集合 TTT 中的抗原,若父母含有某抗原,则连边,求最大匹配。若最大匹配数等于 ∣T∣|T|∣T∣,则说明查询中每个抗原都能被至少一个父母覆盖,其余父母传递 O\\texttt{O}O 类型,后代血型恰为 TTT。
特别地,当 ∣T∣=0|T| = 0∣T∣=0(即 O\\texttt{O}O 型血)时,所有父母必须都能传递 O\\texttt{O}O 类型。一位父母能否传递 O\\texttt{O}O 类型,取决于其等位基因组合中是否至少有一个位置可以取 O\\texttt{O}O。等价地,若父母的血型集合大小小于 NNN,则其组合中必有至少一个 O\\texttt{O}O 位置,可以传递 O\\texttt{O}O;若血型集合大小等于 NNN,则所有位置都必须填满不同抗原,无法提供 O\\texttt{O}O。因此,O\\texttt{O}O 型血可行当且仅当每位父母的血型大小均小于 NNN。
解题思路
建模为二分图匹配
对于每个查询,我们独立判断:
- 源点 SSS 到每位父母节点,容量为 111,表示每位父母最多贡献一个抗原。
- 每位父母到其血型中的每个抗原节点(父母侧抗原),容量为 111,表示该父母可以选择贡献该抗原。
- 父母侧抗原节点到子代侧相同抗原节点,容量为 111,表示该抗原最多被一个父母用来覆盖。
- 子代侧每个抗原节点到汇点 TTT,容量为 111,表示每个查询抗原只需被覆盖一次。
这里,父母侧抗原和子代侧抗原虽然编号相同,但为了网络结构清晰,我们将其分为两层,中间通过“相同抗原”的边连接。这样做的目的是限制每个抗原被匹配的次数,并且使得匹配关系从父母流经抗原再到汇点,符合二分图匹配的本质。
为什么 O\\texttt{O}O 型血单独处理
当 B=0B = 0B=0 时,右侧没有抗原节点,最大流只能为 000,但 000 是否等于 BBB(即 000)总是成立,这会导致所有父母都能贡献 O\\texttt{O}O 的误判。实际上,若某父母血型大小为 NNN,则它无法贡献 O\\texttt{O}O,因此必须单独检查。
复杂度分析
每个查询需要建图并跑一次最大流(Dinic\\texttt{Dinic}Dinic 算法)。图的大小:左侧 NNN 个父母节点,父母侧抗原数不超过 NNN(因为抗原类型只有 111 到 NNN),子代侧抗原数为 BBB(B≤NB \\le NB≤N),总节点数 O(N)O(N)O(N)。边数:源点到父母 NNN 条,父母到父母侧抗原最多 N2N^2N2 条,父母侧到子代侧最多 N2N^2N2 条,子代侧到汇点 BBB 条,总边数 O(N2)O(N^2)O(N2)。由于 N≤100N \\le 100N≤100,Dinic\\texttt{Dinic}Dinic 每次查询均可在微秒级完成,总查询数 Q≤40Q \\le 40Q≤40,完全可行。
代码实现
// Blood Groups
// UVa ID: 13005
// Verdict: Accepted
// Submission Date: 2026-06-22
// UVa Run Time: 0.340s
//
// 版权所有(C)2026,邱秋。metaphysis # yeah dot net
#include <bits/stdc++.h>
using namespace std;
struct Dinic {
struct Edge { int to, rev, cap; };
vector<vector<Edge>> g;
vector<int> level, it;
Dinic(int n) : g(n), level(n), it(n) {}
void addEdge(int v, int to, int cap) {
if (cap <= 0) return;
Edge a{to, (int)g[to].size(), cap};
Edge b{v, (int)g[v].size(), 0};
g[v].push_back(a);
g[to].push_back(b);
}
bool bfs(int s, int t) {
fill(level.begin(), level.end(), –1);
queue<int> q;
level[s] = 0;
q.push(s);
while (!q.empty()) {
int v = q.front(); q.pop();
for (auto &e : g[v])
if (e.cap > 0 && level[e.to] < 0) {
level[e.to] = level[v] + 1;
q.push(e.to);
}
}
return level[t] >= 0;
}
int dfs(int v, int t, int f) {
if (v == t) return f;
for (int &i = it[v]; i < (int)g[v].size(); ++i) {
Edge &e = g[v][i];
if (e.cap > 0 && level[v] < level[e.to]) {
int d = dfs(e.to, t, min(f, e.cap));
if (d > 0) {
e.cap -= d;
g[e.to][e.rev].cap += d;
return d;
}
}
}
return 0;
}
int maxFlow(int s, int t) {
int flow = 0, INF = 1e9;
while (bfs(s, t)) {
fill(it.begin(), it.end(), 0);
while (true) {
int f = dfs(s, t, INF);
if (!f) break;
flow += f;
}
}
return flow;
}
};
bool feasibleForQuery(const vector<set<int>>& parents, const vector<int>& query) {
int N = parents.size();
int B = query.size();
// 查询为空(O型):所有父母必须能贡献O(即血型大小<N)
if (B == 0) {
for (int i = 0; i < N; ++i)
if ((int)parents[i].size() == N) return false;
return true;
}
// 收集父母侧所有抗原(去重)
set<int> parentSet;
for (int i = 0; i < N; ++i)
for (int x : parents[i]) parentSet.insert(x);
vector<int> parentAntigens(parentSet.begin(), parentSet.end());
int PA = parentAntigens.size();
// 节点编号
int S = 0;
int parentStart = 1;
int parentAntigenStart = parentStart + N;
int childAntigenStart = parentAntigenStart + PA;
int T = childAntigenStart + B;
int V = T + 1;
Dinic din(V);
// 源点 -> 每位父母,容量1
for (int i = 0; i < N; ++i)
din.addEdge(S, parentStart + i, 1);
// 父母 -> 父母侧抗原(若父母拥有该抗原)
for (int i = 0; i < N; ++i)
for (int j = 0; j < PA; ++j)
if (parents[i].count(parentAntigens[j]))
din.addEdge(parentStart + i, parentAntigenStart + j, 1);
// 父母侧抗原 -> 子代侧抗原(相同抗原)
for (int j = 0; j < PA; ++j)
for (int k = 0; k < B; ++k)
if (parentAntigens[j] == query[k])
din.addEdge(parentAntigenStart + j, childAntigenStart + k, 1);
// 子代侧抗原 -> 汇点,容量1(每个抗原只需被覆盖一次)
for (int k = 0; k < B; ++k)
din.addEdge(childAntigenStart + k, T, 1);
int flow = din.maxFlow(S, T);
return flow == B;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N, Q;
while (cin >> N >> Q) {
vector<set<int>> parents(N);
for (int i = 0; i < N; ++i) {
int B;
cin >> B;
for (int j = 0; j < B; ++j) {
int c;
cin >> c;
parents[i].insert(c);
}
}
for (int q = 0; q < Q; ++q) {
int B;
cin >> B;
vector<int> query(B);
for (int i = 0; i < B; ++i) cin >> query[i];
cout << (feasibleForQuery(parents, query) ? 'Y' : 'N') << '\\n';
}
}
return 0;
}
优化实现(匈牙利算法)
// Blood Groups
// UVa ID: 13005
// Verdict: Accepted
// Submission Date: 2026-06-22
// UVa Run Time: 0.200s
//
// 版权所有(C)2026,邱秋。metaphysis # yeah dot net
#include <bits/stdc++.h>
using namespace std;
bool canMatch(const vector<set<int>>& parents, const vector<int>& query) {
int N = parents.size();
int B = query.size();
// 查询为空(O型):所有父母必须能贡献O(即血型大小<N)
if (B == 0) {
for (int i = 0; i < N; ++i)
if ((int)parents[i].size() == N) return false;
return true;
}
// 建立二分图:左部父母(0..N-1),右部查询抗原(0..B-1)
vector<vector<bool>> adj(N, vector<bool>(B, false));
for (int i = 0; i < N; ++i)
for (int j = 0; j < B; ++j)
if (parents[i].count(query[j]))
adj[i][j] = true;
// 匈牙利算法
vector<int> matchR(B, –1);
function<bool(int, vector<bool>&)> dfs = [&](int u, vector<bool>& seen) -> bool {
for (int v = 0; v < B; ++v) {
if (adj[u][v] && !seen[v]) {
seen[v] = true;
if (matchR[v] == –1 || dfs(matchR[v], seen)) {
matchR[v] = u;
return true;
}
}
}
return false;
};
int matches = 0;
for (int u = 0; u < N; ++u) {
vector<bool> seen(B, false);
if (dfs(u, seen)) ++matches;
}
return matches == B;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N, Q;
while (cin >> N >> Q) {
vector<set<int>> parents(N);
for (int i = 0; i < N; ++i) {
int B;
cin >> B;
for (int j = 0; j < B; ++j) {
int c;
cin >> c;
parents[i].insert(c);
}
}
for (int q = 0; q < Q; ++q) {
int B;
cin >> B;
vector<int> query(B);
for (int i = 0; i < B; ++i) cin >> query[i];
cout << (canMatch(parents, query) ? 'Y' : 'N') << '\\n';
}
}
return 0;
}
总结
本题的核心是将遗传可行性问题巧妙地转化为二分图匹配(最大流),而无需显式处理复杂的等位基因组合。关键洞察在于:
- 每位父母至多贡献一个非 O\\texttt{O}O 抗原,而 O\\texttt{O}O 抗原相当于“不匹配”,因此我们只需关注非 O\\texttt{O}O 抗原的覆盖。
- 查询血型中的每个抗原必须被至少一位父母覆盖,多余的父母可以贡献 O\\texttt{O}O。
- 当查询为空时,需特殊判断所有父母是否都能贡献 O\\texttt{O}O。
网络流模型将“父母→抗原→查询抗原”的匹配关系清晰表达,并通过最大流判断是否所有查询抗原均被覆盖。此解法简洁高效,适用于 N≤100N \\le 100N≤100 的规模。在解决此类组合覆盖问题时,将可行性转化为匹配或流问题是常用且有效的手段。



