小苯拿序列 - 题解
题目链接:牛客网 - 小苯拿序列
题目简述
小苯面前有一个长度为 的序列
,他需要花费
的代价拿走序列。
为了最小化代价,他可以提前进行任意次操作:
- 选择区间
,花费该区间最大值
的代价
- 将区间内所有元素变为区间最小值
求拿走序列的最小总花费。
一、初见此题:动态规划的直觉
看到这道题,很自然地联想到区间操作 + 最优化问题,这不就是经典的区间DP吗?
1.1 状态设计
定义 表示处理前
个元素的最小代价。
对于第 个位置,我们有两种选择:
- 不操作:直接以原值
的代价拿走,
- 对某个以
结尾的区间
操作:
- 操作花费:
- 操作后代价:
- 总代价:
- 操作花费:
1.2 代码实现
#include <bits/stdc++.h>
using namespace std;
void solve() {
int n;
cin >> n;
vector<long long> a(n + 1);
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
vector<long long> dp(n + 1, 0);
for (int i = 1; i <= n; i++) {
// 不操作
dp[i] = dp[i - 1] + a[i];
// 枚举左端点
long long minVal = a[i], maxVal = a[i];
for (int j = i; j >= 1; j--) {
minVal = min(minVal, a[j]);
maxVal = max(maxVal, a[j]);
// 对 [j, i] 操作
long long cost = dp[j - 1] + maxVal + minVal * (i - j + 1);
dp[i] = min(dp[i], cost);
}
}
cout << dp[n] << "\n";
}
int main() {
ios:: sync_with_stdio(false);
cin.tie(0);
int T;
cin >> T;
while (T--) {
solve();
}
return 0;
}
复杂度分析:
- 时间复杂度:
- 空间复杂度:
这个做法是正确的,可以通过本题。
但... 总觉得哪里不对劲。 的复杂度对于
来说有些勉强,而且这个转移方程似乎在暗示什么...
二、观察数据的规律
让我们观察几组样例,看看能否发现规律。
2.1 样例分析
| 样例 | 数组 |
原始和 |
实际答案 | |
|---|---|---|---|---|
| 1 | [5, 1, 3, 2] | 11 | 5 + 4×1 = 9 | 9 |
| 2 | [1, 1, 1, 1] | 4 | 1 + 4×1 = 5 | 4 |
| 3 | [10, 2, 3] | 15 | 10 + 3×2 = 16 | 15 |
| 4 | [2, 8, 4, 1, 6] | 21 | 8 + 5×1 = 13 | 13 |
发现规律了吗?
答案似乎总是等于 !
也就是说,我们的 DP 在经过复杂的状态转移后,最终只是在计算一个简单的公式?
2.2 直觉猜想
让我们大胆猜测:最优策略只有两种:
- 完全不操作:代价 =
(原始和)
- 对整个数组操作一次:代价 =
但这个猜想对吗?部分操作难道永远不会更优吗?
三、操作的真正意义
让我们停下来,重新审视这个问题的本质。
3.1 操作能做什么?
每次操作,我们:
- 支付:区间最大值的代价
- 收益:将区间内所有元素降低到区间最小值
这本质上是一种"用高价换低价"的交易。
3.2 极限在哪里?
关键观察:无论如何操作,所有元素的最小可能值是多少?
答案:全局最小值
因为:
- 操作只能让元素变成所在区间的最小值
- 而任何区间的最小值
全局最小值
所以 是所有元素能达到的"终极下限"。
3.3 两种极端策略
现在我们有两个极端的策略选择:
策略A(保守):完全不操作
- 代价 =
- 简单直接,没有额外开销
策略B(激进):把所有元素都变成
- 怎么实现?最直接的方法是对整个数组
操作一次
- 操作花费 =
- 操作后数组 =
- 拿走数组的代价 =
- 总代价 =
但还是存在一个疑问:部分操作会不会介于两者之间,或者更优呢?
四、数学证明:为什么只需要考虑两个极端?
现在我们要严格证明:任何操作策略的代价都
4.1 证明框架
设我们采用某种操作方案:
- 操作总花费为
- 最终数组的元素和为
- 总代价 =
我们需要证明:
4.2 核心观察:最大值的命运
关键分类:按照数组中的最大值 是否被操作修改来讨论。
情况1:最大值被修改了
如果 被某次操作修改(变小)了,说明:
- 存在某个操作区间
包含了最大值的位置
- 这个区间的最大值就是
- 这次操作的花费至少是
因此:
操作后,所有元素都 (全局最小值是下界),所以:
结论:
情况2:最大值没被修改
如果 从未被任何操作覆盖,那么:
- 最终数组中至少有一个位置还是
(最大值位置)
- 其他位置的元素都
所以:
现在的问题是: 最小是多少?
关键观察:如果我们真的进行了操作(否则就是策略A,代价为 ),那么:
- 任何操作的花费 = 该操作区间的最大值
- 区间最大值
区间最小值
全局最小值 =
因此,任何一次有效操作的花费都 :
结论:
4.3 总结
无论哪种情况:
- 如果修改了最大值:
- 如果没修改最大值:
所以,任何操作策略的代价都至少是 。
而策略B(全局操作)恰好能达到这个下界!
因此,最优答案就是:
五、最终代码
#include <bits/stdc++.h>
using namespace std;
void solve() {
int n;
cin >> n;
vector<long long> a(n);
long long sum = 0;
long long minVal = LLONG_MAX;
long long maxVal = LLONG_MIN;
for (int i = 0; i < n; i++) {
cin >> a[i];
sum += a[i];
minVal = min(minVal, a[i]);
maxVal = max(maxVal, a[i]);
}
// 策略A:不操作 vs 策略B:全局操作
long long ans = min(sum, maxVal + (long long)n * minVal);
cout << ans << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int T;
cin >> T;
while (T--) {
solve();
}
return 0;
}
复杂度分析:
- 时间复杂度:
- 空间复杂度:
(仅用于存储输入)
从 到
,从复杂的区间DP到一行优雅的公式,这就是数学的力量!
六、举一反三:相似的思维陷阱
这类问题有一个共同特点:表面上需要复杂的动态规划,实则隐藏着简洁的数学本质。
6.1 问题1:石子合并的变种
题意:有 堆石子
,每次可以选择一段连续的石子合并成一堆,代价是这段石子中的最大值。求把所有石子合并成一堆的最小代价。
初见思路:区间DP!设 表示合并
的最小代价...
数学本质:
- 最大的那堆石子无论如何都要参与
次合并
- 答案就是
证明要点:最大值要么被合并(花费它的值),要么和别的合并(也要花费包含它的区间的最大值,还是它自己)。
6.2 问题2:数组分割问题
题意:将数组分成若干段,每段的代价是 ,求最小总代价。
初见思路:DP枚举分割点...
数学本质:
- 如果分成多段:每段的
之和
全局的
- 最优方案是分成一段(如果元素不全相同)
- 答案是
或
(全相同)
6.3 问题3:Codeforces - The Delivery Dilemma
题意: 个物品,可以自己取(时间
)或外卖送(时间
)。自己取的时间累加,外卖的时间取最大值。求最小化总时间。
题目链接:CF1443C
初见思路:状态压缩DP或背包...
数学本质:
- 按
排序
- 枚举"外卖的最大
",贪心选择
- 答案形式:
关键思想:排序 + 贪心,
6.4 问题4:区间覆盖的最小代价
题意:数轴上有 个区间,每个区间有权值
。选择若干区间覆盖
,最小化最大权值。
初见思路:DP + 贪心...
数学本质:
- 二分答案
- 贪心验证:每次选择权值
答案且右端点最大的区间
七、方法论总结
通过这道题,我们可以提炼出一套思维框架:
何时怀疑DP不是最优解?
- DP的转移只依赖全局最值(max/min/sum)
- 问题有明显的"极端策略"(全操作 vs 不操作)
- 中间状态似乎没有优势(要么左要么右)
- 样例的答案有简单的规律
如何寻找数学解?
- 观察极端情况:全操作、不操作、只操作最大/最小元素...
- 分类讨论关键元素:最大值、最小值的命运
- 证明下界:任何策略都
某个值
- 构造达到下界的方案:证明极端策略最优
通用证明模板
1. 定义总代价 = 操作成本 + 最终状态成本
2. 按关键元素(最大值/最小值)是否被改变分类
3. 每种情况下推导代价的下界
4. 证明某个简单策略达到下界
5. 结论:该策略最优
八、最后的思考
"简单的往往是最深刻的。" —— 数学之美
这道题给我们的启示:
- 不要急于实现第一个想到的算法,先观察问题的结构
- 极端情况往往揭示本质,中庸之道未必最优
- 数学证明让直觉变成确信,分类讨论是有力工具
- 当DP退化为比较两个值时,很可能有闭式解
下次遇到看似复杂的优化问题时,不妨先问自己:
- 极端策略是什么?
- 能否证明其他策略都不会更优?
- 问题的本质约束是什么?
希望这道题能成为你算法思维进阶路上的一个里程碑!
附录:完整测试代码
#include <bits/stdc++. h>
using namespace std;
void solve() {
int n;
cin >> n;
vector<long long> a(n);
long long sum = 0;
long long minVal = LLONG_MAX;
long long maxVal = LLONG_MIN;
for (int i = 0; i < n; i++) {
cin >> a[i];
sum += a[i];
minVal = min(minVal, a[i]);
maxVal = max(maxVal, a[i]);
}
long long ans = min(sum, maxVal + (long long)n * minVal);
cout << ans << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int T;
cin >> T;
while (T--) {
solve();
}
return 0;
}
测试样例:
输入:
3
4
5 1 3 2
4
1 1 1 1
3
10 2 3
输出:
9
4
15
标签:动态规划 数学 贪心 思维 区间操作

京公网安备 11010502036488号