Skip to content

15.3   Maximum capacity problem

Question

Input an array \(ht\), where each element represents the height of a vertical partition. Any two partitions in the array, along with the space between them, can form a container.

The capacity of the container is the product of the height and the width (area), where the height is determined by the shorter partition, and the width is the difference in array indices between the two partitions.

Please select two partitions in the array that maximize the container's capacity and return this maximum capacity. An example is shown in the following figure.

Example data for the maximum capacity problem

Figure 15-7   Example data for the maximum capacity problem

The container is formed by any two partitions, therefore the state of this problem is represented by the indices of the two partitions, denoted as \([i, j]\).

According to the problem statement, the capacity equals the product of height and width, where the height is determined by the shorter partition, and the width is the difference in array indices between the two partitions. The formula for capacity \(cap[i, j]\) is:

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

Assuming the length of the array is \(n\), the number of combinations of two partitions (total number of states) is \(C_n^2 = \frac{n(n - 1)}{2}\). The most straightforward approach is to enumerate all possible states, resulting in a time complexity of \(O(n^2)\).

1.   Determination of a greedy strategy

There is a more efficient solution to this problem. As shown in the following figure, we select a state \([i, j]\) where the indices \(i < j\) and the height \(ht[i] < ht[j]\), meaning \(i\) is the shorter partition, and \(j\) is the taller one.

Initial state

Figure 15-8   Initial state

As shown in the following figure, if we move the taller partition \(j\) closer to the shorter partition \(i\), the capacity will definitely decrease.

This is because when moving the taller partition \(j\), the width \(j-i\) definitely decreases; and since the height is determined by the shorter partition, the height can only remain the same (if \(i\) remains the shorter partition) or decrease (if the moved \(j\) becomes the shorter partition).

State after moving the taller partition inward

Figure 15-9   State after moving the taller partition inward

Conversely, we can only possibly increase the capacity by moving the shorter partition \(i\) inward. Although the width will definitely decrease, the height may increase (if the moved shorter partition \(i\) becomes taller). For example, in the Figure 15-10 , the area increases after moving the shorter partition.

State after moving the shorter partition inward

Figure 15-10   State after moving the shorter partition inward

This leads us to the greedy strategy for this problem: initialize two pointers at the ends of the container, and in each round, move the pointer corresponding to the shorter partition inward until the two pointers meet.

The following figures illustrate the execution of the greedy strategy.

  1. Initially, the pointers \(i\) and \(j\) are positioned at the ends of the array.
  2. Calculate the current state's capacity \(cap[i, j]\) and update the maximum capacity.
  3. Compare the heights of partitions \(i\) and \(j\), and move the shorter partition inward by one step.
  4. Repeat steps 2. and 3. until \(i\) and \(j\) meet.

The greedy process for maximum capacity problem

max_capacity_greedy_step2

max_capacity_greedy_step3

max_capacity_greedy_step4

max_capacity_greedy_step5

max_capacity_greedy_step6

max_capacity_greedy_step7

max_capacity_greedy_step8

max_capacity_greedy_step9

Figure 15-11   The greedy process for maximum capacity problem

2.   Implementation

The code loops at most \(n\) times, thus the time complexity is \(O(n)\).

The variables \(i\), \(j\), and \(res\) use a constant amount of extra space, thus the space complexity is \(O(1)\).

max_capacity.py
def max_capacity(ht: list[int]) -> int:
    """最大容量:贪心"""
    # 初始化 i, j,使其分列数组两端
    i, j = 0, len(ht) - 1
    # 初始最大容量为 0
    res = 0
    # 循环贪心选择,直至两板相遇
    while i < j:
        # 更新最大容量
        cap = min(ht[i], ht[j]) * (j - i)
        res = max(res, cap)
        # 向内移动短板
        if ht[i] < ht[j]:
            i += 1
        else:
            j -= 1
    return res
