题目解析
直觉:
- 存在一种排序,使得能得到最值
- h0排在开头
- 直觉选接下来选最高的
,因为这样 是最大的 - 问题变成,
排在开头,这显然是一种递归,选最低的元素 - 每次选择为候选中固定选一种,是 贪心
- 直觉选接下来选最高的
这也是一道非常经典的贪心算法题目,和上一道“看电视”的题目一样,核心都在于找到最优的策略。
1. 核心分析:为什么要“反复横跳”?
题目要求最大化耗费的体力值,体力值的计算公式是
这是一个平方函数。我们知道,
也就是说,为了让
贪心策略如下:
- 第一步:你站在地面(高度 0)。为了让第一跳的代价最大,你应该跳到最高的那块石头上。
- 第二步:你现在站在最高的石头上。为了让下一跳代价最大,你应该跳到最低的那块石头上(因为最高减最低,差值最大)。
- 第三步:你现在站在最低的石头上。剩下的石头里,离你最远的是第二高的石头。
- 第四步:跳到第二低的石头。
- …以此类推。
形象的理解:
这就好比在一个排序后的数组上进行“钟摆运动”或“反复横跳”:
地面(0) -> 最大 -> 最小 -> 次大 -> 次小 -> …
2. 算法流程
- 排序:将所有石头的高度从小到大排序。
- 双指针:
- 左指针
L指向数组最左端(最小值)。 - 右指针
R指向数组最右端(最大值)。 - 记录当前所在位置的高度
current,初始为 0。
- 左指针
- 交替跳跃:
- 只要还有石头没跳过(
L <= R):- 先跳到最大(R指向的石头):计算
(h[R] - current)^2加到总和里,更新current = h[R],然后R--。 - 如果石头跳完了,结束循环。
- 再跳到最小(L指向的石头):计算
(h[L] - current)^2加到总和里,更新current = h[L],然后L++。
- 先跳到最大(R指向的石头):计算
- 只要还有石头没跳过(
3. 复杂度分析
- 时间复杂度:主要是排序,为
。 ,这在计算机里是一瞬间的事。 - 空间复杂度:
存储石头高度。
4. 坑点提示 (重要)
虽然
。 - 单次跳跃最大代价约为
。 - 总共有 300 次跳跃,极限总和约为
。
注意:C++ 中 int 的最大值约为
解决:统计答案的变量必须使用 long long。
5. 参考代码 (C++)
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
int main() {
int n;
if (!(cin >> n)) return 0;
vector<int> h(n);
for (int i = 0; i < n; i++) {
cin >> h[i];
}
// 1. 排序
sort(h.begin(), h.end());
// 2. 双指针初始化
int l = 0; // 左指针(最小)
int r = n - 1; // 右指针(最大)
int current = 0; // 当前高度(初始为地面 0)
long long ans = 0; // 必须用 long long,否则会溢出
// 3. 开始反复横跳
while (l <= r) {
// 第一跳:跳向当前最大的 (r)
ans += (long long)(h[r] - current) * (h[r] - current);
current = h[r];
r--; // 右边那个石头踩过了,指针左移
// 检查是否跳完
if (l > r) break;
// 第二跳:跳向当前最小的 (l)
ans += (long long)(h[l] - current) * (h[l] - current);
current = h[l];
l++; // 左边那个石头踩过了,指针右移
}
cout << ans << endl;
return 0;
}
证明
显然: 不证明贪心的正确性,总感觉怪怪的,“不仅知其然,还要知其所以然” 是非常重要的品质。
证明贪心算法的正确性通常有两种方法:
- “交换论证法”(Exchange Argument)
- “数学不等式法”。
对于这道题,我们可以用代数变形配合**排序不等式(Rearrangement Inequality)**的逻辑来证明。
排序不等式
这是一个非常棒的数学问题!排序不等式 (Rearrangement Inequality) 是高中数学竞赛和 ACM 算法竞赛中处理“最值问题”的一把利器。
既然你已经能熟练使用 C++ 和算法,我们可以用代数推导和直观逻辑两种方式来彻底搞定它。
1. 什么是排序不等式?
假设有两个长度为
我们将这两个序列中的数字两两配对相乘,然后求和。会有三种典型的情况:
-
同序和 (Sequential Sum):大的配大的,小的配小的。
-
乱序和 (Random Sum):随意打乱顺序配对。
(其中
是 的一个随机排列) -
反序和 (Reversed Sum):最大的配最小的,最小的配最大的。
排序不等式的结论是:
简单记忆:
- 想求最大值:这就叫“强强联合”。
- 想求最小值:这就叫“损耗最大化”(或者上一题里的“为了让差值项最小”)。
2. 直观证明(生活中的例子)
想象你有两堆东西:
- 钱堆:有一张 10元,有一张 100元。 (即
) - 倍数卡:有一张 x1卡,有一张 x10卡。 (即
)
你也只有两种搭配方法:
方案 A(同序 - 强强联合):
- 100元
10倍 = 1000 - 10元
1倍 = 10 - 总和 = 1010
方案 B(反序 - 强弱搭配):
- 100元
1倍 = 100 - 10元
10倍 = 100 - 总和 = 200
道理很明显:既然你有“放大器”(倍数卡),你肯定希望那个最大的放大器去放大那个本来就最大的数。把超级放大器用在小树苗上是浪费,用在参天大树上才能获得最大收益。
3. 严格数学证明(微扰分析法 / Exchange Argument)
在算法证明中,我们常用**“微扰分析”**(假如我不按规矩来,结果会不会变差?)来证明贪心策略。
我们只需要证明
设定:
已知
对比:
- 同序和:
- 反序和:
我们要证明
作差法证明:
重新组合各项(合并同类项):
提取公因式
分析符号:
- 因为我们设定了
,所以 是正数。 - 因为我们设定了
,所以 是正数。 - 正数乘以正数,结果恒为正数。
结论:
即:同序和 > 反序和。
对于任意乱序的情况,只要我们发现有一对下标
根据上面两对元素的性质,可以想到
- 设最大乘积和X :
- 按
从小到大排列,那么可以使用反证法,把最大的 移动到与 配对 - 递归, 去除
,变成类一个类似的问题,使用同样的方法
- 按
4. 回到“小跳蛙”题目
在上一题《跳跳!》中,我们的目标是让
展开后其中有一项是减去交叉项:
- 我们要让整个式子最大,就要让被减去的这一项最小。
- 根据排序不等式,要让乘积和最小,必须是反序和。
- 也就是:序列中最大的数,必须和序列中最小的数相乘(相邻)。
这就是为什么我们要把石头排序后,用“最大配最小”的方式来安排跳跃顺序。
5. 总结
- 同序和最大(
):想要最大收益,好马配好鞍。 - 反序和最小(
):想要最小代价(或最小化某个中间项),好马配劣鞍。
这个数学原理是很多贪心算法(比如“排队打水问题”、“最小生成树的某些性质”)的基石。
经典的 “排队接水问题”,就是使用的排序不等式
严格证明1: 不等式法
1. 直观理解:为什么要“一大一小”?
首先,回顾一下体力公式:
这是一个凸函数(Convex Function)。它的特性是:距离越大,收益呈爆炸式增长。
- 距离为 2,收益
- 距离为 4,收益
(距离翻倍,收益变为 4 倍) - 距离为 10,收益
直觉:为了总和最大,我们要尽量避免“小碎步”,而要尽可能多地制造“大跨步”。如果你在低处,就要往最高跳;如果你在高处,就要往最低跳。
2. 严格证明(代数推导)
假设我们的跳跃序列是
我们要最大化总消耗
我们将平方项展开:
把所有项加起来:
整理一下,注意到中间的项
因为
第一点:让平方和部分最大化
这一项里,中间的石头贡献了
为了让总和最大,我们希望数值大的石头都在中间被反复踩(贡献2倍),而数值小的石头或者中等石头做结尾(贡献1倍)。
- 贪心策略的做法:先跳最大的,再跳最小的,再跳次大的……这样最大的那些石头肯定会被当作“中间跳板”,贡献
。最后停下来的往往是中间大小的石头。符合最大化要求。
第二点:让交叉项最小化 (核心)
这是决定性的一步。我们要最大化
这是一个经典的**“排序不等式”**变体。
原理:要让两个数的乘积之和最小,应该用**“最小的数”乘以“最大的数”**。
(例如:有数 1, 10, 20。组合
比 小)。
在我们的序列中,
- 贪心策略的做法:
- 序列:
- 相邻对:
, , - 你看,每一对都是当前可用的极小值配对极大值。这正是让乘积项
最小化的最优排列方式。
- 序列:
3. 举个简单的例子验证
假设石头高度是
方案 A(贪心:反复横跳):
- 消耗:
- 交叉项乘积:
(较小)
方案 B(顺着跳):
-
消耗:
-
交叉项乘积:
-
等等,为什么交叉项一样?
因为在这个短例子里,中间项
和 的位置决定了平方和贡献。 - 方案 A 中间项是 20(最大值),贡献
。 - 方案 B 中间项是 10(最小值),贡献
。 - 显然方案 A 赢在“把大数放在中间”。
- 方案 A 中间项是 20(最大值),贡献
方案 C(乱跳,如果
假设
- 贪心
:代价 。 - 顺跳
:代价 。 - 这里贪心赢在交叉项最小化:
远小于 。
总结
贪心策略之所以正确,是因为它同时做到了两件事:
- 让大数贡献更多:最大的那些石头被安排在路径中间,被计算了两次平方和。
- 让相邻差最大:通过“极大配极小”,最小化了
这个减分项。
这就是为什么你要像青蛙一样“反复横跳”的原因!
严格证明2: 交换验证
参考: https://www.luogu.com.cn/article/y9wvbuth
这个证明确实更直观,而且它用到了贪心算法证明皇冠上的明珠——交换论证法(Exchange Argument),有时候也叫微扰法。
你截图中的这个证明方法,其实比刚才讲的“排序不等式”更具普适性,因为它不需要把整个公式展开,而是直接从路径的角度去思考。
我们来拆解一下这个证明的精髓,看看它是如何通过“交换”把非最优解变成最优解的。
核心逻辑:反证法 + 交换论证
假设:存在一种“最优策略”,它的第一步不跳到最高的
目标:证明如果这样做,我们总能找到一种改法,让总消耗体力变得更大。这就说明刚才的假设不是最优的。
证明分了两种情况,非常有条理:
情况 1: 是终点 (简单情况)
- 原来的走法:
(结束) - 改进的走法:
(相当于把整个路程反过来走) - 比较:
- 原来的第一步消耗:
- 现在的第一步消耗:
- 因为
,显然 。
- 原来的第一步消耗:
- 结论:改完后体力消耗更大,所以第一步必须跳
。
情况 2: 不是终点 (精彩部分)
这是图中最核心的逻辑。
- 原来的走法:
- 意思是你第一步跳到了
,中间经过一堆乱七八糟的跳跃,跳到了最高的 ,然后从 跳去了 。
- 意思是你第一步跳到了
- 改进的走法:
- 怎么改的? 这是一个经典的**“路径翻转”**操作。
- 把
直接连到 。 - 把
到 之间的路径全部反向(翻转)。注意,石头之间的距离是绝对值, ,所以路径中间反过来走,体力消耗是不变的。 - 最后从
连到 。
算账(核心不等式):
我们要比较只发生变化的这两根“连接线”:
- 新路径增量:
(第一跳) + (翻转后的最后一跳) - 旧路径增量:
(第一跳) + (中间那一跳)
做差(新 - 旧):
展开化简(就像图里写的那样):
消掉
分析结果:
(因为是正整数高度) (因为 是最高的, 是矮的) - 所以
。
结论:新的走法比所谓的“最优解”消耗还要大!这说明假设错误,第一步必须跳到
为什么说这个证明“更好”?
- 它不需要数学“巧合”:刚才的排序不等式证明依赖于公式展开后的交叉项恰好符合形式。而这个证明利用的是图论的思想(路径翻转)。
- 通用性强:这种“翻转一段路径”的操作,是解决 TSP(旅行商问题) 等复杂图论问题中常用的 2-opt 优化算法 的核心思想。你现在理解了这个,以后学高级算法会非常快。
- 构造性:它不仅告诉你答案是对的,还通过构造过程(怎么翻转路径)让你看到了为什么原来的不够好。
总结:
只要能证明“我稍微换一下顺序,结果会更好”,就能证明贪心策略的正确性。
这一招在很多题目里都能用,比如经典的**“国王游戏”**(NOIP题),也是用这种相邻交换法证明的