2 条题解

  • 0
    @ 2025-11-28 18:36:25

    这是一个经过优化的 C++ 标准解答代码

    代码特点

    1. 解决段错误 (Segmentation Fault):使用了 滚动数组离线查询 技术。
      • 原题目若直接开 dp[100][200000] 需要约 80MB 内存,且如果定义在局部变量中容易爆栈。
      • 此代码将空间压缩至 dp[2][200000](约 1.6MB),完全避免了内存问题。
    2. 时间复杂度优秀:使用了 滑动窗口 优化转移,总时间复杂度为 O(T×L×max(r))O(T \times \sum L \times \max(r)),能够轻松通过所有测试点。
    3. IO 优化:使用了 cin.tie(0) 加速输入输出。

    标准解答代码 (solution.cpp)

    #include <iostream>
    #include <vector>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    // 值域上限 (题目中数字最大为 200000)
    const int MAX_VAL = 200005; 
    // 最大轮数 (题目中 r <= 100)
    const int MAX_R = 105;
    // 最大查询数
    const int MAX_Q = 100005;
    
    // 滚动数组:
    // pre[v]: 上一轮结束后,数字 v 的状态
    // cur[v]: 当前轮结束后,数字 v 的状态
    // 状态定义: 0=不可达, -1=多人可达, p=仅玩家p可达
    int pre[MAX_VAL], cur[MAX_VAL];
    
    // 离线查询存储:qs[r] 存储所有询问第 r 轮的任务
    // pair: {期望结尾数字 c, 查询编号 index}
    vector<pair<int, int>> qs[MAX_R];
    
    // 存储最终答案
    int ans[MAX_Q];
    
    void solve() {
        int n, k, q;
        if (!(cin >> n >> k >> q)) return;
    
        // 1. 读入词库
        // 使用 vector 动态存储,避免空间浪费
        vector<vector<int>> S(n + 1);
        for (int i = 1; i <= n; ++i) {
            int l;
            cin >> l;
            S[i].resize(l);
            for (int j = 0; j < l; ++j) {
                cin >> S[i][j];
            }
        }
    
        // 2. 读入查询并按轮数归类 (离线处理)
        for(int i=0; i<MAX_R; ++i) qs[i].clear();
        
        int max_r = 0;
        for (int i = 0; i < q; ++i) {
            int r, c;
            cin >> r >> c;
            qs[r].push_back({c, i});
            max_r = max(max_r, r);
        }
        // 题目保证 r <= 100,做个保护
        max_r = min(max_r, 100);
    
        // 3. 初始化状态
        // 清空上一轮状态
        memset(pre, 0, sizeof(pre));
        // 第 0 轮结束时视为以 1 结尾,且状态为 -1 (通用),
        // 这样第 1 轮任何玩家都能以 1 为起点开始接龙
        pre[1] = -1; 
    
        // 4. 开始按轮数 DP
        for (int r = 1; r <= max_r; ++r) {
            // 清空当前轮状态
            memset(cur, 0, sizeof(cur));
    
            // 遍历每一位玩家
            for (int p = 1; p <= n; ++p) {
                const vector<int>& seq = S[p];
                int len = seq.size();
                int valid_cnt = 0;
    
                // Lambda: 检查 seq[idx] 是否可以作为本轮接龙的【起点】
                // 依据是 seq[idx] 在【上一轮】是否是合法的结尾
                auto check = [&](int idx) {
                    int val = seq[idx];
                    int state = pre[val]; 
                    // 如果上一轮是多人可达(-1),则玩家 p 肯定能接
                    if (state == -1) return true;
                    // 如果上一轮是别人(state)可达,且不是玩家 p 自己,则能接
                    if (state > 0 && state != p) return true;
                    // 否则 (不可达 或 是 p 自己达成的) 不能接
                    return false;
                };
    
                // 滑动窗口:维护区间 [i-k+1, i-1] 内合法起点的数量
                // 接龙长度要求 [2, k],所以对于结尾 i:
                // 最短接龙: 起点 i-1 (长度2)
                // 最长接龙: 起点 i-k+1 (长度k)
                for (int i = 0; i < len; ++i) {
                    // 窗口右移:尝试加入新的起点 i-1
                    if (i - 1 >= 0) {
                        if (check(i - 1)) valid_cnt++;
                    }
                    
                    // 窗口左缩:移除过期的起点 i-k
                    // 此时长度为 k+1,不符合要求
                    if (i - k >= 0) {
                        if (check(i - k)) valid_cnt--;
                    }
    
                    // 如果窗口内存在合法的起点,说明 seq[i] 可以作为本轮结尾
                    if (valid_cnt > 0) {
                        int val = seq[i];
                        if (cur[val] == 0) {
                            cur[val] = p;     // 之前没人到达过,标记为 p
                        } else if (cur[val] != p) {
                            cur[val] = -1;    // 之前被别人到达过,标记为多人
                        }
                    }
                }
            }
    
            // 5. 处理当前轮的所有查询
            for (auto& query : qs[r]) {
                int val = query.first;
                int id = query.second;
                // 只要状态不为 0,即表示可以完成
                ans[id] = (cur[val] != 0 ? 1 : 0);
            }
    
            // 6. 滚动状态:当前轮变为下一轮的前一轮
            memcpy(pre, cur, sizeof(cur));
        }
    
        // 7. 输出所有答案
        for (int i = 0; i < q; ++i) {
            cout << ans[i] << "\n";
        }
    }
    
    int main() {
        // 优化 I/O 速度
        ios::sync_with_stdio(false);
        cin.tie(NULL);
        
        int t;
        if (cin >> t) {
            while (t--) {
                solve();
            }
        }
        return 0;
    }
    
    • 0
      @ 2025-11-28 18:19:23

      你好!作为OI教练,很高兴为你提供这道题目(P11230 [CSP-J 2024] 接龙)的解题思路和相关资料。

      这道题是 CSP-J 2024 的第三题,主要考察 动态规划(DP) 的状态设计与 滑动窗口 优化技巧。

      1. 思路提示

      核心问题转化: 题目问的是“第 rr 轮能否以数字 cc 结尾”。这提示我们应该把“轮数”作为 DP 的一个阶段。 我们需要计算每一轮结束后,哪些数字是可以作为结尾的。

      难点突破: 最直接的想法是用 bool dp[r][v] 表示第 rr 轮能否以 vv 结尾。 但是题目有一个限制:“接龙的人不能与上一轮相同”。 这意味着,仅仅知道“vv 能作为第 rr 轮结尾”是不够的,我们还得知道“是vv 成了结尾”。

      • 如果是玩家 A 让第 rr 轮以 vv 结尾,那么第 r+1r+1 轮玩家 A 就不能接这个 vv,但玩家 B、C 都可以接。
      • 如果是玩家 A 和玩家 B 都能让第 rr 轮以 vv 结尾,那么第 r+1r+1 轮无论是谁(包括 A 和 B)想接,都能找到合法的上家(A 想接就找 B 的记录,B 想接就找 A 的记录)。

      DP 状态设计: 定义 dp[r][v]

      • 0: 第 rr 轮无法以 vv 结尾。
      • p (p>0p > 0): 第 rr 轮只能由玩家 p 达成以 vv 结尾。(下一轮 p 不能接,别人能接)
      • -1: 第 rr 轮可以由至少两个不同玩家达成以 vv 结尾。(下一轮谁都能接)

      转移策略与优化: 计算第 rr 轮时,遍历每个玩家 pp 的序列 SpS_p。 对于序列中的每一个位置 jj(作为接龙子串的结尾),我们需要检查在它的前面(距离 22kk 范围内)是否存在一个位置 ii,使得 Sp[i]S_p[i] 在第 r1r-1 轮是合法的结尾(即 dp[r-1][S_p[i]] 允许玩家 pp 接龙)。 朴素做法是 O(k)O(k) 检查,总复杂度太高。 利用 滑动窗口:对于固定的结尾 jj,合法的起点 ii 处于区间 [jk+1,j1][j-k+1, j-1]。我们维护窗口内“合法起点”的数量,即可实现 O(1)O(1) 转移。

      2. 预备知识点

      1. 动态规划 (DP):理解分阶段决策,特别是处理“不能连续由同一人操作”这类状态机模型。
      2. 滑动窗口 / 双指针:用于在线性扫描序列时,高效维护固定长度区间内的统计信息。
      3. 时空复杂度分析rr 很小(100),但值域较大(20万),需要根据数据范围选择正确的 DP 维度。

      3. 标准答案代码 (C++)

      #include <iostream>
      #include <vector>
      #include <cstring>
      #include <algorithm>
      
      using namespace std;
      
      const int MAXN = 200005; // 序列元素值域及可能的长度和
      const int MAXR = 105;    // 最大轮数
      
      // dp[r][v]: 第 r 轮以值 v 结尾的状态
      // 0: 不可达
      // -1: 多人可达 (通用)
      // p > 0: 仅玩家 p 可达
      int dp[MAXR][MAXN];
      
      void solve() {
          int n, k, q;
          cin >> n >> k >> q;
      
          // 读入所有玩家的词库
          vector<vector<int>> S(n + 1);
          for (int i = 1; i <= n; ++i) {
              int len;
              cin >> len;
              S[i].resize(len);
              for (int j = 0; j < len; ++j) {
                  cin >> S[i][j];
              }
          }
      
          // 读入所有任务,记录最大轮数以便控制 DP 边界
          vector<pair<int, int>> queries(q);
          int max_r = 0;
          for (int i = 0; i < q; ++i) {
              cin >> queries[i].first >> queries[i].second;
              max_r = max(max_r, queries[i].first);
          }
          // 题目保证 r <= 100
          max_r = min(max_r, 100);
      
          // 初始化 DP
          // 只需要清空到 max_r 即可,减小常数
          for (int r = 0; r <= max_r; ++r) {
              memset(dp[r], 0, sizeof(dp[r]));
          }
      
          // 第 0 轮虚拟状态:以 1 结尾,视为“多人可达”,这样第 1 轮谁都能接 1
          dp[0][1] = -1;
      
          // 逐轮 DP
          for (int r = 1; r <= max_r; ++r) {
              // 遍历每个玩家
              for (int p = 1; p <= n; ++p) {
                  const vector<int>& seq = S[p];
                  int len = seq.size();
                  
                  // 滑动窗口维护合法起点数量
                  int valid_cnt = 0;
      
                  // 检查 seq[idx] 是否可以作为第 r 轮接龙的【起点】
                  auto check = [&](int idx) {
                      int val = seq[idx];
                      int state = dp[r-1][val];
                      // 状态为 -1 (多人) -> 可接
                      // 状态为 other (且 other != p) -> 可接
                      if (state == -1) return true;
                      if (state > 0 && state != p) return true;
                      return false;
                  };
      
                  // 枚举接龙子串的【终点】i
                  // 子串长度 [2, k] => 起点范围 [i-k+1, i-1]
                  for (int i = 0; i < len; ++i) {
                      // 1. 窗口右移:尝试加入新的起点 i-1
                      if (i - 1 >= 0) {
                          if (check(i - 1)) valid_cnt++;
                      }
                      
                      // 2. 窗口左缩:移除过期的起点 i-k
                      // 此时长度为 k+1,所以要把 i-k 移出
                      if (i - k >= 0) {
                          if (check(i - k)) valid_cnt--;
                      }
      
                      // 3. 更新状态
                      if (valid_cnt > 0) {
                          int val = seq[i];
                          if (dp[r][val] == 0) {
                              dp[r][val] = p; // 之前没人到过,记录 p
                          } else if (dp[r][val] != p) {
                              dp[r][val] = -1; // 之前有别人到过,标记为多人
                          }
                          // 如果已经是 p 或 -1,则状态不变
                      }
                  }
              }
          }
      
          // 回答询问
          for (int i = 0; i < q; ++i) {
              int r = queries[i].first;
              int c = queries[i].second;
              if (dp[r][c] != 0) cout << "1\n";
              else cout << "0\n";
          }
      }
      
      int main() {
          ios::sync_with_stdio(false);
          cin.tie(0);
          int t;
          if (cin >> t) {
              while (t--) {
                  solve();
              }
          }
          return 0;
      }
      

      4. 数据生成器 (Generator)

      该程序会生成 1.in/1.out10.in/10.out。 数据覆盖了题目描述中所有的特殊性质(如 kk 的大小、字符频次限制等)以及各类边界情况。

      #include <iostream>
      #include <vector>
      #include <cstring>
      #include <algorithm>
      #include <fstream>
      #include <string>
      #include <random>
      #include <ctime>
      #include <map>
      
      using namespace std;
      
      // ==========================================
      // Part 1: 标准解答逻辑 (Solver) - 用于生成 .out
      // ==========================================
      
      const int MAXN_SOL = 200005;
      const int MAXR_SOL = 105;
      int dp_sol[MAXR_SOL][MAXN_SOL];
      
      void solve_for_gen(istream& in, ostream& out) {
          int n, k, q;
          if (!(in >> n >> k >> q)) return;
      
          vector<vector<int>> S(n + 1);
          for (int i = 1; i <= n; ++i) {
              int len;
              in >> len;
              S[i].resize(len);
              for (int j = 0; j < len; ++j) {
                  in >> S[i][j];
              }
          }
      
          vector<pair<int, int>> queries(q);
          int max_r = 0;
          for (int i = 0; i < q; ++i) {
              in >> queries[i].first >> queries[i].second;
              max_r = max(max_r, queries[i].first);
          }
          max_r = min(max_r, 100);
      
          for (int r = 0; r <= max_r; ++r) {
              memset(dp_sol[r], 0, sizeof(dp_sol[r]));
          }
          dp_sol[0][1] = -1;
      
          for (int r = 1; r <= max_r; ++r) {
              for (int p = 1; p <= n; ++p) {
                  const vector<int>& seq = S[p];
                  int len = seq.size();
                  int valid_cnt = 0;
      
                  auto check = [&](int idx) {
                      int val = seq[idx];
                      int state = dp_sol[r-1][val];
                      if (state == -1) return true;
                      if (state > 0 && state != p) return true;
                      return false;
                  };
      
                  for (int i = 0; i < len; ++i) {
                      if (i - 1 >= 0) {
                          if (check(i - 1)) valid_cnt++;
                      }
                      if (i - k >= 0) {
                          if (check(i - k)) valid_cnt--;
                      }
      
                      if (valid_cnt > 0) {
                          int val = seq[i];
                          if (dp_sol[r][val] == 0) dp_sol[r][val] = p;
                          else if (dp_sol[r][val] != p) dp_sol[r][val] = -1;
                      }
                  }
              }
          }
      
          for (int i = 0; i < q; ++i) {
              int r = queries[i].first;
              int c = queries[i].second;
              out << (dp_sol[r][c] != 0 ? 1 : 0) << endl;
          }
      }
      
      // ==========================================
      // Part 2: 数据生成逻辑 (Generator)
      // ==========================================
      
      mt19937 rng(time(0));
      
      int rand_int(int min, int max) {
          if (min > max) return min;
          uniform_int_distribution<int> dist(min, max);
          return dist(rng);
      }
      
      void generate_data(int id) {
          string in_file = to_string(id) + ".in";
          string out_file = to_string(id) + ".out";
          ofstream fin(in_file);
          ofstream fout(out_file);
      
          int T = 5; // 默认 T=5
          // 对于大规模数据,T 减小以防生成太慢
          if (id == 4 || id == 6 || id == 8 || id == 10) T = 2;
      
          fin << T << endl;
      
          for (int t = 0; t < T; ++t) {
              int n, k, q;
              int sum_l_limit = 200000;
              int r_limit = 100;
              int max_val = 200000;
              bool prop_c = false; // 字符频次限制
      
              // 根据测试点 ID 设定参数 (覆盖 CSP-J 数据表)
              // 1: Small, r=1
              if (id == 1) {
                  n = rand_int(50, 100); k = rand_int(2, 50); q = rand_int(50, 100);
                  sum_l_limit = 2000; r_limit = 1;
              }
              // 2: Tiny
              else if (id == 2) {
                  n = 10; k = 3; q = 10;
                  sum_l_limit = 20; r_limit = 5; max_val = 20;
              }
              // 3: Prop A (k=2e5), Small
              else if (id == 3) {
                  n = rand_int(50, 500); k = 200000; q = rand_int(50, 500);
                  sum_l_limit = 2000; r_limit = 10;
              }
              // 4: Prop A, Large
              else if (id == 4) {
                  n = rand_int(10000, 50000); k = 200000; q = rand_int(10000, 50000);
                  sum_l_limit = 200000; r_limit = 100;
              }
              // 5: Prop B (k<=5), Small
              else if (id == 5) {
                  n = rand_int(50, 500); k = rand_int(2, 5); q = rand_int(50, 500);
                  sum_l_limit = 2000; r_limit = 10;
              }
              // 6: Prop B, Large
              else if (id == 6) {
                  n = rand_int(10000, 50000); k = rand_int(2, 5); q = rand_int(10000, 50000);
                  sum_l_limit = 200000; r_limit = 100;
              }
              // 7: Prop C (Freq<=5), Small
              else if (id == 7) {
                  n = rand_int(50, 500); k = rand_int(2, 50); q = rand_int(50, 500);
                  sum_l_limit = 2000; r_limit = 10;
                  prop_c = true;
              }
              // 8: Prop C, Large
              else if (id == 8) {
                  n = rand_int(10000, 50000); k = rand_int(10, 1000); q = rand_int(10000, 50000);
                  sum_l_limit = 200000; r_limit = 100;
                  prop_c = true;
              }
              // 9: General Small
              else if (id == 9) {
                  n = rand_int(100, 1000); k = rand_int(10, 100); q = rand_int(100, 1000);
                  sum_l_limit = 2000; r_limit = 10;
              }
              // 10: General Large
              else {
                  n = rand_int(10000, 100000); k = rand_int(10, 200000); q = rand_int(10000, 50000);
                  sum_l_limit = 200000; r_limit = 100;
              }
      
              fin << n << " " << k << " " << q << endl;
      
              // 生成序列
              // 需要分配 sum_l_limit 给 n 个人
              vector<int> lens(n);
              int current_sum = 0;
              for(int i=0; i<n; ++i) lens[i] = 1; // 至少长度1 (题目允许)
              current_sum = n;
              
              // 随机分配剩余长度
              for(int i=0; i<n && current_sum < sum_l_limit; ++i) {
                  int remain = sum_l_limit - current_sum;
                  // 使得分布稍微随机一些,不是全部堆在第一个人
                  int add = rand_int(0, remain / (n - i) + 2); 
                  if (current_sum + add > sum_l_limit) add = sum_l_limit - current_sum;
                  lens[i] += add;
                  current_sum += add;
              }
      
              // 针对 Prop C 的特殊处理:全局计数器
              map<int, int> global_counts;
      
              for (int i = 0; i < n; ++i) {
                  fin << lens[i];
                  for (int j = 0; j < lens[i]; ++j) {
                      int val;
                      if (prop_c) {
                          // 尝试生成一个频次 <= 5 的数
                          int tries = 0;
                          do {
                              val = rand_int(1, max_val);
                              tries++;
                          } while (global_counts[val] >= 5 && tries < 20);
                          // 如果实在找不到(概率极低),就用一个新数或者硬塞(题目是保证,生成器尽力满足)
                          if (global_counts[val] >= 5) val = rand_int(1, max_val); 
                          global_counts[val]++;
                      } else {
                          // 普通生成,增加 1 的概率,保证第一轮有解
                          if (rand_int(1, 100) <= 5) val = 1;
                          else val = rand_int(1, max_val);
                      }
                      fin << " " << val;
                  }
                  fin << endl;
              }
      
              // 生成查询
              for (int i = 0; i < q; ++i) {
                  int r = rand_int(1, r_limit);
                  int c = rand_int(1, max_val);
                  // 偶尔生成一些容易命中的查询
                  if (rand_int(1, 10) <= 3) c = 1; 
                  fin << r << " " << c << endl;
              }
          }
          fin.close();
      
          // 运行 Solver 生成 output
          ifstream fin_read(in_file);
          int t_check;
          if (fin_read >> t_check) {
              while (t_check--) {
                  solve_for_gen(fin_read, fout);
              }
          }
          fin_read.close();
          fout.close();
      
          cout << "Generated Test Point " << id << endl;
      }
      
      int main() {
          // 生成 1 到 10 号测试点
          for (int i = 1; i <= 10; ++i) {
              generate_data(i);
          }
          return 0;
      }
      
      • 1

      信息

      ID
      15767
      时间
      1000ms
      内存
      32MiB
      难度
      7
      标签
      递交数
      2
      已通过
      1
      上传者