盛最多水的容器怎么用双指针解?为什么移动较矮的那条边?
简化版
给一排高度不同的挡板,选两条围成容器,盛水量 = 较矮的高度 × 两板间距。用对撞指针:left、right 从两端开始,每次算面积、更新最大值,然后移动较矮的那条边(左右哪个矮就移哪个)。因为容器高度由短板决定,移动高的一边面积不可能变大,只有移动矮的一边才有希望变大。O(n) 一趟解决。
详细版
int maxArea(int[] h) {
int left = 0, right = h.length - 1, max = 0;
while (left < right) {
int area = Math.min(h[left], h[right]) * (right - left); // 短板 × 宽度
max = Math.max(max, area);
if (h[left] < h[right]) left++; // 移动较矮的一边(左边矮)
else right--; // 右边矮(或相等)就移右
}
return max;
}
- 面积 =
min(h[left], h[right]) × (right - left):宽是间距,高是短板。 - 移动较矮的一边:
h[left] < h[right]就left++,否则right--。 - 时间 O(n)、空间 O(1)。
完整版教学
一、面积由短板和宽度决定
容器能盛多少水,取决于两条边:
- 宽度 = 两条边的间距
right - left。 - 高度 = 较矮的那条边(水会从矮的那边溢出,所以受短板限制)。
所以面积 = min(h[left], h[right]) × (right - left)。这个「短板效应」是理解本题的基础——高度永远由矮的一边决定,这也是「移动矮边」策略的依据。
二、为什么移动较矮的一边(核心)
这是本题最关键、最容易想不通的地方。从两端开始,当前面积由短板和宽度决定。下一步移动指针,宽度必然减 1(指针向内收),要想面积变大,只能寄希望于高度变大。分析移动哪一边:
- 移动较高的那条边:高度仍受较矮的那条边限制(没变),而宽度减小了 → 面积一定变小或不变,毫无意义。
- 移动较矮的那条边:矮边被换掉,新的边可能更高,高度有机会增大,配合宽度减小,面积有可能变大。
所以每次都移动较矮的边,才有可能找到更大的面积;移动高的边是纯亏。这是一种贪心:放弃「不可能更优」的方向。
三、为什么这样不会错过最优解(正确性)
需要论证:移动矮边时,「被丢弃的那些组合」里没有更优解。假设当前 h[left] < h[right],我们要 left++,相当于放弃了「left 和 right 左边所有位置」的组合。为什么它们不可能更优?
- 这些组合的宽度都比当前小(right 左移了)。
- 它们的高度都受
h[left]限制(因为h[left]是短板,和任何≤ right的位置组合,高度都 ≤h[left])。 - 宽更小、高不超过
h[left]→ 面积都 ≤ 当前面积。
所以放弃它们是安全的,不会错过更优解。移动矮边「排除了一整批不可能更优的组合」,这正是双指针 O(n) 且正确的保证。
四、走一个例子
h = [1,8,6,2,5,4,8,3,7]
left=0(1),right=8(7): 面积=min(1,7)*8=8,移矮边(左)left++
left=1(8),right=8(7): 面积=min(8,7)*7=49,移矮边(右)right--
left=1(8),right=7(3): 面积=min(8,3)*6=18,移右 right--
... 继续,最大面积 49
五、和「接雨水」的区别
盛水容器和「接雨水」(LeetCode 42)容易混:
- 盛最多水的容器:选两条边围成一个容器,求最大面积,双指针移矮边,O(n)。
- 接雨水:一排柱子,求所有凹槽能接的雨水总量。也能用双指针,但逻辑不同——每个位置的水由「左边最高」和「右边最高」的较小值决定。
两题都用双指针但模型不同,别套错。
六、复杂度与要点
- 时间 O(n)、空间 O(1)。
- 要点:面积 = 短板 × 宽度;每次移动较矮的边(移高边面积必不增);正确性在于「移矮边排除的组合都不可能更优」。
证明移动短板时,可以固定当前短边:任何更靠内的另一端都会让宽度变小,而高度上限仍受这条短边限制,所以面积不可能更大。正是这个支配关系允许一次排除固定短边对应的所有内部组合。
七、把不变量、推演与工程边界落到代码上
算法正确性的核心不是记住某个 while,而是始终维护这个不变量:当前面积由宽度与较短边决定,移动较高边不可能在宽度变小时改善短板。
对应的状态推进是:计算面积后只移动较短的一端;相等时移动任意一端都不会漏掉更优解。每次推进前都应能解释“为什么被排除的部分不可能再包含答案”,否则只是碰巧通过样例。
初始化边界与状态
while 尚未结束:
根据当前状态作出唯一可证明安全的选择
更新边界、计数或局部结构
断言不变量仍然成立
返回不变量在终止状态下推出的答案
复杂度不能只背一个符号。两个指针各最多移动 n-1 次,时间 O(n)。
带数字走一遍:高度 [1,8,6,2,5,4,8,3,7] 的最优是下标 1 与 8,面积 7×7=49。手算时要同时记录下标、区间含义和本轮排除的候选,才能暴露等号与边界错误。
| 核对项 | 结论 |
|---|---|
| 前提 | 仅适用于由两条边围成容器,不等同于接雨水的逐位置累加 |
| 时间复杂度 | O(n) |
| 额外空间 | O(1) |
| 关键边界 | 面积可能超 int;先计算再移动,不能错误移动较高边 |
| 替代方案 | 接雨水需要左右最大值或单调栈 |
易错点:模板只有在前提和不变量一致时才成立;换了区间定义、重复值规则或输出语义,等号与推进方式就必须重新推导。
实现完成后至少检查五类用例:
- 空输入或题目允许的最小规模,验证初始化不会越界。
- 单元素与两个元素,验证循环条件和最后一次推进。
- 大量重复值,验证相等分支、稳定性或去重语义。
- 已满足目标性质与极端逆序/偏斜分布,观察最好和退化路径。
- 上面的数字样例逐轮手算,并与程序记录的状态轨迹逐项对照。
八、常见误区与追问
- 误区:只要记住模板就适用于所有输入。 本题成立的前提是“仅适用于由两条边围成容器,不等同于接雨水的逐位置累加”,前提被破坏后必须换算法或重新证明。
- 误区:复杂度只写 O(n) 就算完整。 还要说明输入参数、预处理、递归栈以及最坏退化条件;本题的推导是“两个指针各最多移动 n-1 次,时间 O(n)”。
- 误区:重复值和边界值不会改变代码。 面积可能超 int;先计算再移动,不能错误移动较高边。
- 追问:为什么每次推进不会漏掉答案? 因为始终维护“当前面积由宽度与较短边决定,移动较高边不可能在宽度变小时改善短板”,被舍弃区域已由顺序或状态关系证明不可能更优。
- 追问:用一个数字例子怎么讲? 可以从“高度 [1,8,6,2,5,4,8,3,7] 的最优是下标 1 与 8,面积 7×7=49”开始,逐轮写出状态与被排除区间。
- 追问:什么时候不该使用这个方案? 当前提不满足或退化代价不可接受时,应考虑“接雨水需要左右最大值或单调栈”。
- 追问:如何设计自测避免只过样例? 同时覆盖最小规模、重复值、极端分布、无解/边界解,并对关键状态加断言。
九、加强记忆
盛最多水的容器:面积 = 较矮高度 × 宽度(短板效应)。对撞指针从两端开始,每次算面积、移动较矮的那条边。为什么移矮边:宽度每步必减,移高边高度不变(仍受矮边限)→ 面积必减;移矮边高度才可能增大 → 面积才可能变大。正确性:移矮边放弃的组合「宽更小、高不超过矮边」,都不可能更优。O(n) 时间、O(1) 空间。别和「接雨水」混。