2 条题解
-
0
这是一个经过优化的 C++ 标准解答代码。
代码特点
- 解决段错误 (Segmentation Fault):使用了 滚动数组 和 离线查询 技术。
- 原题目若直接开
dp[100][200000]需要约 80MB 内存,且如果定义在局部变量中容易爆栈。 - 此代码将空间压缩至
dp[2][200000](约 1.6MB),完全避免了内存问题。
- 原题目若直接开
- 时间复杂度优秀:使用了 滑动窗口 优化转移,总时间复杂度为 ,能够轻松通过所有测试点。
- 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; } - 解决段错误 (Segmentation Fault):使用了 滚动数组 和 离线查询 技术。
-
0
你好!作为OI教练,很高兴为你提供这道题目(P11230 [CSP-J 2024] 接龙)的解题思路和相关资料。
这道题是 CSP-J 2024 的第三题,主要考察 动态规划(DP) 的状态设计与 滑动窗口 优化技巧。
1. 思路提示
核心问题转化: 题目问的是“第 轮能否以数字 结尾”。这提示我们应该把“轮数”作为 DP 的一个阶段。 我们需要计算每一轮结束后,哪些数字是可以作为结尾的。
难点突破: 最直接的想法是用
bool dp[r][v]表示第 轮能否以 结尾。 但是题目有一个限制:“接龙的人不能与上一轮相同”。 这意味着,仅仅知道“ 能作为第 轮结尾”是不够的,我们还得知道“是谁让 成了结尾”。- 如果是玩家 A 让第 轮以 结尾,那么第 轮玩家 A 就不能接这个 ,但玩家 B、C 都可以接。
- 如果是玩家 A 和玩家 B 都能让第 轮以 结尾,那么第 轮无论是谁(包括 A 和 B)想接,都能找到合法的上家(A 想接就找 B 的记录,B 想接就找 A 的记录)。
DP 状态设计: 定义
dp[r][v]:0: 第 轮无法以 结尾。p(): 第 轮只能由玩家 p 达成以 结尾。(下一轮 p 不能接,别人能接)-1: 第 轮可以由至少两个不同玩家达成以 结尾。(下一轮谁都能接)
转移策略与优化: 计算第 轮时,遍历每个玩家 的序列 。 对于序列中的每一个位置 (作为接龙子串的结尾),我们需要检查在它的前面(距离 到 范围内)是否存在一个位置 ,使得 在第 轮是合法的结尾(即
dp[r-1][S_p[i]]允许玩家 接龙)。 朴素做法是 检查,总复杂度太高。 利用 滑动窗口:对于固定的结尾 ,合法的起点 处于区间 。我们维护窗口内“合法起点”的数量,即可实现 转移。2. 预备知识点
- 动态规划 (DP):理解分阶段决策,特别是处理“不能连续由同一人操作”这类状态机模型。
- 滑动窗口 / 双指针:用于在线性扫描序列时,高效维护固定长度区间内的统计信息。
- 时空复杂度分析: 很小(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.out到10.in/10.out。 数据覆盖了题目描述中所有的特殊性质(如 的大小、字符频次限制等)以及各类边界情况。#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
- 上传者