P1090 [NOIP 2004 提高组] 合并果子
题目背景
P6033 为本题加强版。
题目描述
在一个果园里,多多已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。多多决定把所有的果子合成一堆。
每一次合并,多多可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。可以看出,所有的果子经过 n − 1 n-1 n−1 次合并之后, 就只剩下一堆了。多多在合并果子时总共消耗的体力等于每次合并所耗体力之和。
因为还要花大力气把这些果子搬回家,所以多多在合并果子时要尽可能地节省体力。假定每个果子重量都为 1 1 1 ,并且已知果子的种类 数和每种果子的数目,你的任务是设计出合并的次序方案,使多多耗费的体力最少,并输出这个最小的体力耗费值。
例如有 3 3 3 种果子,数目依次为 1 1 1 , 2 2 2 , 9 9 9 。可以先将 1 1 1 、 2 2 2 堆合并,新堆数目为 3 3 3 ,耗费体力为 3 3 3 。接着,将新堆与原先的第三堆合并,又得到新的堆,数目为 12 12 12 ,耗费体力为 12 12 12 。所以多多总共耗费体力 = 3 + 12 = 15 =3+12=15 =3+12=15 。可以证明 15 15 15 为最小的体力耗费值。
输入格式
共两行。
第一行是一个整数 n ( 1 ≤ n ≤ 10000 ) n(1\leq n\leq 10000) n(1≤n≤10000) ,表示果子的种类数。
第二行包含 n n n 个整数,用空格分隔,第 i i i 个整数 a i ( 1 ≤ a i ≤ 20000 ) a_i(1\leq a_i\leq 20000) ai(1≤ai≤20000) 是第 i i i 种果子的数目。
输出格式
一个整数,也就是最小的体力耗费值。输入数据保证这个值小于 2 31 2^{31} 231 。
输入输出样例 #1
输入 #1
3
1 2 9
输出 #1
15
说明/提示
对于 30 % 30\% 30% 的数据,保证有 n ≤ 1000 n \le 1000 n≤1000;
对于 50 % 50\% 50% 的数据,保证有 n ≤ 5000 n \le 5000 n≤5000;
对于全部的数据,保证有 n ≤ 10000 n \le 10000 n≤10000。
deepseek总结
贪心算法
核心思想
贪心算法 在每一步选择中都采取在当前状态下最好 或最优(即最有利)的选择,从而希望导致结果是全局最好或最优的算法。
简单来说,就是 "每一步都只看眼前利益,做出当前最好的选择,不考虑长远"。
贪心算法的特征
一个问题能否用贪心算法解决,通常取决于它是否具备以下两个性质:
-
贪心选择性质
- 一个问题的全局最优解可以通过一系列局部最优的选择(贪心选择)来达到。
- 关键:我们可以通过做出当前最好的选择来构造一个全局最优解,而不必考虑子问题的解。
-
最优子结构
- 一个问题的最优解包含其子问题的最优解。
- 这和动态规划是一样的,但贪心在做出选择后,不会回退。
与动态规划(DP)的对比
这是理解贪心的关键。我们通过一个表格来对比:
| 方面 | 贪心算法 (Greedy) | 动态规划 (DP) |
|---|---|---|
| 决策依据 | 每一步只根据当前状态做出最优选择 | 每一步的选择依赖于子问题的解 |
| 历史记录 | 不记录历史,选择后不可回退 | 记录所有可能的历史状态(通常用数组) |
| 回退 | 无回退,一条路走到黑 | 会考虑各种可能性,本质是枚举+剪枝 |
| 效率 | 通常高效,时间复杂度低 | 通常相对较低,因为要处理更多状态 |
| 适用问题 | 问题具有贪心选择性质 | 问题具有重叠子问题 和最优子结构 |
| 思路 | 自顶向下 | 通常自底向上 |
通俗比喻:
- 贪心 :像是一个只考虑眼前利益的登山者,每次都选择最陡的路向上爬,希望尽快登顶,但可能最终只到达了一个小山头(局部最优)。
- 动态规划 :像是一个谨慎的规划师,他会预先画好地图,考虑所有可能的路线,并记录下每条路线的信息,最终选择一条绝对最佳的路线(全局最优)。
常见应用场景
- 哈夫曼编码:用于数据压缩。
- 最小生成树:Prim算法和Kruskal算法。
- 单源最短路径:Dijkstra算法(权值为非负时)。
- 区间调度问题:如活动安排。
- 部分背包问题:物品可以分割时。
总结
- 核心:每一步都选择当前最优解。
- 关键 :问题必须具有贪心选择性质 和最优子结构。
- 难点 :如何选择和证明贪心策略的正确性。
- 座右铭:"活在当下",但前提是"当下"的选择能通向往"美好的未来"。
小根堆
一、核心概念总览
| 特性 | 小根堆 (Min Heap) | 大根堆 (Max Heap) |
|---|---|---|
| 核心性质 | 父节点的值 ≤ 所有子节点的值 | 父节点的值 ≥ 所有子节点的值 |
| 堆顶元素 | 全局最小值 | 全局最大值 |
| 主要操作 | 插入、获取最小值、删除最小值 | 插入、获取最大值、删除最大值 |
| 时间复杂度 | 插入/删除: O(log n), 获取最值: O(1) | 插入/删除: O(log n), 获取最值: O(1) |
| 结构 | 完全二叉树 (通常用数组实现) | 完全二叉树 (通常用数组实现) |
| 直观理解 | 一个能随时拿到最小元素的"盒子" | 一个能随时拿到最大元素的"盒子" |
二、C++ STL 中的 priority_queue
priority_queue 是 STL 对堆数据结构的封装实现,让你无需手动维护堆。
1. 默认行为:大根堆
cpp
#include <queue>
priority_queue<int> pq; // 默认就是大根堆
pq.push(3);
pq.push(1);
pq.push(4);
cout << pq.top(); // 输出 4 (当前最大值)
2. 如何定义小根堆
需要通过模板参数指定比较方式:
cpp
#include <queue>
#include <vector>
#include <functional> // 需要包含这个头文件
// 小根堆的定义方式
priority_queue<int, vector<int>, greater<int>> min_pq;
min_pq.push(3);
min_pq.push(1);
min_pq.push(4);
cout << min_pq.top(); // 输出 1 (当前最小值)
3. 模板参数解释
cpp
priority_queue<T, Container, Compare>
- T : 元素类型 (如
int,string) - Container : 底层容器,默认为
vector<T> - Compare : 比较规则,默认为
less<T>(大根堆),小根堆用greater<T>
4. 常用操作
| 操作 | 函数 | 时间复杂度 | 说明 |
|---|---|---|---|
| 插入元素 | push(val) |
O(log n) | 插入并调整堆 |
| 访问堆顶 | top() |
O(1) | 返回优先级最高的元素 |
| 删除堆顶 | pop() |
O(log n) | 删除堆顶并调整堆 |
| 判断空 | empty() |
O(1) | 队列是否为空 |
| 元素数量 | size() |
O(1) | 返回元素个数 |
三、关键要点总结
1. 核心区别
- 大根堆:大的优先级高,堆顶是最大值
- 小根堆:小的优先级高,堆顶是最小值
2. STL 使用技巧
cpp
// 大根堆 (默认)
priority_queue<int> max_heap;
// 小根堆 (必须记住的语法)
priority_queue<int, vector<int>, greater<int>> min_heap;
// 自定义比较器
struct Compare {
bool operator()(const Person& a, const Person& b) {
return a.age < b.age; // 大根堆:年龄大的优先级高
}
};
priority_queue<Person, vector<Person>, Compare> custom_pq;
3. 应用场景
- 大根堆:找最大值、Top K 大问题、任务调度(优先级高的先执行)
- 小根堆:找最小值、Top K 小问题、Dijkstra算法、哈夫曼编码
4. 竞赛建议
- 大部分情况 :直接用
priority_queue,节省编码时间 - 特殊需求:理解底层原理,必要时手动实现
- 性能关键:了解复杂度,避免错误使用
四、一句话总结
- 小根堆:最小的在最上面
- 大根堆:最大的在最上面
priority_queue:C++ STL 帮你实现好的堆,默认是大根堆,通过greater<T>可以变成小根堆
leetcode:2611
有两只老鼠和 n 块不同类型的奶酪,每块奶酪都只能被其中一只老鼠吃掉。
下标为 i 处的奶酪被吃掉的得分为:
如果第一只老鼠吃掉,则得分为 reward1[i] 。
如果第二只老鼠吃掉,则得分为 reward2[i] 。
给你一个正整数数组 reward1 ,一个正整数数组 reward2 ,和一个非负整数 k 。
请你返回第一只老鼠恰好吃掉 k 块奶酪的情况下,最大 得分为多少。
示例 1:
输入:reward1 = [1,1,3,4], reward2 = [4,4,1,1], k = 2
输出:15
解释:这个例子中,第一只老鼠吃掉第 2 和 3 块奶酪(下标从 0 开始),第二只老鼠吃掉第 0 和 1 块奶酪。
总得分为 4 + 4 + 3 + 4 = 15 。
15 是最高得分。
示例 2:
输入:reward1 = [1,1], reward2 = [1,1], k = 2
输出:2
解释:这个例子中,第一只老鼠吃掉第 0 和 1 块奶酪(下标从 0 开始),第二只老鼠不吃任何奶酪。
总得分为 1 + 1 = 2 。
2 是最高得分。
注意贪心的对象,贪心的是差值
cpp
class Solution {
public:
int miceAndCheese(vector<int>& reward1, vector<int>& reward2, int k) {
int n = reward1.size();
priority_queue<int> que;
long long ans = 0;
for(int i = 0; i < n; i++){
que.push(reward1[i]-reward2[i]);
ans += reward2[i];
}
for(int i = 0; i < k; i++){
ans += que.top();
que.pop();
}
return ans;
}
};