Container With Most Water
음이 아닌 정수 목록 height가 주어집니다. i번째 선은 위치 i에 세워진 높이 height[i]의 수직 벽입니다. 임의의 두 선은 땅과 함께 용기를 이루며, 짧은 선의 높이에 두 선 사이의 거리를 곱한 만큼의 물을 담을 수 있습니다. 다른 선들은 방해가 되지 않습니다. 한 쌍의 선이 담을 수 있는 최대 물의 양을 반환하세요.
함수
- heightinteger-array
- 위치 0, 1, 2 등에서의 선 높이
- 반환값integer
- 두 줄에 담을 수 있는 최대 물의 양
제약 조건
2 ≤ height.length ≤ 1040 ≤ height[i] ≤ 104- 정답은 최대 108이므로 32비트 정수에 들어갑니다.
예제
- 입력
- height = [3, 7, 2, 5, 4, 7, 3, 6]
- 출력
- 36
- 설명
- 위치 1과 7에 있는 선의 높이는 각각 7과 6이고 서로 6만큼 떨어져 있으므로, 담을 수 있는 물은 6 × 6 = 36입니다. 가장 높은 두 선인 위치 1과 5의 7은 7 × 4 = 28만 담을 수 있고, 가장 바깥쪽 쌍은 3 × 7 = 21을 담습니다.
- 입력
- height = [4, 4]
- 출력
- 4
- 설명
- 두 개의 선이 정확히 하나의 컨테이너를 만듭니다. 높이는 4이고 너비는 1이므로 4를 담을 수 있습니다.
제출 시 숨은 테스트 +15개
후속 질문
여기서는 네가 선택한 두 선 사이의 선들이 무시됩니다. 모든 선이 단단한 막대라면, 그 모든 선 사이에 물이 얼마나 고일까요? 이것도 O(n)에 계산할 수 있나요?
힌트
하나씩 열어 보세요. 열 때마다 조금 더 알려 줍니다.
두 개의 바깥쪽 선부터 시작하세요. 이 선들이 가장 넓은 컨테이너를 만듭니다. 양 끝 중 하나를 안쪽으로 옮기면 너비가 한 단위 줄어듭니다. 두 선 중 어느 선이 그 손실을 만회할 수 있을까요?
물은 더 짧은 선에 의해 제한됩니다. 더 긴 선을 안쪽으로 옮기면 그 제한은 그대로 유지되면서 너비만 줄어들므로, 절대 도움이 될 수 없습니다. 더 짧은 선을 교체해야만 더 나아질 가능성이 있습니다.
양쪽 끝에 포인터를 하나씩 둡니다. 두 포인터 사이의 물의 양을 측정해 최댓값을 기록한 다음, 더 짧은 선 쪽의 포인터를 안쪽으로 한 칸 옮깁니다. 포인터가 만날 때까지 반복합니다.
풀이
선 쌍은 약 n²/2개이므로, 선이 10^4개일 때 모든 쌍을 확인하면 곱셈을 5 × 10^7번 해야 합니다. 해결 방법은 물의 양이 한 쌍에서 더 짧은 선에만 달려 있다는 점입니다. 어떤 선이 만들 수 있는 가장 넓은 용기에서 더 짧은 쪽이라는 것을 확인하면, 그 선을 사용하는 더 좁은 용기로는 더 나은 결과를 낼 수 없습니다. 투 포인터를 사용하면 이 사실을 양쪽 끝에서 시작하는 한 번의 순회로 활용할 수 있습니다.
모든 쌍 확인하기
정답이지만 가장 큰 테스트에서는 끝나지 않음
핵심 아이디어
각 컨테이너는 두 위치 i < j의 쌍입니다. 물은 더 낮은 벽을 넘쳐흐를 때까지 차오르고, 두 벽 사이의 바닥 너비는 j - i이므로 이 쌍이 담을 수 있는 양은 min(height[i], height[j]) × (j - i)입니다. 모든 쌍을 확인하고 가장 큰 값을 유지하면, 정의에 따라 답을 구할 수 있습니다.
문제는 쌍의 개수입니다. n개의 선은 n(n-1)/2개의 쌍을 만듭니다. 선이 10^4개라면 약 5 × 10^7개이고, 목록이 두 배가 될 때마다 그 수는 네 배가 됩니다. 컴파일 언어로는 이를 1초도 안 되는 시간에 처리할 수 있지만, Python, Ruby 또는 R에서는 몇 초가 걸리며, n이 10^5에 이르면 어떤 언어로도 처리하기에는 개수가 너무 빠르게 늘어납니다.
알고리즘
best를 0으로 설정합니다.- 각
i에 대해, 그 뒤에 오는 각j에 대해min(height[i], height[j]) × (j - i)를 계산합니다. best와 해당 값 중 더 큰 값을 유지합니다.best를 반환합니다.
def maxArea(height):
n = len(height)
best = 0
for i in range(n):
for j in range(i + 1, n):
water = min(height[i], height[j]) * (j - i)
best = max(best, water)
return best높이가 가장 높은 줄부터
핵심 아이디어
더 짧은 변의 쪽에서 용기를 바라보세요. 선 i가 더 짧은 변이라면, 담기는 물의 양은 height[i]에 거리 값을 곱한 것이며, 짝이 되는 선은 적어도 그만큼 높으면 됩니다. 따라서 i가 더 짧은 변인 경우 만들 수 있는 가장 좋은 용기는, 그보다 높거나 같은 선 중 가장 멀리 있는 선과 짝을 이룹니다.
그런 짝을 빠르게 찾으려면 선들을 가장 높은 것부터 가장 낮은 것 순서로 배치하세요. 선 i를 처리할 때, 그보다 먼저 배치된 모든 선은 높이가 적어도 같으며, 그중 가장 멀리 있는 선은 배치된 인덱스의 가장 왼쪽 또는 가장 오른쪽에 있습니다. 이 두 인덱스 lo와 hi를 추적하면, 선 i가 담을 수 있는 최대 물의 양은 height[i] × max(i - lo, hi - i)입니다. 더 짧은 변을 처리할 때 가장 좋은 용기가 계산되므로, 정답은 이 값들 중 가장 큰 값입니다.
첫 번째 예제에서는 위치 1과 5의 두 7이 먼저 처리되어 28을 담습니다. 다음으로 위치 7의 6이 처리되며, 이때 lo = 1이고 hi = 5이므로 6 × 6 = 36을 담습니다. 이보다 더 짧은 선은 이 값을 넘지 못합니다. 높이가 같은 선들은 어떤 순서로 처리해도 됩니다. 같은 높이인 두 선 중 나중에 처리되는 선은 먼저 처리된 선을 짝으로 볼 수 있습니다.
정렬에는 O(n log n), 순회에는 O(n)이 걸리므로 충분히 빠릅니다. 그래도 순서를 저장하기 위해 O(n)의 메모리가 필요하며, 다음 접근법은 정렬과 메모리를 모두 없앱니다.
알고리즘
- 인덱스를 높이 기준으로 내림차순 정렬합니다.
lo와hi를 해당 순서의 첫 번째 인덱스로 설정하고best를 0으로 설정합니다.- 각 다음 인덱스
i에 대해height[i]에i - lo와hi - i중 큰 값을 곱하고, 최선의 값을 유지합니다. lo와hi를 업데이트하여i를 포함시킵니다.best를 반환합니다.
def maxArea(height):
# Indices from the tallest line to the shortest.
order = sorted(range(len(height)), key=lambda i: height[i], reverse=True)
lo = hi = order[0] # leftmost and rightmost index among the lines placed so far
best = 0
for i in order[1:]:
# Every placed line is at least as tall as line i, so line i is the
# shorter side, and its best partner is the placed line farthest away.
best = max(best, height[i] * max(i - lo, hi - i))
lo = min(lo, i)
hi = max(hi, i)
return best양쪽 끝에서 시작하는 두 포인터
핵심 아이디어
가장 넓은 컨테이너인 left = 0과 right = n-1에서 시작해 그 넓이를 계산합니다. 이제 두 선 중 하나를 제외할 수 있으며, 선택은 정해져 있습니다. 더 짧은 선을 제외하세요. height[left] ≤ height[right]라고 해 봅시다. 선 left를 사용하는 다른 모든 컨테이너는 이 선을 right보다 더 가까운 선과 짝지으므로 더 좁고, 높이도 여전히 height[left] 이하입니다. 어느 것도 계산한 물의 양을 넘지 못하므로 선 left는 더 이상 고려할 필요가 없고, left는 오른쪽으로 한 칸 이동합니다. 더 높은 선을 대신 이동하면 높이의 상한은 그대로인 채 너비만 줄어드므로 결과가 더 나빠질 수밖에 없습니다. 두 높이가 같으면 두 선 모두 더 이상 고려할 필요가 없으며, 어느 쪽을 이동해도 괜찮습니다.
각 단계에서 선 하나를 영구적으로 제외하므로, n-1단계가 지나면 포인터가 만납니다. 최선의 쌍을 건너뛰는 일은 없습니다. 두 선 중 하나가 처음 제외되는 순간, 그때 계산한 컨테이너에는 적어도 그만큼의 물이 담깁니다.
[3, 7, 2, 5, 4, 7, 3, 6]에서 위치 0과 7은 3 × 7 = 21을 담습니다. 3이 더 짧으므로 left를 1로 이동합니다. 위치 1과 7은 6 × 6 = 36을 담고, 이제 6이 더 짧으므로 right를 6으로 이동합니다. 그다음 컨테이너들이 담는 물의 양은 15, 28, 12, 10, 2이므로 답은 36으로 유지됩니다.
알고리즘
left = 0,right = n-1,best = 0으로 설정합니다.left < right인 동안min(height[left], height[right]) × (right - left)를 계산하고 최댓값을 유지합니다.height[left] < height[right]이면left를 오른쪽으로 한 칸 이동합니다. 그렇지 않으면right를 왼쪽으로 한 칸 이동합니다.- 포인터가 만나면
best를 반환합니다.
def maxArea(height):
left, right = 0, len(height) - 1
best = 0
while left < right:
water = min(height[left], height[right]) * (right - left)
best = max(best, water)
# The shorter line cannot do better with any line closer in, so drop it.
if height[left] < height[right]:
left += 1
else:
right -= 1
return best
함정과 경계 사례
투 포인터 루프는 짧으므로, 실수는 세부 사항에서 발생합니다.
- 더 높은 선을 이동하는 경우. 첫 번째 예제에서 결과가 36이 아니라 21이 되는 이유는 다음과 같습니다. 위치 7의 6은 첫 번째 쌍에서 더 높은 선이므로 위치 1의 7과 만나기도 전에 제외됩니다.
- 더 높은 선이나 두 선의 평균을 높이로 사용하는 경우. 물은 더 낮은 벽 위로 넘치므로 높이는 최솟값입니다.
- 너비를 계산할 때 하나를 더하거나 덜하는 경우. 위치
i와j의 선 사이 간격은j - i이지j - i + 1이 아니므로, 이웃한 두 선은 더 낮은 높이에 1을 곱한 양의 물을 담습니다. - 정답이 가장 높은 선이나 양 끝의 쌍을 사용한다고 가정하는 경우. 첫 번째 예제에서 두 개의 7은 28을 담고 양 끝의 쌍은 21을 담지만, 정답은 36입니다.
- 제한이 더 클 때 오버플로가 발생하는 경우. 여기서는 물의 양이 10^8 미만이지만, 높이와 길이가 10^5에 가까우면 곱이 2^31을 넘으므로 64비트 정수가 필요합니다.
자주 묻는 질문4
Container With Most Water의 시간 복잡도는 무엇인가요?
투 포인터 풀이의 시간 복잡도는 O(n)이고 추가 공간 복잡도는 O(1)입니다. 각 단계에서 포인터 하나를 안쪽으로 한 위치 이동하므로 단계는 최대 n-1번입니다. 모든 쌍을 확인하는 데는 O(n²)이 걸리고, 높이에 따라 선을 정렬하는 데는 O(n log n)이 걸립니다.
더 짧은 선에서 포인터를 이동하는 이유는 무엇인가요?
물의 높이는 더 짧은 선에 의해 제한됩니다. 그 선을 유지하는 다른 어떤 용기도 더 가까운 곳에 짝이 있으므로 폭이 더 좁고, 높이도 더 짧은 선보다 높지 않습니다. 그 어떤 용기도 이미 측정한 용기보다 더 많은 물을 담을 수 없으므로, 답을 놓치지 않고 더 짧은 선을 제외할 수 있습니다.
가장 많은 물을 담는 컨테이너 문제는 그리디 문제인가요?
맞습니다. 각 단계에서는 더 짧은 선을 버리는 지역적인 선택을 하며, 이 선택은 되돌리지 않습니다. 그 단계에서 배제되는 모든 컨테이너는 이미 측정한 컨테이너보다 나을 수 없으므로 이 선택은 안전합니다. 그래서 이 문제는 그리디와 투 포인터 모두로 분류됩니다.
Container With Most Water는 Trapping Rain Water와 어떻게 다른가요?
여기서는 선택한 두 줄만 중요하고 그 사이의 줄은 무시되므로, 답은 하나의 직사각형입니다. Trapping Rain Water에서는 모든 막대가 채워져 있으며, 물은 양쪽에서 더 낮은 쪽의 가장 높은 막대 높이까지 각 막대 위에 고이므로, 답은 모든 위치의 합입니다. 두 문제 모두 O(n) 투 포인터 풀이가 있지만, 포인터 규칙과 합산하는 대상은 다릅니다.
비슷한 문제
같은 아이디어를 쓰는 문제입니다. 두세 개를 풀면 패턴이 몸에 익습니다.
Python
def maxArea(height):
# 여기에 코드를 작성하세요케이스 1
케이스 2
입력
height = [3, 7, 2, 5, 4, 7, 3, 6]
기대값
36