575. 分糖果
问题描述
Alice 有 n
颗糖果,其中第 i
颗糖果的类型为 candyType[i]
。Alice 注意到她的体重正在增加,所以她想在吃糖果的同时保持身材。
她只允许自己吃 n / 2
颗糖果,并且希望在吃掉的糖果中,不同种类的糖果数量尽可能多。
返回 Alice 可以吃到的不同种类糖果数量的最大值。
示例:
输入: candyType = [1,1,2,2,3,3]
输出: 3
解释: 有6颗糖果,Alice可以吃3颗。有3种不同糖果,她可以每种吃一颗。
输入: candyType = [1,1,2,3]
输出: 2
解释: 有4颗糖果,Alice可以吃2颗。有3种不同糖果,但她最多只能吃2颗,所以最多2种。
算法思路
核心
这是一个典型的限制条件下的最大化问题,有两个限制因素:
- 数量限制:最多只能吃
n/2
颗糖果 - 种类限制:最多只能吃到所有不同种类的数量
贪心策略
要使吃到的糖果种类最多,应该:
- 优先选择不同种类的糖果
- 每种糖果只吃一颗(因为目标是最大化种类数,不是数量)
数学分析
设:
total
= 糖果总数 =n
maxEat
= 最多能吃的糖果数 =n / 2
types
= 不同糖果种类的数量
则最大种类数 = min(maxEat, types)
为什么?
- 如果种类数 ≤ 能吃的数量:可以吃到所有种类
- 如果种类数 > 能吃的数量:最多只能吃到
maxEat
种
代码实现
import java.util.HashSet;
import java.util.Set;
class Solution {
/**
* 计算Alice能吃到的不同糖果种类的最大数量
*
* @param candyType 糖果类型数组,candyType[i]表示第i颗糖果的类型
* @return 能吃到的不同糖果种类的最大数量
*/
public int distributeCandies(int[] candyType) {
int n = candyType.length; // 糖果总数
int maxEat = n / 2; // 最多能吃的糖果数量
// 使用HashSet统计不同糖果种类的数量
Set<Integer> types = new HashSet<>();
for (int type : candyType) {
types.add(type);
}
int totalTypes = types.size(); // 不同糖果种类的总数
// 返回两个限制中的较小值:
// 1. 最多能吃的糖果数量
// 2. 不同糖果种类的总数
return Math.min(maxEat, totalTypes);
}
}
优化(使用Java 8 Stream)
import java.util.Arrays;
class Solution {
/**
* 优化:使用Java 8 Stream API
*
* @param candyType 糖果类型数组
* @return 能吃到的不同糖果种类的最大数量
*/
public int distributeCandies(int[] candyType) {
int maxEat = candyType.length / 2;
// 使用Stream统计不同种类的数量
long uniqueTypes = Arrays.stream(candyType)
.distinct()
.count();
return (int) Math.min(maxEat, uniqueTypes);
}
}
算法分析
-
时间复杂度:O(n)
- 遍历数组一次统计种类数
- HashSet的add操作平均O(1)
-
空间复杂度:O(n)
- HashSet最多存储n个不同元素
- 最坏情况下所有糖果都不同
-
关键:
- 贪心策略避免了复杂的动态规划
- 数学分析得出简单公式
min(n/2, 不同种类数)
算法过程
candyType = [1,1,2,2,3,3]
:
1:计算基本参数
- 糖果总数
n = 6
- 最多能吃
maxEat = 6 / 2 = 3
颗
2:统计不同种类
- 种类集合:{1, 2, 3}
- 不同种类数
totalTypes = 3
3:计算结果
min(3, 3) = 3
策略执行:
- 吃1颗类型1的糖果
- 吃1颗类型2的糖果
- 吃1颗类型3的糖果
- 共吃3颗,3种不同糖果
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:标准示例
int[] candyType1 = {1,1,2,2,3,3};
System.out.println("Test 1: " + solution.distributeCandies(candyType1)); // 3
// 测试用例2:种类多于能吃的数量
int[] candyType2 = {1,1,2,3};
System.out.println("Test 2: " + solution.distributeCandies(candyType2)); // 2
// n=4, maxEat=2, types=3 → min(2,3)=2
// 测试用例3:种类少于能吃的数量
int[] candyType3 = {1,1,1,1};
System.out.println("Test 3: " + solution.distributeCandies(candyType3)); // 1
// n=4, maxEat=2, types=1 → min(2,1)=1
// 测试用例4:所有糖果都不同
int[] candyType4 = {1,2,3,4,5,6,7,8};
System.out.println("Test 4: " + solution.distributeCandies(candyType4)); // 4
// n=8, maxEat=4, types=8 → min(4,8)=4
// 测试用例5:单一种类
int[] candyType5 = {1};
System.out.println("Test 5: " + solution.distributeCandies(candyType5)); // 1
// 实际上,根据题目约束,n总是偶数
// 测试用例6:两个元素
int[] candyType6 = {1,2};
System.out.println("Test 6: " + solution.distributeCandies(candyType6)); // 1
// n=2, maxEat=1, types=2 → min(1,2)=1
}
关键点
-
贪心选择的正确性:
- 目标是最大化种类数,不是数量
- 所以每种糖果只吃一颗是最优策略
- 吃多颗同种糖果不会增加种类数
-
两个限制因素:
- 数量限制:最多吃
n/2
颗 - 种类限制:最多有
uniqueTypes
种 - 最终结果是两者的最小值
- 数量限制:最多吃
-
数据结构选择:
- HashSet适合统计唯一元素
- 也可以用数组(如果类型范围小)或排序去重
-
边界情况:
- 所有糖果同一种类
- 所有糖果都不同种类
- 最小输入(2颗糖果)
常见问题
-
为什么用HashSet而不是数组?
- 糖果类型值范围可能很大(题目约束:-10⁵ ≤ candyType[i] ≤ 10⁵)
- 使用数组需要200001个空间,可能浪费
- HashSet动态分配,更节省空间
-
如果n是奇数怎么办?
- 题目约束:n是偶数,且2 ≤ n ≤ 10⁴
- 所以
n/2
总是整数
-
能否用排序解决?
- 可以,先排序再遍历统计不同种类
- 时间复杂度O(n log n),比HashSet方法慢
-
算法的直观理解:
- 想象糖果按种类分堆
- Alice要从这些堆中选糖果吃
- 她的策略是:每堆只拿一颗,这样能吃到最多种类
- 如果堆数 ≤ 能吃的数量,她可以每堆都拿一颗
- 如果堆数 > 能吃的数量,她最多只能吃到能吃数量的种类
-
为什么这是
贪心算法
?- 每一步都做
局部最优
选择:选择未吃过的新种类 - 这种策略最终得到
全局最优
解 - 因为问题具有贪心选择性质:优先选择不同种类不会影响后续选择
- 每一步都做