返回首页返回书籍《Hello 算法》
目录

目录 · CONTENTS

  1. 01序
  2. 02前言
  3. 03关于本书
  4. 04如何使用本书
  5. 05小结
  6. 06初识算法
  7. 07算法无处不在
  8. 08算法是什么
  9. 09小结
  10. 10复杂度分析
  11. 11算法效率评估
  12. 12迭代与递归
  13. 13时间复杂度
  14. 14空间复杂度
  15. 15小结
  16. 16练习
  17. 17数据结构
  18. 18数据结构分类
  19. 19基本数据类型
  20. 20数字编码
  21. 21字符编码
  22. 22小结
  23. 23练习
  24. 24数组与链表
  25. 25数组
  26. 26链表
  27. 27列表
  28. 28内存与缓存
  29. 29小结
  30. 30练习
  31. 31栈与队列
  32. 32栈
  33. 33队列
  34. 34双向队列
  35. 35小结
  36. 36练习
  37. 37哈希表
  38. 38哈希表
  39. 39哈希冲突
  40. 40哈希算法
  41. 41小结
  42. 42练习
  43. 43树
  44. 44二叉树
  45. 45二叉树遍历
  46. 46二叉树数组表示
  47. 47二叉搜索树
  48. 48AVL 树
  49. 49小结
  50. 50练习
  51. 51堆
  52. 52堆
  53. 53建堆操作
  54. 54Top-k 问题
  55. 55小结
  56. 56练习
  57. 57图
  58. 58图
  59. 59图的基础操作
  60. 60图的遍历
  61. 61小结
  62. 62练习
  63. 63搜索
  64. 64二分查找
  65. 65二分查找插入点
  66. 66二分查找边界
  67. 67哈希优化策略
  68. 68重识搜索算法
  69. 69小结
  70. 70练习
  71. 71排序
  72. 72排序算法
  73. 73选择排序
  74. 74冒泡排序
  75. 75插入排序
  76. 76快速排序
  77. 77归并排序
  78. 78堆排序
  79. 79桶排序
  80. 80计数排序
  81. 81基数排序
  82. 82小结
  83. 83练习
  84. 84分治
  85. 85分治算法
  86. 86分治搜索策略
  87. 87构建二叉树问题
  88. 88汉诺塔问题
  89. 89小结
  90. 90练习
  91. 91回溯
  92. 92回溯算法
  93. 93全排列问题
  94. 94子集和问题
  95. 95n 皇后问题
  96. 96小结
  97. 97练习
  98. 98动态规划
  99. 99初探动态规划
  100. 100动态规划问题特性
  101. 101动态规划解题思路
  102. 1020-1 背包问题
  103. 103完全背包问题
  104. 104编辑距离问题
  105. 105小结
  106. 106练习
  107. 107贪心
  108. 108贪心算法
  109. 109分数背包问题
  110. 110最大容量问题
  111. 111最大切分乘积问题
  112. 112小结
  113. 113练习
  114. 114附录
  115. 115编程环境安装
  116. 116一起参与创作
  117. 117术语表
  118. 118参考文献
  119. 119纸质书
第 110 / 119 节

第 110 节 · SECTION 110

最大容量问题

问题

输入一个数组 $ht$ ,其中的每个元素代表一个垂直隔板的高度。数组中的任意两个隔板,以及它们之间的空间可以组成一个容器。

容器的容量等于高度和宽度的乘积(面积),其中高度由较短的隔板决定,宽度是两个隔板的数组索引之差。

请在数组中选择两个隔板,使得组成的容器的容量最大,返回最大容量。示例如下图所示。

最大容量问题的示例数据

容器由任意两个隔板围成,因此本题的状态为两个隔板的索引,记为 $[i, j]$ 。

根据题意,容量等于高度乘以宽度,其中高度由短板决定,宽度是两隔板的数组索引之差。设容量为 $cap[i, j]$ ,则可得计算公式:

$$ cap[i, j] = \min(ht[i], ht[j]) \times (j - i) $$

设数组长度为 $n$ ,两个隔板的组合数量(状态总数)为 $C_n^2 = \frac{n(n - 1)}{2}$ 个。最直接地,我们可以穷举所有状态,从而求得最大容量,时间复杂度为 $O(n^2)$ 。

贪心策略确定

这道题还有更高效率的解法。如下图所示,现选取一个状态 $[i, j]$ ,其满足索引 $i < j$ 且高度 $ht[i] < ht[j]$ ,即 $i$ 为短板、$j$ 为长板。

初始状态

如下图所示,若此时将长板 $j$ 向短板 $i$ 靠近,则容量一定变小。

这是因为在移动长板 $j$ 后,宽度 $j-i$ 肯定变小;而高度由短板决定,因此高度只可能不变( $i$ 仍为短板)或变小(移动后的 $j$ 成为短板)。

向内移动长板后的状态

反向思考,我们只有向内收缩短板 $i$ ,才有可能使容量变大。因为虽然宽度一定变小,但高度可能会变大(移动后的短板 $i$ 可能会变长)。例如在下图中,移动短板后面积变大。

向内移动短板后的状态

由此便可推出本题的贪心策略:初始化两指针,使其分列容器两端,每轮向内收缩短板对应的指针,直至两指针相遇。

下图展示了贪心策略的执行过程。

  1. 初始状态下,指针 $i$ 和 $j$ 分列数组两端。
  2. 计算当前状态的容量 $cap[i, j]$ ,并更新最大容量。
  3. 比较板 $i$ 和板 $j$ 的高度,并将短板向内移动一格。
  4. 循环执行第 2. 步和第 3. 步,直至 $i$ 和 $j$ 相遇时结束。

<1>

最大容量问题的贪心过程

<2>

max_capacity_greedy_step2

<3>

max_capacity_greedy_step3

<4>

max_capacity_greedy_step4

<5>

max_capacity_greedy_step5

<6>

max_capacity_greedy_step6

<7>

max_capacity_greedy_step7

<8>

max_capacity_greedy_step8

<9>

max_capacity_greedy_step9

代码实现

代码循环最多 $n$ 轮,因此时间复杂度为 $O(n)$ 。

变量 $i$、$j$、$res$ 使用常数大小的额外空间,因此空间复杂度为 $O(1)$ 。

[file]{max_capacity}-[class]{}-[func]{max_capacity}

正确性证明

之所以贪心比穷举更快,是因为每轮的贪心选择都会“跳过”一些状态。

比如在状态 $cap[i, j]$ 下,$i$ 为短板、$j$ 为长板。若贪心地将短板 $i$ 向内移动一格,会导致下图所示的状态被“跳过”。这意味着之后无法验证这些状态的容量大小。

$$ cap[i, i+1], cap[i, i+2], \dots, cap[i, j-2], cap[i, j-1] $$

移动短板导致被跳过的状态

观察发现,这些被跳过的状态实际上就是将长板 $j$ 向内移动的所有状态。前面我们已经证明内移长板一定会导致容量变小。也就是说,被跳过的状态都不可能是最优解,跳过它们不会导致错过最优解。

以上分析说明,移动短板的操作是“安全”的,贪心策略是有效的。