[CSP-J 2022] 上升点列

GitHub跳转原题关系图返回列表

把相邻给定点之间需要补的新增点数当作代价,做 O(n^2k) 的点列 DP,先最大化能选到的给定点数量,答案再加上 k。

OJ: luogu

题目 ID: P8816

难度:普及+/提高

标签:动态规划坐标搜索dp

日期: 2026-06-19 13:10

题意

给出 n 个整数点,还允许你额外添加 k 个整数点。

要求从这些点里选出一个序列,使得相邻两点距离恰好为 1,并且坐标都单调不减。也就是说,每一步只能:

  • 向右走一格
  • 或向上走一格

问这样的点列最大能有多长。

思路

先看小数据暴力:

cpp
#include <bits/stdc++.h>
using namespace std;

// brute.cpp:小数据 DFS 枚举进入上升点列的给定点集合,验证最优答案。

const int MAXN = 25;

struct Point {
    int x, y;
};

int n, k;
Point p[MAXN];
int best_given;

bool cmp_point(const Point &a, const Point &b) {
    if (a.x != b.x) {
        return a.x < b.x;
    }
    return a.y < b.y;
}

void dfs(int last, int used, int cnt) {
    best_given = max(best_given, cnt);

    for (int i = last + 1; i <= n; i++) {
        if (p[i].x < p[last].x || p[i].y < p[last].y) {
            continue;
        }

        int need = (p[i].x - p[last].x) + (p[i].y - p[last].y) - 1;
        if (used + need > k) {
            continue;
        }

        dfs(i, used + need, cnt + 1);
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> k;
    for (int i = 1; i <= n; i++) {
        cin >> p[i].x >> p[i].y;
    }

    sort(p + 1, p + n + 1, cmp_point);

    best_given = 1;
    for (int i = 1; i <= n; i++) {
        dfs(i, 0, 1);
    }

    cout << best_given + k << '\n';
    return 0;
}

下面是另一种「01 序列」风格的暴力写法。它按排序后的给定点依次决定“选 / 不选”,递归生成完整选择后,叶子节点统一检查任意两点的横纵坐标差是否都满足限制,并统计可选点数:

另一种暴力写法:01 序列
cpp
// brute_01_style.cpp:01 序列风格暴力,按排序后的点依次决定选或不选。
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 25;

struct Point {
    int x, y;
};

int n, k;
Point p[MAXN];
int choose_point[MAXN]; // choose_point[i] = 0/1,表示第 i 个给定点不选/选
int best_given;

bool cmp_point(const Point &a, const Point &b) {
    if (a.x != b.x) {
        return a.x < b.x;
    }
    return a.y < b.y;
}

int need_points(int last, int cur) {
    return (p[cur].x - p[last].x) + (p[cur].y - p[last].y) - 1;
}

bool check() {
    int last = 0;
    int used_extra = 0;
    for (int i = 1; i <= n; i++) {
        if (choose_point[i] == 0) continue;
        if (last != 0) {
            if (p[i].x < p[last].x || p[i].y < p[last].y) return false;
            used_extra += need_points(last, i);
            if (used_extra > k) return false;
        }
        last = i;
    }
    return true;
}

int calc_answer() {
    int cnt = 0;
    for (int i = 1; i <= n; i++) {
        if (choose_point[i] == 1) cnt++;
    }
    return cnt;
}

void dfs(int dep) {
    if (dep == n + 1) {
        if (check()) {
            int value = calc_answer();
            if (best_given < value) best_given = value;
        }
        return;
    }

    // 第 dep 个给定点的 01 选择:0 不选,1 选。
    for (int i = 0; i <= 1; i++) {
        choose_point[dep] = i;
        dfs(dep + 1);
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> k;
    for (int i = 1; i <= n; i++) {
        cin >> p[i].x >> p[i].y;
    }

    sort(p + 1, p + n + 1, cmp_point);

    best_given = 0;
    dfs(1);

    cout << best_given + k << '\n';
    return 0;
}

brute.cpp 会枚举哪些给定点被选进最终的上升点列,只要下一个点坐标不下降,并且连接它所需的新增点数还没超预算,就继续 DFS。

关键在于先看清两个给定点之间到底要花多少新增点。

假设要从:

  • A(x1, y1) 走到 B(x2, y2)

并且 x2>=x1,y2>=y1x2 >= x1, y2 >= y1

因为每一步只能向右或向上,所以总步数固定是:

  • (x2-x1) + (y2-y1)

而这段路的两个端点已经是给定点,所以中间真正需要补上的新增点数就是:

  • (x2-x1) + (y2-y1) - 1

两点代价表

起点到终点 总步数 需要补的新增点数
(1,2) -> (3,2) 2 1
(3,3) -> (3,4) 1 0
(2,2) -> (5,5) 6 5

接下来有个很关键的化简:

如果一条路径里一共选了 g 个给定点,并且为了把它们连起来已经用了 t 个新增点,那么这条路径当前长度就是:

  • g + t

而剩下的 k-t 个新增点总能继续接到路径两端,再把长度增加 k-t

所以最终总长度恒为:

  • g + k

也就是说,题目本质上变成了:

  • 在新增点消耗不超过 k 的前提下,最多能选多少个给定点

于是做 DP。

先按 (x 升序, y 升序) 排序,设:

  • dp[i][t] 表示以第 i 个给定点结尾,恰好用了 t 个新增点时,最多能选多少个给定点

j 能接在 i 后面,就计算:

  • need=(xjxi)+(yjyi)1need = (x_j-x_i) + (y_j-y_i) - 1

然后转移:

  • dp[j][t+need] = max(dp[j][t+need], dp[i][t] + 1)

最后找到最多能选到的给定点数 best,答案就是:

  • best + k

DP 公式

两个给定点 i,ji,j 能相连时,所需新增点数为:

need(i,j)=(xjxi)+(yjyi)1 need(i,j)=(x_j-x_i)+(y_j-y_i)-1

dpi,tdp_{i,t} 表示以第 ii 个给定点结尾,恰好用了 tt 个新增点时,最多能选到多少个给定点。若 xjxix_j\geqslant x_iyjyiy_j\geqslant y_i,则:

dpj,t+need(i,j)=max(dpj,t+need(i,j), dpi,t+1) dp_{j,t+need(i,j)}=\max(dp_{j,t+need(i,j)},\ dp_{i,t}+1)

best=maxi,0tkdpi,tbest=\max_{i,0\leqslant t\leqslant k}dp_{i,t},最终答案为:

best+k best+k

公式解释:两个给定点之间的曼哈顿步数固定,中间缺多少整数点也随之固定。dp_{i,t} 记录在新增点预算消耗为 t 时最多选了多少给定点;剩余新增点总能继续补在路径末端,所以最终长度再统一加上 k

样例 1 DP 表格

以样例 1 为例:n=8,k=2n = 8, k = 2,排序后的给定点为:

编号 ii 1 2 3 4 5 6 7 8
坐标 (1,2)(1,2) (2,2)(2,2) (3,1)(3,1) (3,2)(3,2) (3,3)(3,3) (3,6)(3,6) (5,3)(5,3) (5,5)(5,5)

关键转移和 dpi,tdp_{i,t} 的非平凡值(初始 dpi,0=1dp_{i,0} = 1):

转移 iji \to j need(i,j)need(i,j) 更新
121 \to 2 0 dp2,0=2dp_{2,0} = 2
242 \to 4 0 dp4,0=3dp_{4,0} = 3
454 \to 5 0 dp5,0=4dp_{5,0} = 4
575 \to 7 1 dp7,1=5dp_{7,1} = 5
787 \to 8 1 dp8,2=6dp_{8,2} = 6

最优链:(1,2)(2,2)(3,2)(3,3)(5,3)(5,5)(1,2) \to (2,2) \to (3,2) \to (3,3) \to (5,3) \to (5,5),选了 6 个给定点,消耗 need=0+0+0+1+1=2need = 0+0+0+1+1 = 2 个新增点。

best=6best = 6,最终答案 =best+k=6+2=8= best + k = 6 + 2 = 8,与样例输出一致。

代码

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 505;
const int MAXK = 105;

struct Point {
    int x, y;
};

int n, k;
Point p[MAXN];
int dp[MAXN][MAXK]; // dp[i][t]:以第 i 个给定点结尾,恰好用了 t 个新增点时,最多能选多少个给定点

bool cmp_point(const Point &a, const Point &b) {
    if (a.x != b.x) {
        return a.x < b.x;
    }
    return a.y < b.y;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> k;
    for (int i = 1; i <= n; i++) {
        cin >> p[i].x >> p[i].y;
    }

    sort(p + 1, p + n + 1, cmp_point);

    for (int i = 1; i <= n; i++) {
        dp[i][0] = 1;
    }

    int best = 1;

    for (int i = 1; i <= n; i++) {
        for (int j = i + 1; j <= n; j++) {
            if (p[j].x < p[i].x || p[j].y < p[i].y) {
                continue;
            }

            int need = (p[j].x - p[i].x) + (p[j].y - p[i].y) - 1;
            if (need > k) {
                continue;
            }

            for (int t = 0; t + need <= k; t++) {
                if (dp[i][t] == 0) {
                    continue;
                }
                dp[j][t + need] = max(dp[j][t + need], dp[i][t] + 1);
            }
        }
    }

    for (int i = 1; i <= n; i++) {
        for (int t = 0; t <= k; t++) {
            best = max(best, dp[i][t]);
        }
    }

    // 若一条路径中选了 best 个给定点,剩余新增点总能放到路径两端继续延长。
    cout << best + k << '\n';
    return 0;
}

复杂度

  • 时间复杂度:O(n2k)O(n^2 k)
  • 空间复杂度:O(nk)O(nk)

总结

这题最重要的化简是:

  • 先把“路径总长度”转成“选中给定点个数 + k”

这样新增点就只剩下“作为代价连接给定点”的作用,整题自然落到一个带资源限制的点列 DP 上。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析