2 条题解

  • 0
    @ 2026-1-8 17:07:21

    为了方便你构建 OJ 测试数据,我编写了这个 C++ 数据生成器。它能够自动生成 10 组涵盖各种边界情况(如起点终点相同、终点不在库中、路径不通等)的测试数据,并利用内置的 BFS 求解器同步生成标准答案。

    1. 数据生成器设计逻辑

    • 求解逻辑:生成器内置了基于 std::queue 的非递归 BFS 算法,确保 .out 文件的准确性。
    • 覆盖情况
      • T1:起点和终点完全相同(M=0M=0)。
      • T2:只需 1 步即可到达(M=1M=1)。
      • T3:终点不在 bank 中(预期结果 -1)。
      • T4bank 为空,起点终点不同(预期结果 -1)。
      • T5-T6:存在较长路径,且 M=10M=10 达到上限。
      • 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. 测试数据说明

    测试点 MM 规模 特征描述 预期结果 考查点
    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. 教练的优化建议

    1. 文件极小化:本题每个输入文件大约只有 100~200 字节(3行+10行字符串),10 个文件总共不超过 2KB。这在 OJ 上上传和下载极快,完全满足“理想值 2MB 内”的要求。
    2. 不需除法:本算法完全基于字符串比对和队列操作,不存在除以 0 的风险。
    3. 常数优化:虽然 MM 很小,但在实际 NOI 竞赛中,如果要处理更长的基因序列(例如长度为 100),建议使用 unordered_set 替代 set,并将基因序列进行四进制状态压缩(A:0, C:1, G:2, T:3),将 8 位字符串压入一个 int 中,这样比对速度会提升数倍。
    4. 去重:在生成随机数据或求解时,必须使用 vis(或 set)去重,否则在循环变化的基因序列中会陷入死循环。
    • 0
      @ 2026-1-8 17:04:05

      在解决“最短路径”或“最少步数”问题时,宽度优先搜索 (BFS) 是最标准且最高效的工具。由于本题基因库规模极小(m10m \le 10),逻辑的正确性和边界处理比极端性能优化更重要。


      版本一:隐式图 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(竞赛进阶版)

      思路: 将 startGenebank 中的所有基因看作图的节点。预先计算两两之间是否只差一个字符,若是则连边。然后运行 BFS。这种方法在 mm 较大时逻辑更清晰。

      #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;
      }
      

      复杂度分析思考过程

      1. 时间复杂度

        • 状态总数:受限于基因库大小 MM。在本题中 M10M \le 10,即便考虑所有可能的组合也只有 48=655364^8 = 65536 种。
        • 每个状态的转移:长度 L=8L=8,碱基种类 Σ=4\Sigma=4,转移数为 L×(Σ1)=24L \times (\Sigma - 1) = 24
        • 整体复杂度O(M×L)O(M \times L)。在竞赛中,这意味着即使 MM 放大到 10410^4 也能轻松通过。
      2. 空间复杂度

        • 需要存储 bankvisited 集合,空间为 O(M×L)O(M \times L)

      时间复杂度优化建议

      • 双向 BFS (Bidirectional BFS):如果 MM 很大(如 10510^5),从起点和终点同时开启 BFS,相遇即停止。可以将搜索空间从 24step24^{step} 降低到 2×24step/22 \times 24^{step/2}
      • 状态压缩:如版本三所示,将基因序列转为整数,可以避免字符串哈希和频繁的内存分配。
      • A 搜索*:利用当前字符串与目标的“差异字符数”作为启发式函数 h(n)h(n),在 MM 极大时更有优势。

      读题关键词与易错点

      1. “最小变化次数”:看到“最小/最短/最少”步数,第一时间反应 BFS
      2. “只有出现在 bank 中才有效”:这是典型的图遍历约束条件。
      3. M=0M=0 的极端情况:此时如果起点不等于终点,必然输出 -1
      4. 起点和终点相同:注意初始化步数为 0
      5. End 不在 Bank 中:这是最常见的“坑”,必须输出 -1。但在一些特殊题目中,起点也不在 Bank 中,一定要仔细阅读“起始基因序列默认有效”这句话。
      • 1

      信息

      ID
      19457
      时间
      1000ms
      内存
      128MiB
      难度
      10
      标签
      (无)
      递交数
      1
      已通过
      1
      上传者