通过动态规划与精妙的贪心推导,推断出只需修改候选山谷点自身的局部最优性,利用 O(NK) DP 求解。
OJ: roj
题目 ID: 10016
难度:提高
标签:动态规划贪心
创建: 2026-10-10 07:12
更新: 2026-10-10 07:12
目录
形式化题目
给定一个长度为
数据范围:
正解
思路
首先,题意要求最大化“山谷点”的值之和。我们有至多
关键观察如下:
- **只有不相邻的位置才能同时是山谷点。**因为若
和 均为山谷点,根据定义必然推出 和 的矛盾。 - 要让某个非原生山谷点
成为山谷点,最优策略必然是直接修改 。 如果我们去修改 或 变小,这不仅消耗了修改次数,反而会让它们更小,导致 更加难以成为山谷点。因此,如果 要成为山谷点,它的相邻元素在最优解中一定不能被修改,保留原值。 既然 和 保持原值,要使得 成为山谷点并最大化它的值,我们应该将 修改为 。 - 对于原数组中已经是山谷点的元素,我们绝对不会主动去把它变小。
如果把它变小,只会白白损失它的价值;如果放弃它,试图让其相邻元素成为山谷点,相邻元素修改后能达到的最大值必定
,收益不增反降且浪费了修改次数。 因此,原生山谷点如果被放弃,唯一的可能原因是为了在它的左右两侧(即 和 )同时构造出两个新的山谷点,以“一换二”的形式获得更大价值。
基于上述观察,我们可以使用动态规划解决此题:
设 dp[i][j][0] 表示考虑前 dp[i][j][1] 表示考虑前
状态转移如下:
- 若第
个位置在原数组中已经是山谷点(满足 且 ): - 它可以直接成为山谷点(无需消耗修改次数),由于相邻不能为山谷点,故从
dp[i-1][j][0]转移过来:dp[i][j][1] = dp[i-1][j][0] + a[i] - 若它不作为山谷点,那么根据前文分析,这必定是因为在第
处构造了一个山谷点破坏了它(为将来的“一换二”铺路)。因此直接从 dp[i-1][j][1]转移:dp[i][j][0] = dp[i-1][j][1]
- 它可以直接成为山谷点(无需消耗修改次数),由于相邻不能为山谷点,故从
- 若第
个位置在原数组中不是山谷点: - 要让它成为山谷点,我们需要消耗 1 次修改次数,并将其值改为
。前提是 : dp[i][j][1] = dp[i-1][j-1][0] + \min(a_{i-1}-1, a_{i+1}-1) - 若它不成为山谷点,那么它可以承接前一个位置无论是否为山谷点的最大值:
dp[i][j][0] = \max(dp[i-1][j][0], dp[i-1][j][1])
- 要让它成为山谷点,我们需要消耗 1 次修改次数,并将其值改为
最终答案为所有状态的最大值。由于判断需要用到
代码
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-10 07:12
# update_at: 2026-10-10 08:23
import sys
def solve() -> None:
"""dp[i][j][s]:前 i 个位置用了 j 次修改、i 是否为山谷点时的最大价值。"""
data = iter(map(int, sys.stdin.buffer.read().split()))
try:
n, k = next(data), next(data)
except StopIteration:
return
a = [0] + [next(data) for _ in range(n)] + [0] # a[1..n],两端补 0 便于取邻居
# 转移只依赖 i-1 一行,所以滚动成一维:prev 是 i-1 行,cur 是第 i 行;状态量 O(nk) 降到 O(k)
prev0, prev1 = [0] * (k + 1), [0] * (k + 1)
cur0, cur1 = [0] * (k + 1), [0] * (k + 1)
ans = 0
for i in range(2, n): # 首尾都不可能是山谷点
left, mid, right = a[i - 1], a[i], a[i + 1]
is_valley = mid < left and mid < right # 原本就是山谷点,不花修改次数
best_lowered = min(left, right) - 1 # 花 1 次修改后 a[i] 能达到的最大值
for j in range(k + 1):
if is_valley:
cur1[j] = prev0[j] + mid # 保留它:前一个位置不能是山谷点
cur0[j] = prev1[j] # 放弃它:只能因为前一个位置成了山谷点
else:
cur1[j] = prev0[j - 1] + best_lowered if j else 0
cur0[j] = max(prev0[j], prev1[j])
ans = max(ans, cur0[j], cur1[j])
prev0, cur0 = cur0, prev0
prev1, cur1 = cur1, prev1
print(ans)
if __name__ == "__main__":
solve()/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-10 07:12
* update_at: 2026-10-10 08:23
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 2005; // n <= 2000
ll a[MAXN]; // a[i]:第 i 个位置的初始值,下标从 1 开始
ll dp[MAXN][MAXN][2]; // dp[i][j][s]:只看前 i 个位置、用了 j 次修改,
// s = 0 表示 i 不是山谷点,s = 1 表示 i 是山谷点
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k;
if (!(cin >> n >> k)) return 0;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
ll ans = 0;
for (int i = 2; i < n; i++) { // 首尾都不可能是山谷点
for (int j = 0; j <= k; j++) {
if (a[i] < a[i - 1] && a[i] < a[i + 1]) {
// 原本就是山谷点:不修改就能取值;若它不当山谷点,只能是前一个位置成了山谷点
dp[i][j][1] = dp[i - 1][j][0] + a[i];
dp[i][j][0] = dp[i - 1][j][1];
} else {
// 原本不是山谷点:花 1 次修改把 a[i] 改成 min(a[i-1]-1, a[i+1]-1)
if (j > 0) {
dp[i][j][1] = dp[i - 1][j - 1][0] + min(a[i - 1] - 1, a[i + 1] - 1);
}
dp[i][j][0] = max(dp[i - 1][j][0], dp[i - 1][j][1]);
}
ans = max(ans, dp[i][j][0]);
ans = max(ans, dp[i][j][1]);
}
}
cout << ans << "\n";
return 0;
}复杂度
- 时间复杂度:状态数为
,转移时间为 ,总时间复杂度为 。 - 空间复杂度:
。可以使用滚动数组优化至 ,但对于 的数据开普通三维数组仅约 左右空间,满足常规内存限制。
总结
- 本题在设计 DP 状态与转移时,很容易误以为原生山谷点可以随便作为“非山谷点”处理而继承任意状态。事实上,“放弃”一个原生山谷点的代价必须被严格约束在“前一个位置被迫成为山谷点”这一特殊情况下,通过
dp[i][j][0] = dp[i-1][j][1]完美地做到了这种状态转移,否则可能会因违背贪心最优性而算入错误情况。 - 挖掘出**“要想把某点变成山谷点,只需要且只能修改它自身,其余相邻点维持原值”**是解题的核心,这保证了每次修改后的最佳取值可以极其简单地用
求得,无需记录每一个元素实际被修改到了何值。
验证记录(R2,与官方 std.cpp 三方对拍)
⚠ 本题无本地
data/⇒check_new_analysis报「无 data/(跳过)」= 空转, 不构成证据(T1/T65)。证据来源是官方std.cpp(证据等级 ★★★★)。
结论
与官方 std.cpp 三方一致(不写「验证通过」):
main.cpp ≡ std.cpp ≡ main.py,在 186 组合成用例(含 n=k=2000 极值)上
逐字节 186/186 无分歧;另在 84,915 组穷举小数据上与独立 brute 三方无分歧。
std.cpp 语义核对(T67)
| 核对项 | 结论 |
|---|---|
★ 三维 dp 是否属实 |
✅ 属实:ll dp[maxn][maxn][2](maxn=2005,64.3MB 全局数组)⇒ 34 行确实装得下 |
| 循环结构 | 单组(cin>>n>>k 一次,无 while/EOF);拼两组只答第一组,三方一致 |
| 「变幻」是什么 | 任选位置改小;非原生山谷点花 1 次改成 min(a[i-1]-1, a[i+1]-1) |
| 输入变量是否都参与 | n ✅(i<n 上界)、k ✅(j<=k 上界) |
std 分支冗余 |
第 2 支 else if(a[i]<a[i-1]) 与第 3 支 else 函数体逐字相同 ⇒ 合并为一支,语义等价 |
std 的 UB 风险 |
空输入时 int n,k; 未初始化即使用(UB);main.cpp 用 if(!(cin>>n>>k)) return 0; 规避 |
V1 反事实表(真进程 sed 变异,186 用例)
| 变体 | 失败点数 | 判定 |
|---|---|---|
| 删掉非原生山谷点分支 | 123/186 | 承重 |
min(a-1,b-1) → min(a,b) |
139/186 | 承重 |
dp[i-1][j-1][0] → dp[i-1][j][0] |
34/186 | 承重 |
ans 不计 dp[..][1] |
63/186 | 承重 |
循环 j<=k-1 |
48/186 | 承重(k 承重) |
ans 初值 1 |
46/186 | 承重(答案可为 0) |
★ dp[i][j][0]=dp[i-1][j][1] → max(...) |
0/186 | ★ 不承重(如实报告) |
★ 循环从 i=1 起 |
0/186 | ★ 不承重(a[0]=0 补零使端点贡献恒 min(-1,·)=-1<0,结构性 no-op) |
★ 另在 6015/30948 组穷举 + 20000 组随机上做 V1..V7 全表,结论一致。
边界候选不可分辨(V8)
「修改后的值可为 0」 vs 「必须 ≥1」两种读法分别 brute:
失败点集合都是 ∅(66,481 组)⇒ ★ 数据无法区分,如实报告(std 允许 0)。
V9 UBSan + 对照组
- 主测:
-fsanitize=undefined -fno-sanitize-recover=undefined跑全部 186 用例 ⇒ stderr 0 行;-ftrapv全过 - ★ 对照组:强制
-INT_MIN的对照程序在同一命令下报negation of -2147483648 cannot be represented in type 'int'+Abort trap: 6⇒ 证明该 UBSan 命令确实有分辨力(T30闸门分辨力)
修改内容(算法未改)
main.cpp:补标准文件头;long long→typedef long long ll;全局数组加中文语义注释main.py:补 shebang + 作者头;solve() -> None类型标注;iter(map(int, sys.stdin.buffer.read().split()))+next()顺序消费; ★ 3D 表 → 滚动数组(同算法):峰值 562MB → 9.4MB、2.35s → 0.76s, 输出 186/186 逐字节不变