文章目录
题目
标题和出处
标题:吃苹果的最大数目
难度
6 级
题目描述
要求
有一棵特殊的苹果树,连续 n \texttt{n} n 天,每天都可以长出若干个苹果。在第 i \texttt{i} i 天,树上会长出 applesi \texttt{applesi} applesi 个苹果,这些苹果将会在 daysi \texttt{daysi} daysi 天后(第 i + daysi \texttt{i} + \texttt{daysi} i+daysi 天时)腐烂而无法食用。也可能有那么几天,树上不会长出新的苹果,此时用 applesi = 0 \texttt{applesi} = \texttt{0} applesi=0 且 daysi = 0 \texttt{daysi} = \texttt{0} daysi=0 表示。
每天最多 吃一个苹果。可以在 n \texttt{n} n 天之后继续吃苹果。
给定两个长度为 n \texttt{n} n 的整数数组 days \texttt{days} days 和 apples \texttt{apples} apples,返回可以吃掉的苹果的最大数目。
示例
示例 1:
输入: apples = 1,2,3,5,2, days = 3,2,1,4,2 \texttt{apples = 1,2,3,5,2, days = 3,2,1,4,2} apples = 1,2,3,5,2, days = 3,2,1,4,2
输出: 7 \texttt{7} 7
解释:可以吃掉 7 \texttt{7} 7 个苹果:
- 第一天,吃掉第一天长出来的苹果。
- 第二天,吃掉一个第二天长出来的苹果。
- 第三天,吃掉一个第二天长出来的苹果。过了这一天,第三天长出来的苹果就已经腐烂了。
- 第四天到第七天,吃的都是第四天长出来的苹果。
示例 2:
输入: apples = 3,0,0,0,0,2, days = 3,0,0,0,0,2 \texttt{apples = 3,0,0,0,0,2, days = 3,0,0,0,0,2} apples = 3,0,0,0,0,2, days = 3,0,0,0,0,2
输出: 5 \texttt{5} 5
解释:可以吃掉 5 \texttt{5} 5 个苹果:
- 第一天到第三天,吃的都是第一天长出来的苹果。
- 第四天和第五天不吃苹果。
- 第六天和第七天,吃的都是第六天长出来的苹果。
数据范围
- n = apples.length = days.length \texttt{n} = \texttt{apples.length} = \texttt{days.length} n=apples.length=days.length
- 1 ≤ n ≤ 2 × 10 4 \texttt{1} \le \texttt{n} \le \texttt{2} \times \texttt{10}^\texttt{4} 1≤n≤2×104
- 0 ≤ applesi, daysi ≤ 2 × 10 4 \texttt{0} \le \texttt{applesi, daysi} \le \texttt{2} \times \texttt{10}^\texttt{4} 0≤applesi, daysi≤2×104
- 当且仅当 applesi = 0 \texttt{applesi} = \texttt{0} applesi=0 时, daysi = 0 \texttt{daysi} = \texttt{0} daysi=0 才成立
解法
思路和算法
为方便表述,将未腐烂的苹果称为新鲜苹果,将苹果开始腐烂的前一天称为苹果的最后有效期。
为了将吃苹果的数目最大化,应使用贪心策略,遵循以下两点原则。
-
如果当天有剩余的新鲜苹果,则应该吃苹果。理由如下:如果当天不吃苹果,则剩余的新鲜苹果的有效期减少一天,可以吃的苹果数目不变或减少,不可能增加。
-
如果当天有多个剩余的新鲜苹果,则应该吃最后有效期最近的苹果。理由如下:用 a a a 和 b b b 分别表示两个苹果,其中 a a a 苹果是最后有效期最近的苹果, b b b 苹果的最后有效期大于 a a a 苹果,如果先吃 a a a 苹果则一定能在第二天吃 b b b 苹果,如果先吃 b b b 苹果则可能在第二天 a a a 苹果腐烂而无法使用,因此先吃 b b b 苹果的情况下可以吃的苹果数目小于等于先吃 a a a 苹果的情况下可以吃的苹果数目,先吃 a a a 苹果的情况下可以吃的苹果数目最多。
根据贪心策略,可以模拟吃苹果的过程,当有剩余的新鲜苹果时,吃最后有效期最近的苹果。为了得到最后有效期最近的苹果,应使用优先队列存储苹果的最后有效期,优先队列的队首元素是最近的最后有效期。
由于每天长出苹果的数目最多是 2 × 10 4 2 \times 10^4 2×104 个,长出苹果的天数最多是 2 × 10 4 2 \times 10^4 2×104 天,因此苹果总数最多是 4 × 10 8 4 \times 10^8 4×108 个,不能直接使用优先队列存储每个苹果的最后有效期。为了降低复杂度,优先队列应存储二元组,每个二元组包含苹果的最后有效期和苹果数目,优先队列的队首二元组包含最近的最后有效期和对应的苹果数目,此时优先队列中的二元组数目最多为长出苹果的天数 n n n。
吃苹果的过程可以分成两个阶段:第一阶段是第 0 0 0 天到第 n − 1 n - 1 n−1 天,每天都可能长出苹果,需要同时考虑新长出的苹果和吃掉的苹果;第二阶段是第 n n n 天及以后,不会再长出苹果,只需要考虑吃掉的苹果。
第一阶段中,对于每个下标 i i i,执行如下操作。
-
将优先队列中的所有最后有效期小于 i i i 的二元组取出,这些二元组表示第 i i i 天的时候已经腐烂的苹果,无法食用。
-
如果 apples i > 0 \textit{apples}i > 0 applesi>0,则第 i i i 天长出苹果,新长出的苹果的最后有效期是 i + days i − 1 i + \textit{days}i - 1 i+daysi−1,将二元组 ( i + days i − 1 , apples i ) (i + \textit{days}i - 1, \textit{apples}i) (i+daysi−1,applesi) 加入优先队列。
-
如果优先队列不为空,则第 i i i 天可以吃一个新鲜苹果,对应优先队列的队首二元组。将吃苹果的数目加 1 1 1,用 top \textit{top} top 表示优先队列的队首二元组,则该二元组中的最后有效期是 top 0 \textit{top}0 top0,苹果数目是 top 1 \textit{top}1 top1,将 top 1 \textit{top}1 top1 减 1 1 1,如果 top 1 \textit{top}1 top1 变成 0 0 0 则将 top \textit{top} top 从优先队列中取出。
第二阶段中,使用 day \textit{day} day 表示当前日期,初始时 day = n \textit{day} = n day=n,执行如下操作。
-
将优先队列中的所有最后有效期小于 day \textit{day} day 的二元组取出,这些二元组表示第 day \textit{day} day 天的时候已经腐烂的苹果,无法食用。
-
如果优先队列不为空,则从第 day \textit{day} day 天开始可以吃一个或多个新鲜苹果,对应优先队列的队首二元组。用 top \textit{top} top 表示优先队列的队首二元组,则该二元组中的最后有效期是 top 0 \textit{top}0 top0,苹果数目是 top 1 \textit{top}1 top1,从第 day \textit{day} day 天到最后有效期共有 top 0 − day + 1 \textit{top}0 - \textit{day} + 1 top0−day+1 天,因此可以吃的苹果数目是 count = min ( top 0 − day + 1 , top 1 ) \textit{count} = \min(\textit{top}0 - \textit{day} + 1, \textit{top}1) count=min(top0−day+1,top1),将吃苹果的数目加 count \textit{count} count,将 day \textit{day} day 加 count \textit{count} count。
当优先队列为空时,操作结束,此时得到吃苹果的最大数目。
代码
java
class Solution {
public int eatenApples(int[] apples, int[] days) {
int total = 0;
PriorityQueue<int[]> pq = new PriorityQueue<int[]>((a, b) -> a[0] - b[0]);
int n = apples.length;
for (int i = 0; i < n; i++) {
while (!pq.isEmpty() && pq.peek()[0] < i) {
pq.poll();
}
if (apples[i] > 0) {
pq.offer(new int[]{i + days[i] - 1, apples[i]});
}
if (!pq.isEmpty()) {
total++;
int[] top = pq.peek();
top[1]--;
if (top[1] == 0) {
pq.poll();
}
}
}
int day = n;
while (!pq.isEmpty()) {
while (!pq.isEmpty() && pq.peek()[0] < day) {
pq.poll();
}
if (!pq.isEmpty()) {
int[] top = pq.poll();
int count = Math.min(top[0] - day + 1, top[1]);
total += count;
day += count;
}
}
return total;
}
}
复杂度分析
-
时间复杂度: O ( n log n ) O(n \log n) O(nlogn),其中 n n n 是数组 apples \textit{apples} apples 和 days \textit{days} days 的长度。需要遍历数组 apples \textit{apples} apples 和 days \textit{days} days 一次,优先队列中最多有 n n n 个二元组,每个二元组最多加入优先队列和从优先队列中取出各一次,因此时间复杂度是 O ( n log n ) O(n \log n) O(nlogn)。
-
空间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 apples \textit{apples} apples 和 days \textit{days} days 的长度。优先队列最多有 n n n 个二元组。