2 条题解
-
0
你好!我是你的OI教练。
这道题(CSP-J 2022 T3 上升点列)是一道非常有代表性的 动态规划(DP) 题目。它结合了 坐标排序 和 最长上升子序列(LIS) 的思想。
只要你转换一下思考角度,这道题的难度就会大大降低。
1. 核心思维转换:如何利用 k?
题目问的是“序列的最大长度”。这个序列由“原本给定的点”和“自由添加的点”组成。 显然,为了让序列最长,我们肯定会把 个自由点全部用光(如果需要的话,直接接在序列末尾即可)。
关键结论:
$$\text{序列总长度} = \text{被选中的原有整点个数} + \text{自由添加的点数 } k $$所以,我们的目标就变成了:在花费不超过 个自由点的前提下,尽可能多地串联起原本给定的整点。
2. 预处理:排序
因为题目要求序列的横纵坐标“单调不减”( 且 ),这暗示这就是一个二维的 LIS 问题。 为了方便进行 DP 转移(保证我们处理第 个点时,之前的点已经在它“左下方”),我们需要先对所有点进行排序。
- 排序规则:先按 从小到大排;如果 相同,再按 从小到大排。
3. 计算代价:曼哈顿距离
如果我们要把点 和点 连接起来(假设 在 左下方),中间需要填补多少个自由点?
- 两点间的曼哈顿距离是:。 加油!这是一道非常标准的线性 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][...] = dp[prev][...] + dist。 - 这里的
+ dist实际上包含了+ 1(当前原有点)和+ cost(填补的自由点)。
- 这一版思路(新版):
- 存储的是 当前序列中原有整点的数量。
- 转移方程:
dp[i][...] = dp[prev][...] + 1。 - 这里只加 1,非常纯粹地只数“原有坐标点”。
2. 答案计算的区别
- 上一版思路:
- 答案需要计算未使用的 能带来的收益。
- 公式:
Ans = max(dp[i][j] + (k - j))。 - 需要手动把剩下的 加回去。
- 这一版思路:
- 利用结论
总长度 = 原有整点数 + k。 - 公式:
Ans = max(dp[i][j]) + k。 - 这个公式更加直观,它假设我们总是能把 个自由点全部用在序列里(无论是填补中间的空缺,还是接在序列的头尾)。
- 利用结论
3. 总结
这一版思路利用了数学上的恒等关系简化了问题模型。将“混合计算长度”的问题转化为了“在代价限制下这种背包问题式的最长链”问题,代码逻辑上(尤其是转移时的
+1和最后的+k)更加简洁明了,不容易写错。 -
0
你好!作为OI教练,很高兴为你提供这道题目(P8816 [CSP-J 2022] 上升点列)的解题思路。
这道题考察的是 动态规划(DP) 结合 几何性质。由于 的范围比较小(),而坐标范围很大(),这明显是在提示我们算法的复杂度应该与 有关,而不是与坐标大小有关。
以下是逐步的思路提示:
1. 理解几何性质与代价
题目要求序列中的点必须满足横纵坐标单调不减,且相邻两点欧几里得距离为 1。这意味着我们只能向“上”或向“右”走。
-
路径特性:这实际上是在二维平面上走“曼哈顿距离”。
-
连接两点的代价: 假设我们已经有两个给定的点 和 ,其中 且 。 要将 和 连接起来形成合法的上升点列,两点之间需要的步数(即两点间的距离)是 。 但是,因为 和 已经是存在的点,我们需要额外填补的自由点数量是 。
思考:如果 和 之间需要的自由点数量超过了题目给定的 ,还能连接它们吗?
2. 预处理:排序
为了进行动态规划,我们需要保证处理到第 个点时,它前序可能连接的点都已经处理过了。 因为要求序列是“上升”的( 和 都不减),我们可以先对所有给定的 个点进行排序。
- 排序规则:优先按 坐标从小到大排序;如果 相同,则按 坐标从小到大排序。
- 排序后,对于第 个点,它的前驱节点 一定满足 。这样我们就可以线性地进行状态转移了。
3. 设计 DP 状态
我们需要记录当前序列结尾走到哪一个点,以及消耗了多少个自由点 。
-
状态定义: 设 表示:以排序后的第 个给定的点为结尾,且在这个序列中总共额外添加了 个自由点时,该序列所能达到的最大长度。
- 的范围:
- 的范围:
4. 状态转移方程
对于每一个点 (外层循环),我们要枚举它之前的所有点 (内层循环, )来尝试转移。
-
合法性判断:只有当点 在点 的左下方(即 且 )时,才能从 走到 。
-
计算消耗:计算从 走到 需要填充多少个额外的点。设需要的点数为 。
-
转移逻辑: 如果当前剩余的自由点预算 足够支付这个 (即 ),那么我们可以尝试从状态 转移过来。
$$f[i][j] = \max(f[i][j], \ f[p][j - cost] + cost + 1) $$(注意:这里的 是因为我们在 的基础上增加了 个自由点,以及点 本身,长度增加了 。实际上你会发现,这等价于加上了 到 的曼哈顿距离)
5. 初始化与答案计算
-
初始化: 对于任意点 和任意 ,最坏的情况是该序列只包含点 以及 个自由点(自由点可以接在 后面或前面)。 所以初始值 。
-
最终答案: 答案不是简单的 。
我们需要遍历所有的 () 和所有的 ()。 对于状态 ,我们使用了 个自由点,题目允许最多使用 个。如果 ,剩下的 个自由点可以无脑直接接在这个序列的尾部,进一步增加长度。
即:
$$Answer = \max_{1 \le i \le n, \ 0 \le j \le k} \{ f[i][j] + (k - j) \} $$
总结关键点
- 排序是消除后效性的关键。
- DP状态一定要包含“使用了多少个 ”。
- 两点间所需自由点数是 曼哈顿距离 - 1。
- 最后别忘了没用完的 也可以算进长度里。
祝你 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; }代码核心逻辑解析:
-
排序 (
sort):- 将所有点按 优先、 次之的顺序排序。这确保了当我们处理点 时,任何可能作为它“前驱”的点 都已经被处理过(满足 ),从而消除了 DP 的后效性。
-
状态定义 (
dp[i][j]):- :当前序列以排序后的第 个点结尾。
- :连接该序列的过程中,总共已经消耗了 个自由添加的点。
- 值:满足上述条件的最大序列长度。
-
状态转移:
- 计算点 和点 之间的曼哈顿距离 。
- 两者之间需要填补的空位数量(代价)为 。
- 如果当前拥有的自由点预算 大于等于 ,则可以从 转移过来。长度增加 (即填补的 个点加上点 自身)。
-
最终答案计算:
- DP 过程只计算了以“给定点”结尾的情况。
- 如果题目给的 很大,在连到点 并消耗了 个点后,还剩 个点。这些点可以无条件地加在序列末尾,使长度再增加 。所以答案是 。
-
- 1
信息
- ID
- 12651
- 时间
- 1000ms
- 内存
- 32MiB
- 难度
- 5
- 标签
- 递交数
- 1
- 已通过
- 2
- 上传者