LeetCode 11 Container With Most Water 풀이: 짧은 쪽을 이동하는 투 포인터
작성·코드 검증: solve together · 작성 언어: Python
0. 문제 링크와 출처
- 문제: 11. Container With Most Water
- 출처: LeetCode
- 자체 판단 난이도: Level 3
모든 선의 쌍을 비교하면 최대 10^5개 선에서 O(n²) 시간이 걸려 제한을 넘는다. 짧은
선이 컨테이너 높이를 결정한다는 점을 이용해 포인터를 한쪽씩 옮기면 후보를 선형 시간에
줄일 수 있어 Level 3으로 판단했다.
1. 문제 요약
height[i]는 x축의 위치 i에 세워진 수직선의 높이다. 두 선과 x축이 만드는
컨테이너가 담을 수 있는 물의 최대량을 구한다.
두 선의 인덱스가 left, right일 때 컨테이너의 넓이는 다음과 같다.
물의 양 = min(height[left], height[right]) * (right - left)
컨테이너의 높이는 두 선 중 짧은 선을 넘을 수 없고, 너비는 두 인덱스 사이의 거리다.
높이 배열의 길이는 2 이상 10^5 이하이며, 높이는 0 이상 10^4 이하다.
2. 문제 해결에 사용되는 알고리즘이나 자료구조 설명
그리디 선택 기준
가장 넓은 컨테이너에서 시작하기 위해 left = 0, right = n - 1로 초기화한다.
현재 두 선으로 만들 수 있는 물의 양을 계산한 뒤, 두 선 중 더 짧은 쪽의 포인터를
안쪽으로 한 칸 이동한다.
height[left] <= height[right]이면left를 증가시킨다.- 그렇지 않으면
right를 감소시킨다.
두 포인터가 만날 때까지 반복한다.
짧은 선을 옮겨도 최적해를 놓치지 않는 이유
현재 왼쪽 선이 짧다고 하자. 왼쪽 선을 고정한 채 오른쪽만 안쪽으로 옮기면 너비가 줄고,
높이는 계속 height[left] 이하로 제한된다. 따라서 이렇게 만드는 모든 컨테이너는 현재
넓이보다 작다. 이 왼쪽 선을 더는 후보로 볼 필요가 없으므로 left를 옮긴다. 오른쪽 선이
더 짧을 때도 같은 논리로 right를 옮긴다.
공식 예제의 첫 세 비교는 이동 규칙을 이렇게 보여준다.
left |
right |
양끝 높이 | 넓이 | 다음 이동 |
|---|---|---|---|---|
| 0 | 8 | 1, 7 | 8 | 왼쪽 높이 1이 짧으므로 left += 1 |
| 1 | 8 | 8, 7 | 49 | 오른쪽 높이 7이 짧으므로 right -= 1 |
| 1 | 7 | 8, 3 | 18 | 오른쪽 높이 3이 짧으므로 right -= 1 |
첫 비교에서 왼쪽 선을 남기고 오른쪽을 더 옮겨도 높이는 1을 넘지 않고 너비만 감소한다. 따라서 짧은 쪽을 버리는 이동만이 더 높은 경계를 찾아 넓이를 회복할 가능성을 남긴다. 높이가 같다면 어느 한쪽을 옮겨도 같은 논리가 적용된다. 구현에서는 왼쪽을 이동한다.
시간·공간 복잡도
반복할 때마다 왼쪽 또는 오른쪽 포인터 하나가 반드시 이동하고, 각 포인터는 배열을 한 번만
통과한다. 시간 복잡도는 O(n), 추가 공간 복잡도는 O(1)이다.
3. 문제 해결 코드 (Python)
class Solution:
def maxArea(self, height: list[int]) -> int:
left = 0
right = len(height) - 1
best = 0
while left < right:
width = right - left
area = min(height[left], height[right]) * width
best = max(best, area)
if height[left] <= height[right]:
left += 1
else:
right -= 1
return best
현재 넓이를 계산한 후 더 짧은 선의 포인터를 이동한다. 높이가 같을 때는 왼쪽을
이동하지만, 오른쪽을 이동해도 최댓값을 놓치지 않는다. width는 선 사이의 간격이므로
right - left로 계산하며, 양끝 인덱스를 모두 포함하는 길이인 right - left + 1이 아니다.
복사해 실행할 테스트
아래 assert를 풀이 코드와 같은 Python 파일에 붙여 실행하면 공식 예제와 높이 경계를 확인할 수 있다.
solver = Solution()
assert solver.maxArea([1, 8, 6, 2, 5, 4, 8, 3, 7]) == 49
assert solver.maxArea([1, 1]) == 1
assert solver.maxArea([0, 0, 0]) == 0
4. 공식 예제와 추가 검증
공식 예제와 동일한 높이, 0 높이, 양끝이 아닌 내부의 높은 선을 포함한 케이스를 Python 코드로 실행했다.
공식 예제
| 입력 | 기대 출력 | 확인 결과 |
|---|---|---|
height = [1, 8, 6, 2, 5, 4, 8, 3, 7] |
49 | 통과 |
height = [1, 1] |
1 | 통과 |
추가 검증 1: 세 선의 양끝과 가운데
입력: height = [1, 2, 1]
기대 출력: 2
확인 결과: 통과
검증 목적: 짧은 양끝을 이동해 가운데 높이를 탐색하는지 확인
추가 검증 2: 모든 높이가 같은 경우
입력: height = [5, 5, 5]
기대 출력: 10
확인 결과: 통과
검증 목적: 같은 높이에서 한 포인터만 이동하는지 확인
추가 검증 3: 높이가 모두 0인 경우
입력: height = [0, 0, 0]
기대 출력: 0
확인 결과: 통과
검증 목적: 높이가 0인 컨테이너의 경계 처리 확인
추가 검증 4: 한쪽에 매우 높은 선이 있는 경우
입력: height = [1, 100, 2, 3, 4]
기대 출력: 12
확인 결과: 통과
검증 목적: 높은 선보다 반대쪽의 짧은 선이 넓이를 제한하는지 확인
5. 다른 풀이 방식 검색 및 비교
- Container With Most Water 공식 문제는 두 수직선과 x축이 만드는 컨테이너의 최대 물의 양을 정의한다.
- NeetCode의 Container With Most Water 설명은 완전탐색과 양끝 투 포인터를 비교하고, 짧은 선을 이동하는 이유를 설명한다.
완전탐색은 모든 쌍을 확인해 직관적이지만 O(n^2) 시간이 필요하다. 양끝에서 시작해
짧은 선을 이동하는 투 포인터는 최적이 될 수 없는 쌍을 증명과 함께 버리므로 O(n)에
최댓값을 구한다. 높이가 같은 경우 한 포인터만 이동하는 것이 두 포인터를 모두 이동하는
것보다 안전하다.
마무리
Container With Most Water는 너비와 짧은 선의 높이가 함께 넓이를 결정한다는 점을 이용하는
그리디 투 포인터 문제다. 처음에는 가장 넓은 구간을 확인하고, 더 큰 넓이를 만들 가능성이
있는 짧은 선만 교체하면 모든 포인터 이동을 O(n) 안에 처리할 수 있다.