max_capacity.cpp
/* 最大容量:贪心 */
int maxCapacity(vector<int> &ht) {
    // 初始化 i, j,使其分列数组两端
    int i = 0, j = ht.size() - 1;
    // 初始最大容量为 0
    int res = 0;
    // 循环贪心选择,直至两板相遇
    while (i < j) {
        // 更新最大容量
        int cap = min(ht[i], ht[j]) * (j - i);
        res = max(res, cap);
        // 向内移动短板
        if (ht[i] < ht[j]) {
            i++;
        } else {
            j--;
        }
    }
    return res;
}
max_capacity.java
/* 最大容量:贪心 */
int maxCapacity(int[] ht) {
    // 初始化 i, j,使其分列数组两端
    int i = 0, j = ht.length - 1;
    // 初始最大容量为 0
    int res = 0;
    // 循环贪心选择,直至两板相遇
    while (i < j) {
        // 更新最大容量
        int cap = Math.min(ht[i], ht[j]) * (j - i);
        res = Math.max(res, cap);
        // 向内移动短板
        if (ht[i] < ht[j]) {
            i++;
        } else {
            j--;
        }
    }
    return res;
}
max_capacity.cs
/* 最大容量:贪心 */
int MaxCapacity(int[] ht) {
    // 初始化 i, j,使其分列数组两端
    int i = 0, j = ht.Length - 1;
    // 初始最大容量为 0
    int res = 0;
    // 循环贪心选择,直至两板相遇
    while (i < j) {
        // 更新最大容量
        int cap = Math.Min(ht[i], ht[j]) * (j - i);
        res = Math.Max(res, cap);
        // 向内移动短板
        if (ht[i] < ht[j]) {
            i++;
        } else {
            j--;
        }
    }
    return res;
}
max_capacity.go
/* 最大容量:贪心 */
func maxCapacity(ht []int) int {
    // 初始化 i, j,使其分列数组两端
    i, j := 0, len(ht)-1
    // 初始最大容量为 0
    res := 0
    // 循环贪心选择,直至两板相遇
    for i < j {
        // 更新最大容量
        capacity := int(math.Min(float64(ht[i]), float64(ht[j]))) * (j - i)
        res = int(math.Max(float64(res), float64(capacity)))
        // 向内移动短板
        if ht[i] < ht[j] {
            i++
        } else {
            j--
        }
    }
    return res
}
max_capacity.swift
/* 最大容量:贪心 */
func maxCapacity(ht: [Int]) -> Int {
    // 初始化 i, j,使其分列数组两端
    var i = ht.startIndex, j = ht.endIndex - 1
    // 初始最大容量为 0
    var res = 0
    // 循环贪心选择,直至两板相遇
    while i < j {
        // 更新最大容量
        let cap = min(ht[i], ht[j]) * (j - i)
        res = max(res, cap)
        // 向内移动短板
        if ht[i] < ht[j] {
            i += 1
        } else {
            j -= 1
        }
    }
    return res
}
max_capacity.js
/* 最大容量:贪心 */
function maxCapacity(ht) {
    // 初始化 i, j,使其分列数组两端
    let i = 0,
        j = ht.length - 1;
    // 初始最大容量为 0
    let res = 0;
    // 循环贪心选择,直至两板相遇
    while (i < j) {
        // 更新最大容量
        const cap = Math.min(ht[i], ht[j]) * (j - i);
        res = Math.max(res, cap);
        // 向内移动短板
        if (ht[i] < ht[j]) {
            i += 1;
        } else {
            j -= 1;
        }
    }
    return res;
}
max_capacity.ts
/* 最大容量:贪心 */
function maxCapacity(ht: number[]): number {
    // 初始化 i, j,使其分列数组两端
    let i = 0,
        j = ht.length - 1;
    // 初始最大容量为 0
    let res = 0;
    // 循环贪心选择,直至两板相遇
    while (i < j) {
        // 更新最大容量
        const cap: number = Math.min(ht[i], ht[j]) * (j - i);
        res = Math.max(res, cap);
        // 向内移动短板
        if (ht[i] < ht[j]) {
            i += 1;
        } else {
            j -= 1;
        }
    }
    return res;
}
max_capacity.dart
/* 最大容量:贪心 */
int maxCapacity(List<int> ht) {
  // 初始化 i, j,使其分列数组两端
  int i = 0, j = ht.length - 1;
  // 初始最大容量为 0
  int res = 0;
  // 循环贪心选择,直至两板相遇
  while (i < j) {
    // 更新最大容量
    int cap = min(ht[i], ht[j]) * (j - i);
    res = max(res, cap);
    // 向内移动短板
    if (ht[i] < ht[j]) {
      i++;
    } else {
      j--;
    }
  }
  return res;
}
max_capacity.rs
/* 最大容量:贪心 */
fn max_capacity(ht: &[i32]) -> i32 {
    // 初始化 i, j,使其分列数组两端
    let mut i = 0;
    let mut j = ht.len() - 1;
    // 初始最大容量为 0
    let mut res = 0;
    // 循环贪心选择,直至两板相遇
    while i < j {
        // 更新最大容量
        let cap = std::cmp::min(ht[i], ht[j]) * (j - i) as i32;
        res = std::cmp::max(res, cap);
        // 向内移动短板
        if ht[i] < ht[j] {
            i += 1;
        } else {
            j -= 1;
        }
    }
    res
}
max_capacity.c
/* 最大容量:贪心 */
int maxCapacity(int ht[], int htLength) {
    // 初始化 i, j,使其分列数组两端
    int i = 0;
    int j = htLength - 1;
    // 初始最大容量为 0
    int res = 0;
    // 循环贪心选择,直至两板相遇
    while (i < j) {
        // 更新最大容量
        int capacity = myMin(ht[i], ht[j]) * (j - i);
        res = myMax(res, capacity);
        // 向内移动短板
        if (ht[i] < ht[j]) {
            i++;
        } else {
            j--;
        }
    }
    return res;
}
max_capacity.kt
/* 最大容量:贪心 */
fun maxCapacity(ht: IntArray): Int {
    // 初始化 i, j,使其分列数组两端
    var i = 0
    var j = ht.size - 1
    // 初始最大容量为 0
    var res = 0
    // 循环贪心选择,直至两板相遇
    while (i < j) {
        // 更新最大容量
        val cap = min(ht[i], ht[j]) * (j - i)
        res = max(res, cap)
        // 向内移动短板
        if (ht[i] < ht[j]) {
            i++
        } else {
            j--
        }
    }
    return res
}
max_capacity.rb
[class]{}-[func]{max_capacity}
max_capacity.zig
[class]{}-[func]{maxCapacity}
Code Visualization

3.   Proof of correctness

The reason why the greedy method is faster than enumeration is that each round of greedy selection "skips" some states.

For example, under the state \(cap[i, j]\) where \(i\) is the shorter partition and \(j\) is the taller partition, greedily moving the shorter partition \(i\) inward by one step leads to the "skipped" states shown below. This means that these states' capacities cannot be verified later.

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

States skipped by moving the shorter partition

Figure 15-12   States skipped by moving the shorter partition

It is observed that these skipped states are actually all states where the taller partition \(j\) is moved inward. We have already proven that moving the taller partition inward will definitely decrease the capacity. Therefore, the skipped states cannot possibly be the optimal solution, and skipping them does not lead to missing the optimal solution.

The analysis shows that the operation of moving the shorter partition is "safe", and the greedy strategy is effective.

Feel free to drop your insights, questions or suggestions