2 条题解

  • 0
    @ 2025-11-28 16:50:49

    你好!我是你的OI教练。

    这道题(CSP-J 2022 T3 上升点列)是一道非常有代表性的 动态规划(DP) 题目。它结合了 坐标排序最长上升子序列(LIS) 的思想。

    只要你转换一下思考角度,这道题的难度就会大大降低。

    1. 核心思维转换:如何利用 k?

    题目问的是“序列的最大长度”。这个序列由“原本给定的点”和“自由添加的点”组成。 显然,为了让序列最长,我们肯定会把 kk 个自由点全部用光(如果需要的话,直接接在序列末尾即可)。

    关键结论

    $$\text{序列总长度} = \text{被选中的原有整点个数} + \text{自由添加的点数 } k $$

    所以,我们的目标就变成了:在花费不超过 kk 个自由点的前提下,尽可能多地串联起原本给定的整点。

    2. 预处理:排序

    因为题目要求序列的横纵坐标“单调不减”(xi+1xix_{i+1} \ge x_iyi+1yiy_{i+1} \ge y_i),这暗示这就是一个二维的 LIS 问题。 为了方便进行 DP 转移(保证我们处理第 ii 个点时,之前的点已经在它“左下方”),我们需要先对所有点进行排序

    • 排序规则:先按 xx 从小到大排;如果 xx 相同,再按 yy 从小到大排。

    3. 计算代价:曼哈顿距离

    如果我们要把点 A(x1,y1)A(x_1, y_1) 和点 B(x2,y2)B(x_2, y_2) 连接起来(假设 AABB 左下方),中间需要填补多少个自由点?

    • 两点间的曼哈顿距离是:d=(x2x1)+(y2y1)d = (x_2 - x_1) + (y_2 - y_1)。 加油!这是一道非常标准的线性 DP 变形题。

    基于你提供的“转换角度”的思路,通过计算 “最多能选出的原有整点数量” 来解题。

    参考代码

    #include <iostream>
    #include <algorithm>
    #include <vector>
    
    using namespace std;
    
    const int MAXN = 505;
    const int MAXK = 105;
    
    struct Point {
        int x, y;
    };
    
    // 排序规则:x 坐标优先,x 相同时 y 坐标优先
    bool cmp(Point a, Point b) {
        if (a.x != b.x) return a.x < b.x;
        return a.y < b.y;
    }
    
    int n, k;
    Point p[MAXN];
    // dp[i][j] 定义:以第 i 个点结尾,且连接过程中消耗了 j 个自由点时,
    // 序列中包含的【原有整点】的最大数量。
    int dp[MAXN][MAXK];
    
    int main() {
        ios::sync_with_stdio(false);
        cin.tie(0);
    
        cin >> n >> k;
        for (int i = 1; i <= n; ++i) {
            cin >> p[i].x >> p[i].y;
        }
    
        // 1. 预处理:排序
        sort(p + 1, p + n + 1, cmp);
    
        // 2. 动态规划
        for (int i = 1; i <= n; ++i) {
            // 初始化:每个点至少包含它自己这 1 个原有整点
            // 无论消耗多少 k,任何点都可以作为序列的起点(原有整点数=1)
            for (int j = 0; j <= k; ++j) {
                dp[i][j] = 1;
            }
    
            // 枚举前驱节点 prev
            for (int prev = 1; prev < i; ++prev) {
                // 必须满足坐标单调不减
                if (p[prev].y > p[i].y) continue;
    
                // 计算两点间的曼哈顿距离
                int dist = (p[i].x - p[prev].x) + (p[i].y - p[prev].y);
                
                // 计算需要的自由点代价:距离 - 1
                // 例如:(1,1) 到 (1,2) 距离为1,需要 0 个自由点
                //      (1,1) 到 (2,2) 距离为2,需要 1 个自由点
                int cost = dist - 1;
    
                // 如果代价在可承受范围内
                if (cost <= k) {
                    // 状态转移:枚举 prev 已经消耗的代价 j
                    // 新的总代价 = prev的代价(j) + 连接这两点的代价(cost)
                    for (int j = 0; j <= k - cost; ++j) {
                        // 状态含义变化:这里只加 1,因为我们只统计“原有整点”的数量
                        dp[i][j + cost] = max(dp[i][j + cost], dp[prev][j] + 1);
                    }
                }
            }
        }
    
        // 3. 计算答案
        // 题目思路结论:序列最大长度 = 选中的原有整点数 + k
        int max_original_points = 0;
        for (int i = 1; i <= n; ++i) {
            for (int j = 0; j <= k; ++j) {
                max_original_points = max(max_original_points, dp[i][j]);
            }
        }
    
        // 即使 k 没用完,也可以补在末尾,所以直接 + k
        cout << max_original_points + k << endl;
    
        return 0;
    }
    

    思路对比分析

    这两个思路虽然最终都能 AC,但在 DP 状态定义的物理含义答案计算方式 上有本质区别:

    1. 状态定义的区别

    • 上一版思路(标准版):
      • dp[i][j]dp[i][j] 存储的是 当前序列的总长度(包含原有点 + 已使用的自由点)。
      • 转移方程:dp[i][...] = dp[prev][...] + dist
      • 这里的 + dist 实际上包含了 + 1(当前原有点)和 + cost(填补的自由点)。
    • 这一版思路(新版):
      • dp[i][j]dp[i][j] 存储的是 当前序列中原有整点的数量
      • 转移方程:dp[i][...] = dp[prev][...] + 1
      • 这里只加 1,非常纯粹地只数“原有坐标点”。

    2. 答案计算的区别

    • 上一版思路:
      • 答案需要计算未使用的 kk 能带来的收益。
      • 公式:Ans = max(dp[i][j] + (k - j))
      • 需要手动把剩下的 kjk-j 加回去。
    • 这一版思路:
      • 利用结论 总长度 = 原有整点数 + k
      • 公式:Ans = max(dp[i][j]) + k
      • 这个公式更加直观,它假设我们总是能把 kk 个自由点全部用在序列里(无论是填补中间的空缺,还是接在序列的头尾)。

    3. 总结

    这一版思路利用了数学上的恒等关系简化了问题模型。将“混合计算长度”的问题转化为了“在代价限制下这种背包问题式的最长链”问题,代码逻辑上(尤其是转移时的 +1 和最后的 +k)更加简洁明了,不容易写错。

    • 0
      @ 2025-11-28 16:45:31

      你好!作为OI教练,很高兴为你提供这道题目(P8816 [CSP-J 2022] 上升点列)的解题思路。

      这道题考察的是 动态规划(DP) 结合 几何性质。由于 nn 的范围比较小(n500n \le 500),而坐标范围很大(10910^9),这明显是在提示我们算法的复杂度应该与 nn 有关,而不是与坐标大小有关。

      以下是逐步的思路提示:

      1. 理解几何性质与代价

      题目要求序列中的点必须满足横纵坐标单调不减,且相邻两点欧几里得距离为 1。这意味着我们只能向“上”或向“右”走。

      • 路径特性:这实际上是在二维平面上走“曼哈顿距离”。

      • 连接两点的代价: 假设我们已经有两个给定的点 A(x1,y1)A(x_1, y_1)B(x2,y2)B(x_2, y_2),其中 x1x2x_1 \le x_2y1y2y_1 \le y_2。 要将 AABB 连接起来形成合法的上升点列,两点之间需要的步数(即两点间的距离)是 dist=(x2x1)+(y2y1)dist = (x_2 - x_1) + (y_2 - y_1)。 但是,因为 AABB 已经是存在的点,我们需要额外填补的自由点数量是 dist1dist - 1

        思考:如果 AABB 之间需要的自由点数量超过了题目给定的 kk,还能连接它们吗?

      2. 预处理:排序

      为了进行动态规划,我们需要保证处理到第 ii 个点时,它前序可能连接的点都已经处理过了。 因为要求序列是“上升”的(xxyy 都不减),我们可以先对所有给定的 nn 个点进行排序。

      • 排序规则:优先按 xx 坐标从小到大排序;如果 xx 相同,则按 yy 坐标从小到大排序。
      • 排序后,对于第 ii 个点,它的前驱节点 jj 一定满足 j<ij < i。这样我们就可以线性地进行状态转移了。

      3. 设计 DP 状态

      我们需要记录当前序列结尾走到哪一个点,以及消耗了多少个自由点 kk

      • 状态定义: 设 f[i][j]f[i][j] 表示:以排序后的第 ii 个给定的点为结尾,且在这个序列中总共额外添加jj 个自由点时,该序列所能达到的最大长度

        • ii 的范围:1n1 \sim n
        • jj 的范围:0k0 \sim k

      4. 状态转移方程

      对于每一个点 ii(外层循环),我们要枚举它之前的所有点 pp(内层循环, 1p<i1 \le p < i)来尝试转移。

      • 合法性判断:只有当点 pp 在点 ii 的左下方(即 xpxix_p \le x_iypyiy_p \le y_i)时,才能从 pp 走到 ii

      • 计算消耗:计算从 pp 走到 ii 需要填充多少个额外的点。设需要的点数为 costcost

        cost=(xixp)+(yiyp)1cost = (x_i - x_p) + (y_i - y_p) - 1
      • 转移逻辑: 如果当前剩余的自由点预算 jj 足够支付这个 costcost(即 jcostj \ge cost),那么我们可以尝试从状态 f[p][jcost]f[p][j - cost] 转移过来。

        $$f[i][j] = \max(f[i][j], \ f[p][j - cost] + cost + 1) $$

        (注意:这里的 +cost+1+ cost + 1 是因为我们在 pp 的基础上增加了 costcost 个自由点,以及点 ii 本身,长度增加了 cost+1cost+1。实际上你会发现,这等价于加上了 ppii 的曼哈顿距离)

      5. 初始化与答案计算

      • 初始化: 对于任意点 ii 和任意 jj,最坏的情况是该序列只包含点 ii 以及 jj 个自由点(自由点可以接在 ii 后面或前面)。 所以初始值 f[i][j]=j+1f[i][j] = j + 1

      • 最终答案: 答案不是简单的 f[n][k]f[n][k]

        我们需要遍历所有的 ii (1n1 \sim n) 和所有的 jj (0k0 \sim k)。 对于状态 f[i][j]f[i][j],我们使用了 jj 个自由点,题目允许最多使用 kk 个。如果 j<kj < k,剩下的 kjk - j 个自由点可以无脑直接接在这个序列的尾部,进一步增加长度。

        即:

        $$Answer = \max_{1 \le i \le n, \ 0 \le j \le k} \{ f[i][j] + (k - j) \} $$

      总结关键点

      1. 排序是消除后效性的关键。
      2. DP状态一定要包含“使用了多少个 kk”。
      3. 两点间所需自由点数是 曼哈顿距离 - 1
      4. 最后别忘了没用完的 kk 也可以算进长度里。

      祝你 AC!如果有具体代码实现的细节卡住了,可以再问我。


      这是一个基于动态规划的 C++ 标准解答代码。代码中包含了必要的注释来解释关键步骤。

      #include <iostream>
      #include <algorithm>
      #include <vector>
      
      using namespace std;
      
      // 定义点的结构体
      struct Point {
          int x, y;
      };
      
      // 排序规则:先按 x 坐标从小到大,若 x 相同则按 y 坐标从小到大
      bool cmp(Point a, Point b) {
          if (a.x != b.x) return a.x < b.x;
          return a.y < b.y;
      }
      
      const int MAXN = 505;
      const int MAXK = 105;
      
      int n, k;
      Point p[MAXN];
      // dp[i][j] 表示以第 i 个点为结尾,且使用了 j 个自由点时的最大序列长度
      int dp[MAXN][MAXK];
      
      int main() {
          // 优化 I/O 速度
          ios::sync_with_stdio(false);
          cin.tie(0);
      
          cin >> n >> k;
          for (int i = 1; i <= n; ++i) {
              cin >> p[i].x >> p[i].y;
          }
      
          // 1. 排序,保证处理第 i 个点时,其前驱点一定在索引小于 i 的位置
          sort(p + 1, p + n + 1, cmp);
      
          int ans = 0;
      
          // 2. 动态规划过程
          for (int i = 1; i <= n; ++i) {
              // 初始化:每个点自身可以作为一个序列的开始
              // 如果使用了 j 个自由点,那么长度就是 1 (点 i 本身) + j (自由点)
              for (int j = 0; j <= k; ++j) {
                  dp[i][j] = j + 1;
              }
      
              // 枚举前驱点 prev
              for (int prev = 1; prev < i; ++prev) {
                  // 如果 prev 的 y 坐标比 i 大,则无法满足“单调不减”,跳过
                  if (p[prev].y > p[i].y) continue;
      
                  // 计算曼哈顿距离
                  int dist = (p[i].x - p[prev].x) + (p[i].y - p[prev].y);
                  
                  // 计算连接两点需要的额外自由点数量
                  // 距离为 dist,中间需要填补 dist - 1 个点
                  int cost = dist - 1;
      
                  // 如果需要的点数超过了 k,或者超过了当前枚举的 j 的上限(在后面循环判断),则无法直接连接
                  if (cost <= k) {
                      // 更新状态:枚举使用了多少个自由点 j
                      // 我们至少需要 cost 个点才能从 prev 连到 i,所以 j 从 cost 开始
                      for (int j = cost; j <= k; ++j) {
                          // 状态转移方程:
                          // 新长度 = 前驱状态长度 + 连接所需的代价(cost) + 1(点 i 本身)
                          // 注意:dp[prev][j - cost] 已经包含了 prev 点,
                          // 这里 + (dist) 等价于 + cost + 1
                          dp[i][j] = max(dp[i][j], dp[prev][j - cost] + dist);
                      }
                  }
              }
      
              // 3. 更新全局答案
              // 对于以 i 结尾且消耗了 j 个自由点的状态,
              // 剩下的 (k - j) 个自由点可以直接追加在序列末尾增加长度
              for (int j = 0; j <= k; ++j) {
                  ans = max(ans, dp[i][j] + (k - j));
              }
          }
      
          cout << ans << endl;
      
          return 0;
      }
      

      代码核心逻辑解析:

      1. 排序 (sort)

        • 将所有点按 xx 优先、yy 次之的顺序排序。这确保了当我们处理点 ii 时,任何可能作为它“前驱”的点 prevprev 都已经被处理过(满足 prev<iprev < i),从而消除了 DP 的后效性。
      2. 状态定义 (dp[i][j])

        • ii:当前序列以排序后的第 ii 个点结尾。
        • jj:连接该序列的过程中,总共已经消耗了 jj 个自由添加的点。
        • 值:满足上述条件的最大序列长度。
      3. 状态转移

        • 计算点 ii 和点 prevprev 之间的曼哈顿距离 dist=Δx+Δydist = \Delta x + \Delta y
        • 两者之间需要填补的空位数量(代价)为 cost=dist1cost = dist - 1
        • 如果当前拥有的自由点预算 jj 大于等于 costcost,则可以从 dp[prev][jcost]dp[prev][j-cost] 转移过来。长度增加 distdist(即填补的 costcost 个点加上点 ii 自身)。
      4. 最终答案计算

        • DP 过程只计算了以“给定点”结尾的情况。
        • 如果题目给的 kk 很大,在连到点 ii 并消耗了 jj 个点后,还剩 kjk-j 个点。这些点可以无条件地加在序列末尾,使长度再增加 kjk-j。所以答案是 max(dp[i][j]+kj)\max(dp[i][j] + k - j)
      • 1

      信息

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