把相邻给定点之间需要补的新增点数当作代价,做 O(n^2k) 的点列 DP,先最大化能选到的给定点数量,答案再加上 k。
OJ: luogu
题目 ID: P8816
难度:普及+/提高
标签:动态规划坐标搜索dp
日期: 2026-06-19 13:10
题意
给出 n 个整数点,还允许你额外添加 k 个整数点。
要求从这些点里选出一个序列,使得相邻两点距离恰好为 1,并且坐标都单调不减。也就是说,每一步只能:
- 向右走一格
- 或向上走一格
问这样的点列最大能有多长。
思路
先看小数据暴力:
#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 序列
// 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-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 后面,就计算:
然后转移:
dp[j][t+need] = max(dp[j][t+need], dp[i][t] + 1)
最后找到最多能选到的给定点数 best,答案就是:
best + k
DP 公式
两个给定点
设
设
公式解释:两个给定点之间的曼哈顿步数固定,中间缺多少整数点也随之固定。dp_{i,t} 记录在新增点预算消耗为 t 时最多选了多少给定点;剩余新增点总能继续补在路径末端,所以最终长度再统一加上 k。
样例 1 DP 表格
以样例 1 为例:
| 编号 |
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|
| 坐标 |
关键转移和
| 转移 |
更新 | |
|---|---|---|
| 0 | ||
| 0 | ||
| 0 | ||
| 1 | ||
| 1 |
最优链:
代码
#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;
}复杂度
- 时间复杂度:
- 空间复杂度:
总结
这题最重要的化简是:
- 先把“路径总长度”转成“选中给定点个数 + k”
这样新增点就只剩下“作为代价连接给定点”的作用,整题自然落到一个带资源限制的点列 DP 上。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
