2 条题解
-
0
为了方便你构建 OJ 测试数据,我编写了这个 C++ 数据生成器。它能够自动生成 10 组涵盖各种边界情况(如起点终点相同、终点不在库中、路径不通等)的测试数据,并利用内置的 BFS 求解器同步生成标准答案。
1. 数据生成器设计逻辑
- 求解逻辑:生成器内置了基于
std::queue的非递归 BFS 算法,确保.out文件的准确性。 - 覆盖情况:
- T1:起点和终点完全相同()。
- T2:只需 1 步即可到达()。
- T3:终点不在
bank中(预期结果-1)。 - T4:
bank为空,起点终点不同(预期结果-1)。 - T5-T6:存在较长路径,且 达到上限。
- T7:终点在
bank中但路径由于字符限制不通。 - T8-T10:随机生成的复杂变化路径。
- 安全性:采用迭代 BFS 逻辑,避免了递归导致的栈溢出风险。
2. 数据生成器源代码 (C++14)
#include <iostream> #include <fstream> #include <string> #include <vector> #include <queue> #include <set> #include <algorithm> #include <random> using namespace std; // --- 内置求解器:生成标准答案 --- int solve_mutation(string start, string end, vector<string> bank) { if (start == end) return 0; set<string> bank_set(bank.begin(), bank.end()); if (bank_set.find(end) == bank_set.end()) return -1; queue<pair<string, int>> q; q.push({start, 0}); set<string> vis; vis.insert(start); char genes[] = {'A', 'C', 'G', 'T'}; while (!q.empty()) { string curr = q.front().first; int step = q.front().second; q.pop(); for (int i = 0; i < 8; ++i) { char old = curr[i]; for (char g : genes) { if (g == old) continue; curr[i] = g; if (curr == end) return step + 1; if (bank_set.count(curr) && !vis.count(curr)) { vis.insert(curr); q.push({curr, step + 1}); } } curr[i] = old; } } return -1; } // --- 随机字符串工具 --- string gen_random_gene(mt19937 &rng) { string g = "ACGT"; string res = ""; for(int i=0; i<8; ++i) res += g[rng() % 4]; return res; } // 修改一个字符 string mutate_one(string s, mt19937 &rng) { int pos = rng() % 8; string g = "ACGT"; char old = s[pos]; char next_c = old; while(next_c == old) next_c = g[rng() % 4]; s[pos] = next_c; return s; } // --- 生成测试点 --- void create_test(int t) { string in_name = to_string(t) + ".in"; string out_name = to_string(t) + ".out"; ofstream fin(in_name); mt19937 rng(t + 1337); string start, end; vector<string> bank; int m = 0; if (t == 1) { // 边界:相同 start = "AAAAACCC"; end = "AAAAACCC"; m = 0; } else if (t == 2) { // 1步 start = "AACCGGTT"; end = "AACCGGTA"; m = 1; bank.push_back(end); } else if (t == 3) { // 终点不在库 start = "AACCGGTT"; end = "AAACGGTA"; m = 2; bank.push_back("AACCGGTA"); bank.push_back("AACCGCTA"); } else if (t == 4) { // 库为空 start = "AAAAAAAA"; end = "TTTTTTTT"; m = 0; } else if (t == 5) { // 较长路径 start = "AAAAAAAA"; string curr = start; for(int i=0; i<8; ++i) { curr = mutate_one(curr, rng); bank.push_back(curr); } end = bank.back(); m = bank.size(); } else if (t == 6) { // 终点在库但断路 start = "AAAAAAAA"; end = "TTTTTTTT"; m = 2; bank.push_back("AAAAAAAT"); bank.push_back(end); } else if (t == 7) { // 冗余基因库 start = "GCTATATA"; string next = mutate_one(start, rng); bank.push_back(next); for(int i=0; i<9; ++i) bank.push_back(gen_random_gene(rng)); end = next; m = bank.size(); } else { // 随机复杂 start = gen_random_gene(rng); string curr = start; int steps = rand() % 5 + 3; for(int i=0; i<steps; i++) { curr = mutate_one(curr, rng); bank.push_back(curr); } end = curr; while(bank.size() < 10) bank.push_back(gen_random_gene(rng)); m = bank.size(); } // 写入输入 fin << start << "\n" << end << "\n" << m << "\n"; for(auto &s : bank) fin << s << "\n"; fin.close(); // 生成答案 int ans = solve_mutation(start, end, bank); ofstream fout(out_name); fout << ans << endl; fout.close(); } int main() { for (int i = 1; i <= 10; ++i) { create_test(i); cout << "Testcase " << i << " generated." << endl; } return 0; }3. 测试数据说明
测试点 规模 特征描述 预期结果 考查点 1 0 起点等于终点 0 最小边界情况 2 1 只有一步变化 1 基础 BFS 扩散 3 2 终点不在 bank-1 库约束判断 4 0 库为空且起点不同 无效路径处理 5 8 连续 8 步变化 8 搜索深度与路径还原 6 2 终点在库但无法通过单步到达 -1 图的连通性 7 10 包含随机冗余基因 1 搜索干扰项排除 8-10 随机生成的混合路径 变化 综合稳健性测试 4. 教练的优化建议
- 文件极小化:本题每个输入文件大约只有 100~200 字节(3行+10行字符串),10 个文件总共不超过 2KB。这在 OJ 上上传和下载极快,完全满足“理想值 2MB 内”的要求。
- 不需除法:本算法完全基于字符串比对和队列操作,不存在除以 0 的风险。
- 常数优化:虽然 很小,但在实际 NOI 竞赛中,如果要处理更长的基因序列(例如长度为 100),建议使用
unordered_set替代set,并将基因序列进行四进制状态压缩(A:0, C:1, G:2, T:3),将 8 位字符串压入一个int中,这样比对速度会提升数倍。 - 去重:在生成随机数据或求解时,必须使用
vis(或set)去重,否则在循环变化的基因序列中会陷入死循环。
- 求解逻辑:生成器内置了基于
-
0
在解决“最短路径”或“最少步数”问题时,宽度优先搜索 (BFS) 是最标准且最高效的工具。由于本题基因库规模极小(),逻辑的正确性和边界处理比极端性能优化更重要。
版本一:隐式图 BFS(最直观版本)
思路: 不预先建图,而是在搜索过程中,通过修改当前字符串的每一个位置来“试探”下一步。利用
std::set快速判断新基因是否在基因库中。#include <iostream> #include <string> #include <queue> #include <set> using namespace std; // 基因包含的四个碱基 const char genes[] = {'A', 'C', 'G', 'T'}; int main() { string start, end; int m; if (!(cin >> start >> end >> m)) return 0; set<string> bank; for (int i = 0; i < m; i++) { string s; cin >> s; bank.insert(s); } // 易错点:如果目标基因不在库中,根据题意无法到达 if (bank.find(end) == bank.end()) { // 特判:如果起点和终点相同,步数为0(即使终点不在库中) if (start == end) cout << 0 << endl; else cout << -1 << endl; return 0; } // BFS 队列:存储 <当前基因序列, 当前步数> queue<pair<string, int>> q; q.push({start, 0}); // 访问标记数组,防止死循环 set<string> visited; visited.insert(start); while (!q.empty()) { string curr = q.front().first; int step = q.front().second; q.pop(); if (curr == end) { cout << step << endl; return 0; } // 尝试修改 8 个位置中的每一个 for (int i = 0; i < 8; i++) { char old_char = curr[i]; for (char g : genes) { if (g == old_char) continue; curr[i] = g; // 修改 // 如果修改后的基因在库中,且没走过 if (bank.count(curr) && !visited.count(curr)) { visited.insert(curr); q.push({curr, step + 1}); } } curr[i] = old_char; // 回溯,还原字符串以便测试下一个位置 } } cout << -1 << endl; return 0; }
版本二:显式图建图 + BFS(竞赛进阶版)
思路: 将
startGene和bank中的所有基因看作图的节点。预先计算两两之间是否只差一个字符,若是则连边。然后运行 BFS。这种方法在 较大时逻辑更清晰。#include <iostream> #include <vector> #include <string> #include <queue> using namespace std; // 判断两个基因序列是否只差一个字符 bool can_mutate(const string& a, const string& b) { int diff = 0; for (int i = 0; i < 8; i++) { if (a[i] != b[i]) diff++; } return diff == 1; } int main() { string start, end; int m; cin >> start >> end >> m; vector<string> nodes; nodes.push_back(start); int target_idx = -1; for (int i = 0; i < m; i++) { string s; cin >> s; if (s == start) continue; // 避免重复 nodes.push_back(s); if (s == end) target_idx = nodes.size() - 1; } // 如果终点不在 bank 中 if (target_idx == -1) { if (start == end) cout << 0 << endl; else cout << -1 << endl; return 0; } // 1. 建图:邻接表 int n = nodes.size(); vector<int> adj[20]; // m <= 10, 节点数很小 for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { if (can_mutate(nodes[i], nodes[j])) { adj[i].push_back(j); adj[j].push_back(i); } } } // 2. BFS queue<pair<int, int>> q; q.push({0, 0}); // 从 nodes[0] (即 startGene) 开始 vector<bool> vis(n, false); vis[0] = true; while (!q.empty()) { int u = q.front().first; int step = q.front().second; q.pop(); if (u == target_idx) { cout << step << endl; return 0; } for (int v : adj[u]) { if (!vis[v]) { vis[v] = true; q.push({v, step + 1}); } } } cout << -1 << endl; return 0; }
版本三:状态压缩 + BFS(最优常数版)
思路: 在 NOI 中,字符串操作相对较慢。由于碱基只有 4 种,可以用 2 位二进制表示(A:00, C:01, G:10, T:11)。长度为 8 的基因序列可以用 16 位整数(
int)表示。这能极大地优化空间和比对速度。#include <iostream> #include <string> #include <queue> #include <unordered_map> using namespace std; // 将字符转换为 2位二进制 int charToBits(char c) { if (c == 'A') return 0; if (c == 'C') return 1; if (c == 'G') return 2; return 3; // 'T' } // 将 8位基因转为 16位 int int compress(string s) { int res = 0; for (char c : s) { res = (res << 2) | charToBits(c); } return res; } int main() { string s1, s2; int m; if (!(cin >> s1 >> s2 >> m)) return 0; int start = compress(s1), end = compress(s2); unordered_map<int, bool> is_bank; for (int i = 0; i < m; i++) { string s; cin >> s; is_bank[compress(s)] = true; } if (start == end) { cout << 0 << endl; return 0; } if (is_bank.find(end) == is_bank.end()) { cout << -1 << endl; return 0; } queue<pair<int, int>> q; q.push({start, 0}); unordered_map<int, int> vis; // 记录访问及步数 vis[start] = 0; while (!q.empty()) { int curr = q.front().first; int step = q.front().second; q.pop(); if (curr == end) { cout << step << endl; return 0; } // 尝试改变 8 个位置 for (int i = 0; i < 8; i++) { // 提取出当前第 i 个位置的碱基 (从右往左第 i 个 2bits) int shift = i * 2; int old_bits = (curr >> shift) & 3; for (int b = 0; b < 4; b++) { if (b == old_bits) continue; // 构造新状态:清空原位,填入新位 int next_state = (curr & ~(3 << shift)) | (b << shift); if (is_bank.count(next_state) && !vis.count(next_state)) { vis[next_state] = step + 1; q.push({next_state, step + 1}); } } } } cout << -1 << endl; return 0; }
复杂度分析思考过程
-
时间复杂度:
- 状态总数:受限于基因库大小 。在本题中 ,即便考虑所有可能的组合也只有 种。
- 每个状态的转移:长度 ,碱基种类 ,转移数为 。
- 整体复杂度:。在竞赛中,这意味着即使 放大到 也能轻松通过。
-
空间复杂度:
- 需要存储
bank和visited集合,空间为 。
- 需要存储
时间复杂度优化建议
- 双向 BFS (Bidirectional BFS):如果 很大(如 ),从起点和终点同时开启 BFS,相遇即停止。可以将搜索空间从 降低到 。
- 状态压缩:如版本三所示,将基因序列转为整数,可以避免字符串哈希和频繁的内存分配。
- A 搜索*:利用当前字符串与目标的“差异字符数”作为启发式函数 ,在 极大时更有优势。
读题关键词与易错点
- “最小变化次数”:看到“最小/最短/最少”步数,第一时间反应 BFS。
- “只有出现在 bank 中才有效”:这是典型的图遍历约束条件。
- 的极端情况:此时如果起点不等于终点,必然输出
-1。 - 起点和终点相同:注意初始化步数为
0。 - End 不在 Bank 中:这是最常见的“坑”,必须输出
-1。但在一些特殊题目中,起点也不在 Bank 中,一定要仔细阅读“起始基因序列默认有效”这句话。
-
- 1
信息
- ID
- 19457
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 1
- 已通过
- 1
- 上传者