2 条题解
-
0
这里提供两部分代码:
- 标准题解代码:这是通过 OJ 的标准答案,逻辑清晰,适合教学。
- 数据生成器:这是一个“一键生成”工具。它包含了题解逻辑,运行一次即可在当前目录下生成
1.in/1.out到10.in/10.out,并且覆盖了小数据、随机大数据、顺序、逆序、等值、完全山峰等各种边界情况。
1. 标准题解代码 (C++)
这是一个标准的 动态规划解法。
#include <iostream> #include <vector> #include <algorithm> using namespace std; // 核心逻辑函数 int solve() { int n; if (!(cin >> n)) return 0; vector<int> t(n); for (int i = 0; i < n; i++) { cin >> t[i]; } // dp1[i]: 以第i个人为结尾的从左向右的最长上升子序列长度 // dp2[i]: 以第i个人为结尾的从右向左的最长上升子序列长度(即原序列从i开始向右的下降序列) vector<int> dp1(n, 1), dp2(n, 1); // 1. 计算 dp1 (从左往右 LIS) for (int i = 0; i < n; i++) { for (int j = 0; j < i; j++) { if (t[i] > t[j]) { dp1[i] = max(dp1[i], dp1[j] + 1); } } } // 2. 计算 dp2 (从右往左 LIS) for (int i = n - 1; i >= 0; i--) { for (int j = n - 1; j > i; j--) { if (t[i] > t[j]) { dp2[i] = max(dp2[i], dp2[j] + 1); } } } // 3. 枚举每个点作为山顶,求最大合唱队形长度 int max_k = 0; for (int i = 0; i < n; i++) { // 减1是因为山顶同学在dp1和dp2中都被计算了一次 max_k = max(max_k, dp1[i] + dp2[i] - 1); } // 4. 结果是总人数减去保留的最大人数 return n - max_k; } int main() { // 加速IO ios::sync_with_stdio(false); cin.tie(0); cout << solve() << endl; return 0; }
2. 数据生成器 (Data Generator)
将下面的代码保存为
generator.cpp,编译运行即可。它会在当前目录创建 20 个文件(10组输入输出)。生成策略说明:
- Test 1-2: 小规模随机数据 (),用于验证基本逻辑。
- Test 3-5: 中大规模随机数据 (),一般情况。
- Test 6: 严格单调递增。答案应为0(直接全保留)。
- Test 7: 严格单调递减。答案应为0。
- Test 8: 完美山峰(先增后减)。答案应为0。
- Test 9: 全部相等。边界情况,最长只能保留1人,答案应为 。
- Test 10: 随机但数值范围极小。测试大量重复数值的情况。
#include <iostream> #include <vector> #include <algorithm> #include <fstream> #include <string> #include <random> #include <chrono> using namespace std; // ================= 题解逻辑(用于生成 .out 文件) ================= int solve_for_file(const vector<int>& t) { int n = t.size(); if (n == 0) return 0; vector<int> dp1(n, 1), dp2(n, 1); // LIS Left -> Right for (int i = 0; i < n; i++) { for (int j = 0; j < i; j++) { if (t[i] > t[j]) { dp1[i] = max(dp1[i], dp1[j] + 1); } } } // LIS Right -> Left for (int i = n - 1; i >= 0; i--) { for (int j = n - 1; j > i; j--) { if (t[i] > t[j]) { dp2[i] = max(dp2[i], dp2[j] + 1); } } } int max_k = 0; for (int i = 0; i < n; i++) { max_k = max(max_k, dp1[i] + dp2[i] - 1); } return n - max_k; } // ================= 数据生成工具函数 ================= // 随机数生成器初始化 mt19937 rng(chrono::steady_clock::now().time_since_epoch().count()); int random_int(int l, int r) { uniform_int_distribution<int> dist(l, r); return dist(rng); } void generate_case(int case_id, int n, string type) { string in_file = to_string(case_id) + ".in"; string out_file = to_string(case_id) + ".out"; ofstream fout(in_file); fout << n << endl; vector<int> arr(n); // 根据不同类型生成数据 if (type == "random") { for (int i = 0; i < n; i++) arr[i] = random_int(130, 230); } else if (type == "increasing") { // 严格递增:从130开始,步长随机1-2,确保不超230 int curr = 130; for(int i=0; i<n; ++i) { arr[i] = curr; if (curr < 230) curr++; } } else if (type == "decreasing") { // 严格递减 int curr = 230; for(int i=0; i<n; ++i) { arr[i] = curr; if (curr > 130) curr--; } } else if (type == "mountain") { // 完美山峰:中间高两边低 int peak = n / 2; // 左半边递增 int val = 130; for (int i = 0; i <= peak; i++) arr[i] = val++; // 右半边递减 val--; for (int i = peak + 1; i < n; i++) arr[i] = --val; // 修正:确保在范围内且不为负(虽简单构造不会越界,保险起见) for(auto &x : arr) x = max(130, min(230, x)); } else if (type == "flat") { // 全部相等 int val = random_int(150, 200); for(int i=0; i<n; ++i) arr[i] = val; } else if (type == "narrow_random") { // 范围很小,容易重复 for(int i=0; i<n; ++i) arr[i] = random_int(160, 165); } // 写入 .in 文件 for (int i = 0; i < n; i++) { fout << arr[i] << (i == n - 1 ? "" : " "); } fout << endl; fout.close(); // 计算答案并写入 .out 文件 int ans = solve_for_file(arr); ofstream f_ans(out_file); f_ans << ans << endl; f_ans.close(); cout << "Generated Case " << case_id << ": N=" << n << " Type=" << type << " -> Ans=" << ans << endl; } int main() { // Case 1: 极小数据 generate_case(1, 5, "random"); // Case 2: 小数据 generate_case(2, 10, "random"); // Case 3: 中等数据 generate_case(3, 20, "random"); // Case 4: 较大随机 generate_case(4, 50, "random"); // Case 5: 最大随机 generate_case(5, 100, "random"); // Case 6: 边界 - 严格递增 (预期输出0,因为本身就是合唱队形的一种:只有左坡) generate_case(6, 100, "increasing"); // Case 7: 边界 - 严格递减 (预期输出0,只有右坡) generate_case(7, 100, "decreasing"); // Case 8: 边界 - 完美山峰 (预期输出0) generate_case(8, 100, "mountain"); // Case 9: 边界 - 全部相等 (预期输出 n-1,因为严格小于/大于,只能留1个) generate_case(9, 100, "flat"); // Case 10: 边界 - 密集重复数据 (测试严格不等号逻辑) generate_case(10, 100, "narrow_random"); cout << "All test cases generated successfully!" << endl; return 0; } -
0
你好!很高兴能以OI教练的身份为你指点迷津。这是一道非常经典的 动态规划(DP) 入门与进阶衔接题。
我们不要急着写代码,先用你的草稿纸和笔,跟我一起把思路画出来。
第一部分:思路提示
1. 逆向思维: 题目问的是“最少需要几位同学出列”。 直接去想踢掉谁比较麻烦,不如反过来想:最多能保留多少位同学,让他们组成合唱队形? 如果我们算出了最多能保留 位同学,那么答案就是 。
2. 抓住形状的核心特征: 再读一遍合唱队形的定义:。 想象一下这组数据的折线图,这像什么形状? 是的,像一座山峰(中间高,两边低)。
- 是这座山的山顶。
- 山顶的左边( 到 )必须是单调递增的。
- 山顶的右边( 到 )必须是单调递减的。
3. 枚举山顶: 在这个队伍里,每个人都有可能成为山顶。 既然我们不知道谁做山顶最好,那我们应该怎么办? (提示:计算机最擅长做什么?) 枚举。我们可以试着让第 个同学做山顶,算算这时候队伍多长;让第 个同学做山顶,算算队伍多长……最后取最大值。
第二部分:预备知识点总结
要顺利解决这道题,你需要掌握以下知识:
- 动态规划(Dynamic Programming, DP)基础:理解状态定义和状态转移方程。
- 最长上升子序列(LIS, Longest Increasing Subsequence):这是本题的绝对核心。你需要熟练掌握如何求一个序列的 LIS。
- 通常 的算法就足够通过此题(因为 )。
- 状态定义通常是: 表示以第 个元素结尾的最长上升子序列长度。
- 最长下降子序列(LDS):其实就是反过来的 LIS。
- 简单的数组操作与循环控制。
第三部分:启发式教学 —— 草稿纸上的推理
现在,拿起你的笔,我们在草稿纸上模拟一下这个思考过程。
步骤一:画图拆解
假设有一排同学,我们选定第 个同学作为“山顶”(Peak)。请在草稿纸上画一个这样的示意图:
Peak (第 i 个人) / \ / \ / \ Left Side Right Side (上升) (下降)教练提问: 如果第 个人是山顶,那么:
- 他左边剩下的同学必须满足什么条件?并且为了让总人数最多,我们需要在左边找什么?
- 引导回答:要在 的范围内,找到以 结尾的最长上升子序列。
- 他右边剩下的同学必须满足什么条件?
- 引导回答:要在 的范围内,找到以 开头的最长下降子序列。
步骤二:定义状态
让我们设两个数组(DP表),在草稿纸上写下它们的定义:
-
:表示以第 位同学结尾,从左向右的最长上升子序列的长度。
- 例如:
1 5 3 2 4,如果 指向4,那么序列是1 3 4或1 2 4,长度是 3。
- 例如:
-
:表示以第 位同学开头,从左向右的最长下降子序列的长度。
- 教练提示: 从左往右求下降,其实等价于从右往左求上升!
- 所以在计算 时,你可以倒着循环,把问题变成求以 结尾(从右看)的最长上升子序列。
步骤三:模拟合并
现在假设我们已经算好了所有的 和 数组。 对于第 个同学做山顶,整个合唱队的总人数是多少?
请看下面的图示:
位置: 1 2 3 4 5 身高: ... ... Ti ... ... 左边(LIS): [-------] (长度 f[i]) 右边(LDS): [-------] (长度 g[i])关键问题: 如果你直接算 ,会有什么问题?
- 观察: 包含了 自己, 也包含了 自己。
- 结论: 被算了两次!
- 修正公式:以 为山顶的最大长度 。
步骤四:最终演练(样例数据)
让我们用样例数据简单走一遍流程,加深印象。 数据:
186 186 150 200 160 130 197 2201. 计算 数组(从左往右 LIS)
- : 1 (自己)
- : 1 (前面没比它小的)
- : 1
- : 接在186或150后面 -> 2
- ...
- 让学生自己往下推导几个...
2. 计算 数组(从右往左 LIS)
- : 1
- : 接在220后面(如果看作下降) -> 2
- : 接在197或220后面 -> 3
- ...
3. 合并寻找最大值 遍历 从 1 到 : 计算 。 找到 中的最大值,记为 。
4. 最后的陷阱 题目要输出什么? 不是 ,而是 。
教练总结: 这道题其实是两次 LIS 的组合拳。你要做的是:
- 写一个循环求正向 LIS。
- 写一个循环求反向 LIS(或者正向求下降)。
- 枚举连接点,找出最大和。
快去试试把逻辑转换成代码吧!别忘了 在 时是非常快的,不用担心超时。
- 1
信息
- ID
- 4870
- 时间
- 1000ms
- 内存
- 32MiB
- 难度
- 3
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者