2 条题解

  • 0
    @ 2025-12-24 16:38:14

    作为教练,制作带限制的子序列和这类题目的数据时,核心在于构造能够让 DP 决策点在单调队列中频繁“换届”的序列,特别是针对 kk 值的大小变化以及负数的分布进行压力测试。

    本题的时间复杂度要求为 O(N)O(N),因此数据生成器中的标程逻辑必须严格使用单调队列优化,以确保在生成 10510^5 级数据时瞬时完成。

    一、 数据生成器代码 (C++14 标准)

    该程序会自动循环生成 1.in/out10.in/out。它包含了各种边界情况:全负数(考察非空子序列)、全正数、大跨度 kk 值等。

    #include <iostream>
    #include <fstream>
    #include <vector>
    #include <string>
    #include <deque>
    #include <algorithm>
    #include <random>
    #include <ctime>
    
    using namespace std;
    
    // ================= 标准标程逻辑 (用于生成 .out) =================
    // 采用单调队列优化 DP, 时间复杂度 O(N)
    long long solve_standard(int n, int k, const vector<int>& nums) {
        if (n == 0) return 0;
        vector<long long> dp(n);
        deque<int> dq;
        long long global_max = -2e18; // 保证非空子序列
    
        for (int i = 0; i < n; ++i) {
            // 1. 弹出不在窗口 [i-k, i-1] 内的决策点
            while (!dq.empty() && dq.front() < i - k) {
                dq.pop_front();
            }
    
            // 2. 状态转移: dp[i] = nums[i] + max(0, max_dp_in_window)
            long long prev_best = dq.empty() ? 0 : max(0LL, dp[dq.front()]);
            dp[i] = nums[i] + prev_best;
            
            // 更新结果
            global_max = max(global_max, dp[i]);
    
            // 3. 维护单调队列 (递减)
            while (!dq.empty() && dp[i] >= dp[dq.back()]) {
                dq.pop_back();
            }
            dq.push_back(i);
        }
        return global_max;
    }
    
    // ================= 数据生成辅助函数 =================
    void write_test_case(int id, int n, int k, const vector<int>& nums) {
        string in_name = to_string(id) + ".in";
        string out_name = to_string(id) + ".out";
    
        // 写入输入文件
        ofstream fout(in_name);
        fout << n << " " << k << "\n";
        for (int i = 0; i < n; ++i) {
            fout << nums[i] << (i == n - 1 ? "" : " ");
        }
        fout << endl;
        fout.close();
    
        // 生成标准输出
        long long result = solve_standard(n, k, nums);
        ofstream fans(out_name);
        fans << result << endl;
        fans.close();
    
        cout << "Testcase " << id << " generated: N=" << n << ", k=" << k << endl;
    }
    
    // ================= 主生成逻辑 =================
    int main() {
        mt19937 rng(time(0));
        uniform_int_distribution<int> val_dist(-10000, 10000);
    
        for (int i = 1; i <= 10; ++i) {
            int n, k;
            vector<int> nums;
    
            if (i == 1) { // 样例 1
                n = 5; k = 2; nums = {10, 2, -10, 5, 20};
            } 
            else if (i == 2) { // 样例 2 (全负数)
                n = 3; k = 1; nums = {-1, -2, -3};
            }
            else if (i == 3) { // 样例 3
                n = 5; k = 2; nums = {10, -2, -10, -5, 20};
            }
            else if (i == 4) { // 边界:k=1 (只能选相邻或跳过)
                n = 1000; k = 1;
                for(int j=0; j<n; ++j) nums.push_back(val_dist(rng));
            }
            else if (i == 5) { // 边界:k=n (可以跳过任何负数,选所有正数)
                n = 1000; k = n;
                for(int j=0; j<n; ++j) nums.push_back(val_dist(rng));
            }
            else if (i == 6) { // 全正数极限
                n = 10000; k = 500;
                for(int j=0; j<n; ++j) nums.push_back(rng() % 10000);
            }
            else if (i == 7) { // 密集负数,少量极大正数
                n = 10000; k = 10;
                for(int j=0; j<n; ++j) {
                    if (j % 100 == 0) nums.push_back(10000);
                    else nums.push_back(-10000);
                }
            }
            else if (i == 8) { // 锯齿形波动
                n = 50000; k = 100;
                for(int j=0; j<n; ++j) nums.push_back(j % 2 == 0 ? 10000 : -10000);
            }
            else if (i <= 10) { // 极限随机数据
                n = 100000; k = rng() % n + 1;
                for(int j=0; j<n; ++j) nums.push_back(val_dist(rng));
            }
    
            write_test_case(i, n, k, nums);
        }
        return 0;
    }
    

    二、 测试点设计思路(创建 OJ 评测点参考)

    作为教练,我们需要通过以下维度确保选手代码的鲁棒性:

    测试点 数据特征 考察重点
    1-3 官方样例 确保基础逻辑与题目完全一致。
    4 k=1k=1 限制最严的情况,考察 DP 基础状态转移。
    5 k=nk=n 限制最松的情况,考察是否能正确识别“只选正数”的最优策略。
    6 全正数 考察 long long 累加是否溢出(最大值可达 10910^9)。
    7 稀疏正数 考察单调队列在长距离负数区间内维护最大值的能力。
    8 剧烈波动 考察单调队列频繁 pop_backpop_front 的性能。
    9-10 极限规模 10510^5 性能测试:确保算法是 O(N)O(N) 而非 O(NK)O(NK)

    三、 考场避坑建议

    1. 非空子序列: 题目要求是非空子序列。如果数组全是负数(如 [-5, -2, -10]),结果应该是 -2 而不是 0。在代码实现中,ans 的初始值应设为极小值或第一个元素的 dp 值。

    2. 数值溢出N=105N=10^5,每个数最大 10410^4,理论最大和为 10910^9,虽然在 int 范围内,但为了安全起见,NOI 选手的标配是 全程 long long

    3. 单调队列的性质

      • 队首弹出:检查决策点是否过期(下标差距 >k>k)。
      • 队尾弹出:检查新决策点是否比旧决策点更优(dp[i] >= dp[dq.back()])。
      • 取值逻辑:只有当队首的 dp 值为正时才累加,否则当前元素自立门户更好。
    4. 非递归安全: 该 DP 方案本身是迭代实现的,生成器和标程均无递归,在任何系统栈限制下都能稳定运行。

    这份生成器生成的 .in.out 格式严谨,可以直接用于创建练习赛。加油!

    • 0
      @ 2025-12-24 16:36:59

      你好,同学。这道题是 单调队列优化动态规划(DP) 的进阶实战。在 NOI 竞赛中,单调队列是解决“区间最值”类转移方程的最优工具。

      以下是基于 C++14 标准 的满分题解代码。


      一、 标程代码 (C++14 标准)

      #include <iostream>
      #include <vector>
      #include <deque>
      #include <algorithm>
      
      using namespace std;
      
      /**
       * 题目:带限制的子序列和
       * 核心思想:动态规划 + 单调队列优化
       * 
       * 状态定义:dp[i] 表示以 nums[i] 结尾的满足条件的子序列最大和。
       * 转移方程:dp[i] = nums[i] + max(0, max{dp[j]}) 其中 i-k <= j < i
       * 优化点:使用单调队列维护窗口 [i-k, i-1] 内 dp 值的最大值。
       */
      
      void solve() {
          int n, k;
          if (!(cin >> n >> k)) return;
      
          vector<int> nums(n);
          for (int i = 0; i < n; ++i) cin >> nums[i];
      
          // dp[i] 可能达到 10^9 级别(10^5 * 10^4),int 勉强够,但竞赛建议用 long long
          vector<long long> dp(n);
          // dq 存储下标,维护 dp 值的单调递减
          deque<int> dq;
          
          long long ans = -2e18; // 初始化为极小值
      
          for (int i = 0; i < n; ++i) {
              // 1. 弹出过时的下标:如果队首不在窗口 [i-k, i-1] 内
              while (!dq.empty() && dq.front() < i - k) {
                  dq.pop_front();
              }
      
              // 2. 状态转移
              // 队首就是窗口内 dp 值最大的下标。如果最大值比 0 小,我们宁愿不选前面的。
              long long prev_max = dq.empty() ? 0 : max(0LL, dp[dq.front()]);
              dp[i] = nums[i] + prev_max;
              
              // 更新全局最大值
              ans = max(ans, dp[i]);
      
              // 3. 维护单调队列的递减性
              // 如果当前 dp[i] 比队尾的 dp 大,说明队尾已经没有存在的意义了(更旧且更弱)
              while (!dq.empty() && dp[i] >= dp[dq.back()]) {
                  dq.pop_back();
              }
              
              dq.push_back(i);
          }
      
          cout << ans << endl;
      }
      
      int main() {
          // NOI 竞赛必备优化:加速 I/O 效率
          ios::sync_with_stdio(false);
          cin.tie(0);
      
          solve();
          return 0;
      }
      

      二、 复杂度分析思考过程

      1. 时间复杂度:O(n)O(n)

      • 推演过程
        • 外层循环遍历 nn 个元素。
        • 内部虽然有 while 循环,但观察每个下标 ii:它最多只会被 push_back 进队列一次,也最多只会被从队首或队尾 pop 出去一次。
        • 均摊分析:所有队列操作的总次数上限是 2n2n
      • 结论:总时间复杂度为线性 O(n)O(n)。即使 n,kn, k 均达到 10510^5,计算量也仅在 10510^5 级别,1.0s 时限内绰绰有余。

      2. 空间复杂度:O(n)O(n)

      • 我们需要一个 O(n)O(n)dp 数组来记录状态。
      • 单调队列在最坏情况下(数据单调递增)可能存储 kk 个元素,空间为 O(k)O(k)
      • 结论:总空间复杂度为 O(n)O(n)

      三、 时间复杂度优化建议 (NOI 提高组技巧)

      1. 数组模拟双端队列 (Manual Deque)std::deque 内部为了支持复杂的内存块管理,常数较大。在 NOI 考场上,如果时限给得非常紧(如 0.5s 处理 10610^6 数据),手写数组模拟队列可以快 2-3 倍:

        int q[100005]; 
        int head = 0, tail = -1;
        // push_back: q[++tail] = i;
        // pop_back: tail--;
        // pop_front: head++;
        
      2. 原地滚动优化: 如果题目只要求最终的 ans,且内存极其受限,可以发现 dp[i]dp[i] 只依赖于前 kk 个值,但由于单调队列需要引用历史下标的 dpdp 值,完全滚动较为复杂。在 256MB 的限制下,O(n)O(n) 存储是绝对安全的。

      3. 快读 (Fast I/O): 对于 10510^5 量级的整数输入,ios::sync_with_stdio(false) 已经足够。如果数据量提升到 10610^6,建议使用 getchar() 版本的自定义读入函数。


      四、 总结:读题关键词与思维锚点

      在 NOI 竞赛中,看到以下关键词的组合,应立刻定位到“单调队列优化 DP”:

      1. “子序列” (Subsequence):考虑 DP 状态 dp[i]dp[i] 表示以第 ii 个数结尾的结果。
      2. “相邻下标距离限制 kk:意味着 dp[i]dp[i] 的来源范围被限定在一个滑动窗口内 [ik,i1][i-k, i-1]
      3. “最大和”:转移方程中涉及 max\max 操作,且是在滑动窗口内找最大。
      4. N,K105N, K \ge 10^5:排除 O(NK)O(NK) 暴力,目标必须是线性 O(N)O(N)

      教练寄语: 单调队列优化 DP 的本质是决策排除。新产生的决策点如果比旧的决策点更强且更年轻,那么旧的就可以永远被丢弃。掌握了这种“优胜劣汰”的筛选法,你就掌握了线性优化 DP 的精髓。加油!

      • 1

      信息

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