2 条题解

  • 0
    @ 2025-12-3 20:50:38

    这里提供两部分代码:

    1. 标准题解代码:这是通过 OJ 的标准答案,逻辑清晰,适合教学。
    2. 数据生成器:这是一个“一键生成”工具。它包含了题解逻辑,运行一次即可在当前目录下生成 1.in/1.out10.in/10.out,并且覆盖了小数据、随机大数据、顺序、逆序、等值、完全山峰等各种边界情况。

    1. 标准题解代码 (C++)

    这是一个标准的 O(N2)O(N^2) 动态规划解法。

    #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: 小规模随机数据 (N20N \le 20),用于验证基本逻辑。
    • Test 3-5: 中大规模随机数据 (N=50,100N=50, 100),一般情况。
    • Test 6: 严格单调递增。答案应为0(直接全保留)。
    • Test 7: 严格单调递减。答案应为0。
    • Test 8: 完美山峰(先增后减)。答案应为0。
    • Test 9: 全部相等。边界情况,最长只能保留1人,答案应为 N1N-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
      @ 2025-12-3 20:47:01

      你好!很高兴能以OI教练的身份为你指点迷津。这是一道非常经典的 动态规划(DP) 入门与进阶衔接题。

      我们不要急着写代码,先用你的草稿纸和笔,跟我一起把思路画出来。


      第一部分:思路提示

      1. 逆向思维: 题目问的是“最少需要几位同学出列”。 直接去想踢掉谁比较麻烦,不如反过来想:最多能保留多少位同学,让他们组成合唱队形? 如果我们算出了最多能保留 KmaxK_{max} 位同学,那么答案就是 nKmaxn - K_{max}

      2. 抓住形状的核心特征: 再读一遍合唱队形的定义:t1<<ti>>tkt_1 < \dots < t_i > \dots > t_k。 想象一下这组数据的折线图,这像什么形状? 是的,像一座山峰(中间高,两边低)。

      • tit_i 是这座山的山顶
      • 山顶的左边(11ii)必须是单调递增的。
      • 山顶的右边(iikk)必须是单调递减的。

      3. 枚举山顶: 在这个队伍里,每个人都有可能成为山顶。 既然我们不知道谁做山顶最好,那我们应该怎么办? (提示:计算机最擅长做什么?) \rightarrow 枚举。我们可以试着让第 11 个同学做山顶,算算这时候队伍多长;让第 22 个同学做山顶,算算队伍多长……最后取最大值。


      第二部分:预备知识点总结

      要顺利解决这道题,你需要掌握以下知识:

      1. 动态规划(Dynamic Programming, DP)基础:理解状态定义和状态转移方程。
      2. 最长上升子序列(LIS, Longest Increasing Subsequence):这是本题的绝对核心。你需要熟练掌握如何求一个序列的 LIS。
        • 通常 O(N2)O(N^2) 的算法就足够通过此题(因为 N100N \le 100)。
        • 状态定义通常是:dp[i]dp[i] 表示以第 ii 个元素结尾的最长上升子序列长度。
      3. 最长下降子序列(LDS):其实就是反过来的 LIS。
      4. 简单的数组操作与循环控制

      第三部分:启发式教学 —— 草稿纸上的推理

      现在,拿起你的笔,我们在草稿纸上模拟一下这个思考过程。

      步骤一:画图拆解

      假设有一排同学,我们选定第 ii 个同学作为“山顶”(Peak)。请在草稿纸上画一个这样的示意图:

             Peak (第 i 个人)
              /  \
             /    \
            /      \
      Left Side   Right Side
      (上升)       (下降)
      

      教练提问: 如果第 ii 个人是山顶,那么:

      1. 左边剩下的同学必须满足什么条件?并且为了让总人数最多,我们需要在左边找什么?
        • 引导回答:要在 1i1 \dots i 的范围内,找到以 ii 结尾的最长上升子序列
      2. 右边剩下的同学必须满足什么条件?
        • 引导回答:要在 ini \dots n 的范围内,找到以 ii 开头的最长下降子序列

      步骤二:定义状态

      让我们设两个数组(DP表),在草稿纸上写下它们的定义:

      • f[i]f[i]:表示以第 ii 位同学结尾,从左向右的最长上升子序列的长度。

        • 例如:1 5 3 2 4,如果 ii 指向 4,那么序列是 1 3 41 2 4,长度是 3。
      • g[i]g[i]:表示以第 ii 位同学开头,从左向右的最长下降子序列的长度。

        • 教练提示: 从左往右求下降,其实等价于从右往左求上升
        • 所以在计算 g[i]g[i] 时,你可以倒着循环,把问题变成求以 ii 结尾(从右看)的最长上升子序列。

      步骤三:模拟合并

      现在假设我们已经算好了所有的 ffgg 数组。 对于第 ii 个同学做山顶,整个合唱队的总人数是多少?

      请看下面的图示:

      位置:   1   2   3   4   5
      身高:  ... ...  Ti ... ...
      
      左边(LIS): [-------]  (长度 f[i])
      右边(LDS):         [-------] (长度 g[i])
      

      关键问题: 如果你直接算 f[i]+g[i]f[i] + g[i],会有什么问题?

      • 观察f[i]f[i] 包含了 TiT_i 自己,g[i]g[i] 也包含了 TiT_i 自己。
      • 结论TiT_i 被算了两次!
      • 修正公式:以 ii 为山顶的最大长度 =f[i]+g[i]1= f[i] + g[i] - 1

      步骤四:最终演练(样例数据)

      让我们用样例数据简单走一遍流程,加深印象。 数据:186 186 150 200 160 130 197 220

      1. 计算 ff 数组(从左往右 LIS)

      • i=1(186)i=1 (186): 1 (自己)
      • i=2(186)i=2 (186): 1 (前面没比它小的)
      • i=3(150)i=3 (150): 1
      • i=4(200)i=4 (200): 接在186或150后面 -> 2
      • ...
      • 让学生自己往下推导几个...

      2. 计算 gg 数组(从右往左 LIS)

      • i=8(220)i=8 (220): 1
      • i=7(197)i=7 (197): 接在220后面(如果看作下降) -> 2
      • i=6(130)i=6 (130): 接在197或220后面 -> 3
      • ...

      3. 合并寻找最大值 遍历 ii 从 1 到 nn: 计算 Total[i]=f[i]+g[i]1Total[i] = f[i] + g[i] - 1。 找到 TotalTotal 中的最大值,记为 MaxLenMaxLen

      4. 最后的陷阱 题目要输出什么? 不是 MaxLenMaxLen,而是 nMaxLenn - MaxLen


      教练总结: 这道题其实是两次 LIS 的组合拳。你要做的是:

      1. 写一个循环求正向 LIS。
      2. 写一个循环求反向 LIS(或者正向求下降)。
      3. 枚举连接点,找出最大和。

      快去试试把逻辑转换成代码吧!别忘了 O(N2)O(N^2)N=100N=100 时是非常快的,不用担心超时。

      • 1

      信息

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