1. 题目背景与理解
洛谷P1094 [NOIP 2007 普及组] 纪念品分组是一道经典的贪心算法练习题。题目大意是:给定一组纪念品的价格和一个上限值,要求将这些纪念品分组,每组的价格总和不超过上限,且组数尽可能少。
这道题考察的是对贪心算法的理解和应用能力。贪心算法在解决这类"最优化"问题时非常有效,它通过每一步都采取当前看起来最优的选择,希望最终达到全局最优解。
2. 问题分析与建模
2.1 输入输出分析
题目通常会给出:
- 纪念品的价格列表(一个数组)
- 每组价格的上限值W
要求输出最少的分组数量。
2.2 关键约束条件
- 每组最多两件纪念品
- 每组价格总和不超过W
- 组数尽可能少
2.3 贪心策略选择
对于这类问题,常见的贪心策略是:
- 先将纪念品按价格排序
- 使用双指针法,一个指向最便宜的,一个指向最贵的
- 尝试将最便宜和最贵的配对
- 如果配对成功,两个指针都移动;否则只移动高价指针
3. 算法实现详解
3.1 排序预处理
首先需要对纪念品价格进行排序。排序的目的是为了方便后续的双指针操作。在C++中可以使用sort函数:
cpp复制sort(prices.begin(), prices.end());
排序的时间复杂度是O(nlogn),这通常是整个算法的时间瓶颈。
3.2 双指针策略实现
设置两个指针i和j,分别指向数组的首尾:
cpp复制int i = 0, j = prices.size() - 1;
int groups = 0;
while(i <= j) {
if(prices[i] + prices[j] <= W) {
i++;
}
j--;
groups++;
}
这个循环的核心逻辑是:
- 如果能将最便宜和最贵的放在一组(和≤W),就一起放入(i++, j--)
- 否则,只能将最贵的单独放一组(j--)
- 无论哪种情况,组数都增加1
3.3 边界条件处理
需要考虑的特殊情况包括:
- 所有纪念品价格都相同
- 所有纪念品价格都大于W/2
- 空输入的情况
- 单个纪念品的情况
4. 正确性证明
4.1 贪心选择性质
我们需要证明每一步的局部最优选择能导致全局最优解。关键在于:
- 对于当前最贵的物品,要么它能和最便宜的配对,要么它必须单独成组
- 如果它能和最便宜的配对,这样配对不会比不配对更差(因为最便宜的物品之后可能无法与其他物品配对)
4.2 最优子结构
问题的最优解包含子问题的最优解。在分组后,剩下的物品仍然需要最优分组,这与原问题的性质相同。
5. 复杂度分析
5.1 时间复杂度
主要时间消耗在排序上,为O(nlogn)。双指针过程是O(n),所以总时间复杂度是O(nlogn)。
5.2 空间复杂度
如果可以在原数组上排序,空间复杂度是O(1)。否则需要O(n)的额外空间。
6. 代码实现示例
完整C++实现:
cpp复制#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int minGroups(vector<int>& prices, int W) {
sort(prices.begin(), prices.end());
int i = 0, j = prices.size() - 1;
int groups = 0;
while(i <= j) {
if(prices[i] + prices[j] <= W) {
i++;
}
j--;
groups++;
}
return groups;
}
int main() {
int W, n;
cin >> W >> n;
vector<int> prices(n);
for(int i = 0; i < n; i++) {
cin >> prices[i];
}
cout << minGroups(prices, W) << endl;
return 0;
}
7. 测试用例设计
好的测试用例应该覆盖各种边界情况:
-
常规测试用例:
- 输入:W=100, prices=[90, 20, 30, 50, 60, 70]
- 输出:3(分组为[20,90], [30,70], [50,60])
-
所有物品价格相同:
- 输入:W=100, prices=[50, 50, 50, 50]
- 输出:2(分组为[50,50], [50,50])
-
所有物品都大于W/2:
- 输入:W=100, prices=[60, 70, 80, 90]
- 输出:4(每个物品单独一组)
-
单个物品:
- 输入:W=100, prices=[50]
- 输出:1
-
空输入:
- 输入:W=100, prices=[]
- 输出:0
8. 常见错误与调试技巧
8.1 常见错误
- 忘记排序:会导致配对策略失效
- 循环条件错误:应该是i <= j而不是i < j
- 组数计数位置错误:应该在每次循环都增加,而不是只在配对成功时增加
8.2 调试技巧
- 打印中间变量:在循环中打印i,j和当前组数
- 小规模测试:先用小数据测试,确保基本逻辑正确
- 边界测试:专门测试空输入、单个输入等情况
9. 算法优化与变种
9.1 优化空间
如果纪念品数量很大但价格范围有限,可以考虑计数排序,将时间复杂度降到O(n + k),其中k是价格范围。
9.2 问题变种
- 每组允许最多k件纪念品(k>2)
- 不同纪念品有不同权重,要求每组权重和不超过上限
- 在分组的同时,要求各组之间的价格差异最小化
10. 实际应用场景
这类分组问题在实际中有很多应用:
- 资源分配:将任务分配给机器,每台机器有最大负载
- 打包问题:将物品装入箱子,每个箱子有重量限制
- 时间安排:将活动安排到时间段,每个时间段有持续时间限制
理解这个问题的解法有助于解决更复杂的资源分配和优化问题。贪心算法虽然不一定能得到全局最优解,但在很多实际场景中提供了简单高效的近似解决方案。
