尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

从一道GESP真题出发:聊聊贪心排序与前缀和优化

从一道GESP真题出发:聊聊贪心排序与前缀和优化 题源洛谷 P17010 [GESP202606 五级] 排排坐https://www.luogu.com.cn/problem/P17010背景你有没有想过同样一组数字换个顺序排一排结果能差出多少在算法竞赛里这类排排坐问题看似只是简单的排序实则藏着贪心思想的精髓——不是盲目排序而是要让每个数字的话语权最大化。GESP五级这道题就是一道非常经典的入门贪心题老师给小朋友分糖果规则是每个小朋友获得自己及左侧所有小朋友数字之和的糖果数。目标很简单让总糖果数最大。这类问题在竞赛中定位明确属于普及-难度考察的是选手能否从排序这个动作中提炼出贡献度分析的思维方式。很多初学者会直觉地想把大的放前面但很少能说出为什么。本文就带你从直觉走向证明彻底搞懂这类问题的底层逻辑。核心思想问题本质前缀和的总和题目要求最大化总糖果量。设座位顺序为 b1, b2, …, bn则第1个小朋友获得b1第2个小朋友获得b1 b2第3个小朋友获得b1 b2 b3…总糖果量sum(i1 to n) sum(j1 to i) bj这个双重求和可以换个角度看每个数字 bj 会被包含在从第 j 位到第 n 位的所有前缀和中共被累加 (n - j 1) 次。核心洞察位置越靠左数字被累加的次数越多。第1位的数字被加 n 次第 n 位的数字只被加1次。贪心策略交换论证假设当前排列不是降序的存在相邻位置 i 和 i1 满足 bi b(i1)。我们来算一笔账交换前这两个位置对总和的贡献为bi * (n-i1) b(i1) * (n-i)交换后贡献变为b(i1) * (n-i1) bi * (n-i)两者之差Delta (b(i1) - bi) * [(n-i1) - (n-i)] b(i1) - bi 0结论任何逆序对小数在大数左边都可以通过交换使总和变大。因此降序排列是唯一最优解。前缀和优化避免重复计算直接按贡献度公式 sum bi * (n-i1) 计算需要两次遍历。更优雅的做法是先排序再维护前缀和数组 sa[i] sa[i-1] bi总糖果量就是所有前缀和之和ans sum(i1 to n) sa[i]这样做的优势代码更简洁逻辑更直观避免手动计算每个位置的贡献系数为后续扩展如动态修改、区间查询留下接口算法模板算法到底在干什么想象一条传送带上面放着 n 个包裹每个包裹上标着重量。规则是每经过一个包裹就要把当前传送带上所有包裹的重量加一遍记为这一站的运费。你要做的就是调整包裹的顺序让总运费最多。贪心排序就是把最重的包裹放在最前面让它被累加最多次最轻的放最后面只累加一次。万能模板伪代码function maxCandy(n, a): sort(a, descending) // 降序排序 prefix[0] 0 ans 0 for i 1 to n: prefix[i] prefix[i-1] a[i] ans prefix[i] return ans核心代码C#include bits/stdc.h using namespace std; // 贪心排序 前缀和求最大总和 // 适用于每个元素的贡献与位置权重相关的最优排列问题 long long maxTotal(vectorint a) { sort(a.begin(), a.end(), greaterint()); // 降序排列 long long prefix 0, ans 0; for (int x : a) { prefix x; // 维护前缀和 ans prefix; // 累加每个位置的前缀和 } return ans; }例题实现#include bits/stdc.h using namespace std; #define int long long const int N 1005; // 常量最大小朋友数量 int n; // n: 小朋友个数 int ans; // ans: 最大糖果总数量 int a[N]; // a[i]: 第 i 个小朋友手上的数字 int sa[N]; // sa[i]: 前 i 个小朋友数字的前缀和 signed main() { cin n; // 读入小朋友个数 for (int i 1; i n; i) // 读入 n 个小朋友手上的数字 cin a[i]; sort(a 1, a n 1, greaterint()); // 按数字从大到小排序让大的数字尽量靠左 for (int i 1; i n; i) // 计算最大糖果总量 { sa[i] sa[i - 1] a[i]; // 计算前 i 个小朋友数字的前缀和 ans sa[i]; // 第 i 个小朋友分到的糖果数 前 i 个数字之和累加到总量 } cout ans endl; // 输出最大糖果总数量 return 0; }对比实现直接贡献度法除了前缀和累加也可以直接按贡献系数计算// 方法2直接计算每个位置的贡献 sort(a 1, a n 1, greaterint()); long long ans 0; for (int i 1; i n; i) { ans a[i] * (n - i 1); // 第i位被累加(n-i1)次 }对比表格方法代码量直观性扩展性前缀和累加稍多高模拟实际过程好支持动态修改直接贡献度更少中需要理解系数差系数固定推荐竞赛中两种方法均可前缀和法更符合模拟题意的思维习惯。变体清单变体类型题目特征贪心策略关键变化最小化版本求最小总糖果量升序排列贡献度分析方向相反带权位置每个位置有额外权重 wi按 ai/wi 或特定比值排序可能需要更复杂的排序规则部分排列只能选 k 个数字排列选最大的 k 个再降序排增加选择环节环形排列小朋友围成一圈需要断环为链枚举断点复杂度升至 O(n^2)负数元素数字可能为负不能简单降序需分类讨论正数放左负数放右什么时候不能用元素有负数时降序排列可能不是最优。例如 [-5, 100, 1]若按降序排为 [100, 1, -5]但如果把 -5 放最前面它只会被加一次负面影响最小需要重新分析。位置权重不均匀时如果每个位置的被累加次数不是简单的 (n-i1)而是任意给定的权重可能需要按比值排序或其他策略。约束条件复杂时如要求某些元素必须相邻、某些位置固定等贪心可能失效需要动态规划。底层逻辑为什么贪心是对的贪心算法的正确性通常需要交换论证或拟阵理论支撑。本题属于前者最优子结构假设最优排列中前 k 个元素已经确定那么后 n-k 个元素的排列也必须是这 n-k 个元素的最优排列。贪心选择性质每次选择当前最大的未使用元素放在最左侧不会导致后续无法达到最优。这两个性质共同保证了局部最优每次选最大能推出全局最优总和最大。与经典问题的对比问题相似点不同点活动安排问题都是排序后贪心按结束时间排序选相容活动霍夫曼编码都是让高频/大权重元素更省资源用堆维护合并而非排列调度问题都是最小化/最大化加权总和可能涉及多机调度更复杂本题的独特之处在于权重结构是固定的前缀和形式这使得排序规则特别简单纯降序不需要像调度问题那样计算比值或动态调整。隐含约束的分析题目中正整数这个条件至关重要若允许负数大负数放左边会严重拉低总和若允许零零的位置不影响结果最优解不唯一正整数保证了严格降序唯一最优且交换论证中的 Delta 0 严格成立决策表场景特征推荐方案核心操作所有数字为正求最大前缀和总和降序排序 前缀和累加sort(…, greater())所有数字为正求最小前缀和总和升序排序 前缀和累加sort(…, less())数字含负数求最大总和分类讨论正数降序放左负数升序放右分段排序位置有自定义权重 wi按 ai * wi 贡献排序自定义比较函数需要动态修改元素值树状数组/线段树维护前缀和数据结构优化n 10^5需 O(n log n)上述排序方法均可标准排序即可n 10^7需接近 O(n)计数排序/基数排序线性时间排序工程视角这类贡献度加权排序的思想在实际工程中非常常见任务调度系统CPU调度中短作业优先SJF就是让执行时间短的任务被等待的次数最少本质是让权重小的任务尽量靠后。反过来如果每个任务的收益不同就要按收益/时间排序——这正是贪心排序的延伸。数据库查询优化在多表连接中选择最优的连接顺序以最小化中间结果集大小。虽然实际会用动态规划如PostgreSQL的遗传算法动态规划但核心思想与让大数据量操作尽量靠后一致。广告投放策略假设有 n 个广告位每个位置的曝光量递减第1位最多第n位最少。要最大化总点击率应该把点击率最高的广告放在最前面——与本题模型完全一致。缓存替换策略LRU最近最少使用虽然不是排序问题但让高频访问数据更容易被命中的思想与让大贡献元素被累加更多次异曲同工。小结这道题教会我们的核心认知是当每个元素的贡献与它的位置相关时最优排列往往可以通过贡献度分析 交换论证确定而不需要枚举所有排列。用公式化语言总结最优排列 sort(a, descending) max sum(i1 to n) sum(j1 to i) aj sum(i1 to n) ai * (n - i 1)关键认知升级从直觉排序到证明排序不只是觉得大的放前面好而是能用交换论证严格证明从双重循环到前缀和优化用前缀和将 O(n^2) 的暴力计算优化到 O(n)从具体题目到通用模型识别出位置权重 * 元素值这类问题的通用贪心框架下次遇到排一排让总和最大/最小的问题先问自己每个位置对总和的贡献是什么答案往往就藏在排序规则里。
返回列表