• LeetCode - 1705 吃苹果的最大数目


    目录

    题目来源

    题目描述

    示例

    提示

    题目解析

    算法源码


    题目来源

    1705. 吃苹果的最大数目 - 力扣(LeetCode)

    题目描述

    有一棵特殊的苹果树,一连 n 天,每天都可以长出若干个苹果。在第 i 天,树上会长出 apples[i] 个苹果,这些苹果将会在 days[i] 天后(也就是说,第 i + days[i] 天时)腐烂,变得无法食用。也可能有那么几天,树上不会长出新的苹果,此时用 apples[i] == 0 且 days[i] == 0 表示。

    你打算每天 最多 吃一个苹果来保证营养均衡。注意,你可以在这 n 天之后继续吃苹果。

    给你两个长度为 n 的整数数组 days 和 apples ,返回你可以吃掉的苹果的最大数目。

    示例

    输入apples = [1,2,3,5,2], days = [3,2,1,4,2]
    输出7
    解释你可以吃掉 7 个苹果:
    - 第一天,你吃掉第一天长出来的苹果。
    - 第二天,你吃掉一个第二天长出来的苹果。
    - 第三天,你吃掉一个第二天长出来的苹果。过了这一天,第三天长  出来的苹果就已经腐烂了。
    - 第四天到第七天,你吃的都是第四天长出来的苹果。
    输入apples = [3,0,0,0,0,2], days = [3,0,0,0,0,2]
    输出5
    解释你可以吃掉 5 个苹果:
    - 第一天到第三天,你吃的都是第一天长出来的苹果。
    - 第四天和第五天不吃苹果。
    - 第六天和第七天,你吃的都是第六天长出来的苹果。

    提示

    • apples.length == n
    • days.length == n
    • 1 <= n <= 2 * 10^4
    • 0 <= apples[i], days[i] <= 2 * 10^4
    • 只有在 apples[i] = 0 时,days[i] = 0 才成立

    题目解析

    本题的解题思路其实就是贪心思维。

    比如你手上有两包临期薯片A和B,假设A薯片明天过期,B薯片后天过期,你一天只能吃一包,那么你如何吃才能吃的多,且不会吃到过期薯片呢?

    答案很简单,先吃快要过期。

    即今天吃A薯片,明天吃B薯片,这样的话,就都赶在每包薯片过期前吃完了。

    如果你今天吃B薯片,则明天你就不能吃A薯片了,因为明天时,A薯片就过期了。

    本题比上面的情况要复杂一点,那就是每天都有新的临期薯片加入,因此每当有新的临期薯片加入时,我们就需要重新将薯片按照过期时间由近到远进行排序,先吃快要过期的。

    这就是本题的解题思路。

    最终代码实现如下

    1. /**
    2. * @param {number[]} apples
    3. * @param {number[]} days
    4. * @return {number}
    5. */
    6. var eatenApples = function (apples, days) {
    7. const pq = [];
    8. let count = 0;
    9. let i = 0;
    10. while (i < apples.length || pq.length !== 0) { // 注意即使没有新苹果的加入,存货苹果还是可能存在未腐烂的
    11. if (apples[i] > 0) {
    12. pq.push({
    13. apple: apples[i],
    14. day: i + days[i],
    15. });
    16. pq.sort((a, b) => a.day - b.day); // 过期时间排序
    17. }
    18. while (true) {
    19. if (pq.length === 0) break;
    20. let head = pq[0];
    21. if (head.day <= i || head.apple === 0) { // 如果过期了,或者没了,则出队
    22. pq.shift();
    23. continue;
    24. } else {
    25. head.apple--;
    26. count++;
    27. break;
    28. }
    29. }
    30. i++;
    31. }
    32. return count;
    33. };

    但是上面这种算法的性能非常低

    上面程序中pq就是一个优先队列,pq中的元素都有一个优先级,优先级高的会先出队。在本题中,过期时间越近,即day越小,则优先级越高。

    而上面程序中,pq的优先队列是基于有序数组实现的,这意味着每次有新元素加入,pq都需要经历至少O(n)时间才能保持优先队列的特性。

    因此,算法的整体时间复杂度就会达到O(n^2),而1 <= n <= 2 * 10^4,因此上面算法的性能就非常低了。

    因此,我们需要优化优先队列的实现。

    优先队列其实并不需要维护成一个严格有序的数组,这样的成本是极高的。优先队列的特性是每次出队时,都让优先级高的出队,因此我们只需要保证队头元素优先级最高即可。

    优先队列通常采用堆结构来实现,所谓堆结构,即一颗完全二叉树。

    那么什么是完全二叉树呢?

    一棵深度为k的有n个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为i(1≤i≤n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树。

    由上图我们可知,完全二叉树的最深的一层如果节点不满的话,则会优先填满左边。 

    并且完全二叉树中某节点的序号为k的话,则其左孩子节点的序号必然为2k+1,其右孩子节点的序号必然为2k+2。因此上面的完全二叉树可以用数组来进行模拟:

    可以发现数组的索引刚好就是完全二叉树节点的序号。 

    堆结构对应的完全二叉树需要满足以下两个条件之一:

    • 父节点要大于或等于其左右孩子节点,此时堆称为最大堆
    • 父节点要小于或等于其左右孩子节点,此时堆称为最小堆

    这样的话,堆结构才能快速地找到最值节点,即堆结构的顶点。

    堆结构模拟优先队列,主要就是实现优先队列的入队和出队操作。

    当我们向堆结构中入队一个新元素,需要先将新元素加入到堆结构对应的数组的尾部,但是这样的话可能会破坏堆结构的顺序性,因此我们需要通过上浮操作,来调整堆的顺序。

    关于上浮操作,请看下面示例:

    如下图,是一个最大堆,父节点的值总是大于其左右子孩子节点的值

     现在我们需要向堆中新增一个元素29,则先放在尾部,假设此时29的序号为k,则其父节点的序号必然为 Math.floor((k-1)/2)

    然后比较29和其父节点值得大小,如果29 > 父节点值,则交换节点的值,完成29的上浮行为

    然后继续比较,29和其父节点值的大小, 如果29 > 父节点值,则交换节点的值,完成29的上浮行为

    直到,29发现其小于等于父节点值时,停止上浮,或者29已经上浮到k=0序号位置,即顶点位置时,停止上浮。

    当我们需要优先队列出队时,即出队头,此时相当于堆结构删除堆顶元素,但是我们不能冒失的直接将堆顶元素删除,这样会让堆结构散架。

    好的做法,是将堆顶元素和堆尾元素值交换,然后将堆尾元素弹出(堆结构可以用数组模拟,因此可以使用pop操作) ,但是此时堆顶元素的值其实并非最大值,因此我们需要使用下沉操作来调整堆结构,维护其顺序性。

    关于下沉操作,我们可以看如下示例:

    下图是一个最大堆,我们现在需要删除堆顶30

     则第一步是交换堆顶元素和堆尾元素的值,然后将堆尾元素弹出

    此时最大堆的顺序性被破坏,我们开始执行下沉操作,所谓下沉操作,即将破坏顺序性的节点12和max(左孩子值,右孩子值) 比较,若12< max(左孩子值,右孩子值),则交换

    当下沉到没有左右孩子,或者大于等于max(左孩子,右孩子)时,即停止下沉。

    我们可以发现,使用堆结构模拟的优先队列,每次入队都会触发上浮操作,每次出队都会触发下沉操作,但是上浮和下沉的次数最多就是完全二叉树的深度,而完全二叉树的深度为logN,也就是说堆结构维护的优先队列每次入队和出队的时间复杂度为O(logN),这要比使用有序数组维护的优先队列的入队出队的时间复杂度O(n),大大提升了效率。 

    在Java语言中已经提供了实现好的优先队列工具类,但是在JavaScript语言中并没有,因此我们需要自己实现一个优先队列。大家可以参考下面算法源码中MyPriorityQueue类的实现。 

    算法源码

    1. /**
    2. * @param {number[]} apples
    3. * @param {number[]} days
    4. * @return {number}
    5. */
    6. var eatenApples = function (apples, days) {
    7. const pq = new MyPriorityQueue((a, b) => a.day - b.day);
    8. let count = 0;
    9. let i = 0;
    10. while (i < apples.length || !pq.isEmpty()) {
    11. if (apples[i] > 0) {
    12. pq.push({
    13. apple: apples[i],
    14. day: i + days[i],
    15. });
    16. }
    17. while (true) {
    18. if (pq.isEmpty()) break;
    19. let head = pq.top();
    20. if (head.day <= i || head.apple === 0) {
    21. pq.shift();
    22. continue;
    23. } else {
    24. head.apple--;
    25. count++;
    26. break;
    27. }
    28. }
    29. i++;
    30. }
    31. return count;
    32. };
    33. class MyPriorityQueue {
    34. constructor(compare) {
    35. this.queue = [];
    36. this.compare = compare;
    37. }
    38. swap(i, j) {
    39. let tmp = this.queue[i];
    40. this.queue[i] = this.queue[j];
    41. this.queue[j] = tmp;
    42. }
    43. top() {
    44. return this.queue[0];
    45. }
    46. /* 判断队是否为空 */
    47. isEmpty() {
    48. return this.queue.length === 0;
    49. }
    50. /* 上浮 */
    51. swim() {
    52. let child = this.queue.length - 1;
    53. while (child !== 0) {
    54. let father = Math.floor((child - 1) / 2);
    55. if (this.compare(this.queue[child], this.queue[father]) < 0) {
    56. this.swap(child, father);
    57. child = father;
    58. } else {
    59. break;
    60. }
    61. }
    62. }
    63. /* 下沉 */
    64. sink() {
    65. let k = 0;
    66. while (true) {
    67. let l = 2 * k + 1;
    68. let r = l + 1;
    69. let K = this.queue[k];
    70. let L = this.queue[l];
    71. let R = this.queue[r];
    72. let t;
    73. if (L && R) {
    74. this.compare(L, R) > 0 ? (t = r) : (t = l);
    75. } else if (L && !R) {
    76. t = l;
    77. } else if (!L && R) {
    78. t = r;
    79. } else {
    80. break;
    81. }
    82. let T = this.queue[t];
    83. if (this.compare(T, K) < 0) {
    84. this.swap(t, k);
    85. k = t;
    86. } else {
    87. break;
    88. }
    89. }
    90. }
    91. /* 入队 */
    92. push(ele) {
    93. this.queue.push(ele);
    94. this.swim();
    95. }
    96. /* 出队 */
    97. shift() {
    98. this.swap(0, this.queue.length - 1);
    99. this.queue.pop();
    100. this.sink();
    101. }
    102. }

  • 相关阅读:
    【软件测试】requests 库请求体字符串解码
    007 数据结构_堆——“C”
    算法通关村第三关-白银挑战双指针思想
    漏洞赏金猎人开源工具集合,自动辅助渗透测试工具
    Mediacodec 编码过程源码解析
    leetCode 45.跳跃游戏 II 贪心算法
    C语言基础知识 -- 初识结构体
    现代修谱,如何处理族员离婚再娶,配偶携子改嫁同服弟等情况
    如何采用Python读取一个图像
    有关git commit --amend的用法及若干个问题
  • 原文地址:https://blog.csdn.net/qfc_128220/article/details/127695013