# 盛最多水的容器 (Container With Most Water) ## 题目描述 给定一个长度为 `n` 的整数数组 `height`。有 `n` 条垂直线,第 `i` 条线的两个端点是 `(i, 0)` 和 `(i, height[i])`。 找出两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。 返回容器可以储存的最大水量。 **说明:**你不能倾斜容器。 ### 示例 **示例 1:** ``` 输入:[1,8,6,2,5,4,8,3,7] 输出:49 解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。 ``` **示例 2:** ``` 输入:[1,1] 输出:1 ``` ### 约束条件 - `n == height.length` - `2 <= n <= 10^5` - `0 <= height[i] <= 10^4` ## 思路推导 ### 暴力解法分析 **最直观的思路**:枚举所有可能的线对,计算每对线构成的容器容量。 ```python def maxArea(height): max_water = 0 n = len(height) for i in range(n): for j in range(i+1, n): # 容量 = 宽度 × 高度 width = j - i h = min(height[i], height[j]) max_water = max(max_water, width * h) return max_water ``` **时间复杂度**:O(n²) - 外层循环:O(n) - 内层循环:O(n) - 总计:O(n) × O(n) = O(n²) **空间复杂度**:O(1) **问题分析**: 1. 效率低:n=10⁵ 时,n² 不可接受 2. 重复计算:很多组合明显不可能最优 3. 无法利用单调性优化 ### 优化思考 - 第一步:双指针策略 **观察**:容器的容量由 `min(height[left], height[right]) × (right - left)` 决定 **关键问题**:如何移动指针才能找到更大的容量? **直觉思考**: - 如果移动较高的指针,宽度减小,高度只能保持不变或减小 - 如果移动较短的指针,虽然宽度减小,但可能会找到更高的线 **为什么这样思考?** - 容量受限于较短的线 - 移动较短的线才有可能增加容量 - 移动较高的线不可能增加容量 **优化后的思路**: ```python left, right = 0, len(height) - 1 max_water = 0 while left < right: width = right - left h = min(height[left], height[right]) max_water = max(max_water, width * h) # 移动较短的指针 if height[left] < height[right]: left += 1 else: right -= 1 ``` **时间复杂度**:O(n) - 每次移动一个指针 - 最多移动 n 次 - 总计:O(n) ### 优化思考 - 第二步:数学证明 **问题**:双指针法一定能找到最优解吗? **证明**: 假设当前指针在 `left` 和 `right`,且 `height[left] < height[right]`。 **当前容量**:`S = height[left] × (right - left)` **如果移动 right 指针**: - 新容量:`S' = min(height[left], height[right-1]) × (right - 1 - left)` - 由于 `height[left] < height[right]`,且 `right-1 < right` - 所以 `S' <= height[left] × (right - 1 - left) < height[left] × (right - left) = S` **结论**:移动较高的指针不会得到更大的容量。 **因此**:每次移动较短的指针,可以保证不遗漏最优解。 ## 解题思路 ### 方法一:双指针法(最优解) **核心思想:**使用两个指针,一个在数组开头,一个在数组末尾。每次移动较短的指针向中间靠拢。 **为什么这样做?** - 容器的容量由 `min(height[left], height[right]) * (right - left)` 决定 - 如果移动较高的指针,宽度减小,高度只能保持不变或减小,容量一定不会增大 - 如果移动较短的指针,虽然宽度减小,但可能会找到更高的线,从而增大容量 **算法步骤:** 1. 初始化 `left = 0`,`right = len(height) - 1`,`maxArea = 0` 2. 当 `left < right` 时: - 计算当前面积:`area = min(height[left], height[right]) * (right - left)` - 更新 `maxArea = max(maxArea, area)` - 如果 `height[left] < height[right]`,则 `left++`,否则 `right--` 3. 返回 `maxArea` ### 方法二:暴力枚举(不推荐) 枚举所有可能的线对,计算每对线构成的容器容量,取最大值。时间复杂度 O(n²),会超时。 ## 代码实现 ### Go 实现 ```go package main import ( "fmt" ) func maxArea(height []int) int { left, right := 0, len(height)-1 maxArea := 0 for left < right { // 计算当前面积 width := right - left h := height[left] if height[right] < h { h = height[right] } area := width * h // 更新最大面积 if area > maxArea { maxArea = area } // 移动较短的指针 if height[left] < height[right] { left++ } else { right-- } } return maxArea } // 测试用例 func main() { // 测试用例1 height1 := []int{1, 8, 6, 2, 5, 4, 8, 3, 7} fmt.Printf("输入: %v\n", height1) fmt.Printf("输出: %d\n", maxArea(height1)) // 期望输出: 49 // 测试用例2 height2 := []int{1, 1} fmt.Printf("\n输入: %v\n", height2) fmt.Printf("输出: %d\n", maxArea(height2)) // 期望输出: 1 // 测试用例3: 递增序列 height3 := []int{1, 2, 3, 4, 5} fmt.Printf("\n输入: %v\n", height3) fmt.Printf("输出: %d\n", maxArea(height3)) // 期望输出: 6 // 测试用例4: 递减序列 height4 := []int{5, 4, 3, 2, 1} fmt.Printf("\n输入: %v\n", height4) fmt.Printf("输出: %d\n", maxArea(height4)) // 期望输出: 6 // 测试用例5: 包含0 height5 := []int{0, 2} fmt.Printf("\n输入: %v\n", height5) fmt.Printf("输出: %d\n", maxArea(height5)) // 期望输出: 0 } ``` - **时间复杂度:** O(n) - 只需遍历数组一次,每次移动一个指针 - 指针最多移动 n 次 - **空间复杂度:** O(1) - 只使用了常数级别的额外空间 - 只需要几个变量存储指针和最大面积 ### 暴力枚举 - **时间复杂度:** O(n²) - 需要枚举所有可能的线对 - 共有 n(n-1)/2 种组合 - **空间复杂度:** O(1) - 只需要常数级别的额外空间 ## 进阶问题 ### Q1: 如果可以倾斜容器,问题会如何变化? **A:** 如果可以倾斜容器,问题会变得复杂得多。需要考虑水的倾斜角度和容器的几何形状,这涉及到更多的物理和几何计算。 ### Q2: 如果需要返回构成最大容器的两条线的索引,应该如何修改? **A:** 在更新 `maxArea` 的同时,记录当前的 `left` 和 `right` 索引。 ```go // Go 版本 func maxAreaWithIndex(height []int) (int, int, int) { left, right := 0, len(height)-1 maxArea, bestL, bestR := 0, 0, 0 for left < right { h := height[left] if height[right] < h { h = height[right] } area := (right - left) * h if area > maxArea { maxArea = area bestL, bestR = left, right } if height[left] < height[right] { left++ } else { right-- } } return maxArea, bestL, bestR } ``` ### Q3: 如果数组中有负数,应该如何处理? **A:** 如果高度可以为负数,需要先过滤掉负数或取绝对值。通常物理意义上的高度不应为负,但如果题目允许,可以这样处理: ```go // 处理负数:取绝对值 func maxAreaWithNegative(height []int) int { left, right := 0, len(height)-1 maxArea := 0 for left < right { h := height[left] if height[right] < h { h = height[right] } h = abs(h) // 取绝对值 area := (right - left) * h maxArea = max(maxArea, area) if abs(height[left]) < abs(height[right]) { left++ } else { right-- } } return maxArea } func abs(x int) int { if x < 0 { return -x } return x } ``` ## P7 加分项 ### 1. 深度理解:为什么双指针法一定正确? **数学证明:** 假设当前指针在 `left` 和 `right`,且 `height[left] < height[right]`。 我们要证明:移动 `right` 指针一定不会得到更大的面积。 - 当前面积:`S1 = height[left] * (right - left)` - 移动 `right` 后的面积:`S2 = min(height[left], height[right-1]) * (right - 1 - left)` - 由于 `height[left] < height[right]`,且 `right-1 < right` - 所以 `S2 <= height[left] * (right - 1 - left) < height[left] * (right - left) = S1` 因此,移动较高的指针不会得到更大的面积。 ### 2. 实战扩展:接雨水问题 (Trapping Rain Water) **LeetCode 42:** 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。 ```go func trap(height []int) int { if len(height) < 3 { return 0 } left, right := 0, len(height)-1 leftMax, rightMax := 0, 0 water := 0 for left < right { if height[left] < height[right] { if height[left] >= leftMax { leftMax = height[left] } else { water += leftMax - height[left] } left++ } else { if height[right] >= rightMax { rightMax = height[right] } else { water += rightMax - height[right] } right-- } } return water } ``` **核心区别:** - 盛水容器:找两条线构成最大面积 - 接雨水:计算所有能接的雨水总量 ### 3. 变形题目 #### 变形1:盛最多水的容器 II(允许倾斜) 如果允许容器倾斜,最大水量取决于两条线之间的最小距离和角度。 #### 变形2:三维盛水 给定一个 m × n 的矩阵,每个格子表示高度,找出能盛最多水的四个角构成的容器。 #### 变形3:动态盛水 容器的高度会随时间变化,求某个时间段内能盛的最大水量。 ### 4. 优化技巧 #### 优化1:提前终止 如果当前可能的面积(即使宽度最大)已经小于 `maxArea`,可以提前终止。 ```go func maxAreaOptimized(height []int) int { left, right := 0, len(height)-1 maxArea := 0 for left < right { area := (right - left) * min(height[left], height[right]) maxArea = max(maxArea, area) // 提前终止:如果当前宽度已经很窄,可能无法超过maxArea if right-left <= maxArea/max(height[left], height[right]) { break } if height[left] < height[right] { left++ } else { right-- } } return maxArea } ``` #### 优化2:跳过明显不可能的线 如果移动后的线高度比移动前还低,可以继续移动直到找到更高的线。 ```go func maxAreaSkip(height []int) int { left, right := 0, len(height)-1 maxArea := 0 for left < right { area := (right - left) * min(height[left], height[right]) maxArea = max(maxArea, area) if height[left] < height[right] { oldLeft := left left++ // 跳过比oldLeft还低的线 for left < right && height[left] <= height[oldLeft] { left++ } } else { oldRight := right right-- // 跳过比oldRight还低的线 for left < right && height[right] <= height[oldRight] { right-- } } } return maxArea } ``` ### 5. 实际应用场景 - **水库设计:** 计算水库的最大蓄水量 - **城市规划:** 确定建筑物之间的最佳距离以最大化绿化面积 - **数据压缩:** 在某些压缩算法中寻找最优的分段点 ### 6. 面试技巧 **面试官可能会问:** 1. "为什么双指针法一定能找到最优解?" 2. "如果数组有 10^8 个元素,你的算法还能用吗?" 3. "如何证明你的算法是正确的?" **回答要点:** 1. 给出数学证明(如上所述) 2. 讨论算法的局限性,考虑分布式处理 3. 提供正确的证明思路 ### 7. 相关题目推荐 - LeetCode 42: 接雨水 - LeetCode 11: 盛最多水的容器(本题) - LeetCode 84: 柱状图中最大的矩形 - LeetCode 85: 最大矩形