LeetCode/NeetCode

[Two Pointers] 42. Trapping Rain Water ★★★

hyunkookim 2024. 11. 28. 14:43

42. Trapping Rain Water

 

https://youtu.be/ZI2z5pq0TqA?si=vMNp4_SRqkju2a6b

 

 

O(n) 공간 사용

from typing import List

class Solution:
    def trap(self, height: List[int]) -> int:
        # 높이 배열의 길이를 가져옵니다.
        n = len(height)
        # 배열이 비어있는 경우 0을 반환 (예외 처리)
        if not n:
            return 0

        # maxLeft: 각 위치에서 왼쪽으로 가장 높은 기둥의 높이를 저장할 리스트
        # maxRight: 각 위치에서 오른쪽으로 가장 높은 기둥의 높이를 저장할 리스트
        # minLR: 각 위치에서 maxLeft와 maxRight 중 작은 값을 저장할 리스트
        maxLeft, maxRight, minLR = [0] * n, [0] * n, [0] * n

        # maxLeft의 첫 번째 값은 첫 번째 기둥의 높이로 초기화
        maxLeft[0] = height[0]
        # maxRight의 마지막 값은 마지막 기둥의 높이로 초기화
        maxRight[n-1] = height[n-1]

        # 왼쪽에서 오른쪽으로 순회하며 maxLeft 값을 계산
        for i in range(1, n):
            # 현재 위치의 maxLeft는 이전 위치의 maxLeft와 바로 이전 기둥의 높이 중 큰 값
            maxLeft[i] = max(maxLeft[i-1], height[i-1])

        # 오른쪽에서 왼쪽으로 순회하며 maxRight 값을 계산
        for i in range(n-2, -1, -1):
            # 현재 위치의 maxRight는 다음 위치의 maxRight와 바로 다음 기둥의 높이 중 큰 값
            maxRight[i] = max(maxRight[i+1], height[i+1])

        # 최종적으로 담길 수 있는 빗물의 양을 계산하기 위한 변수
        res = 0
        # 각 위치에서 빗물의 양을 계산
        for i in range(n):
            # minLR: maxLeft와 maxRight 중 작은 값을 저장
            minLR[i] = min(maxLeft[i], maxRight[i])
            # 빗물의 양은 minLR에서 현재 기둥 높이를 뺀 값
            sub = minLR[i] - height[i]
            # 빗물이 양수일 때만 결과에 더해줌 (음수는 빗물이 담기지 않는 경우)
            res += sub if sub >= 0 else 0 

        # 최종적으로 계산된 빗물의 양 반환
        return res

 

O(1) 공간 사용

from typing import List

class Solution:
    def trap(self, height: List[int]) -> int:
        # 투 포인터를 사용하여 빗물의 양을 계산
        l, r = 0, len(height) - 1  # 왼쪽 포인터 l과 오른쪽 포인터 r 초기화
        leftMax, rightMax = height[l], height[r]  # 왼쪽과 오른쪽에서 가장 높은 기둥의 초기값 설정
        res = 0  # 최종적으로 담길 빗물의 양을 저장할 변수

        # 왼쪽 포인터가 오른쪽 포인터를 넘기 전까지 반복
        while l < r:
            # 왼쪽 최대값이 오른쪽 최대값보다 작은 경우
            if leftMax < rightMax:
                l += 1  # 왼쪽 포인터 이동
                leftMax = max(leftMax, height[l])  # 새로운 leftMax 갱신
                res += leftMax - height[l]  # 빗물의 양은 leftMax와 현재 높이의 차이
            else:
                # 오른쪽 최대값이 왼쪽 최대값보다 작거나 같은 경우
                r -= 1  # 오른쪽 포인터 이동
                rightMax = max(rightMax, height[r])  # 새로운 rightMax 갱신
                res += rightMax - height[r]  # 빗물의 양은 rightMax와 현재 높이의 차이

        # 최종적으로 계산된 빗물의 양 반환
        return res

 

주석 설명

  1. 투 포인터 초기화:
    • l은 왼쪽 끝에서 시작하고, r은 오른쪽 끝에서 시작합니다.
    • leftMax와 rightMax는 각각 왼쪽과 오른쪽에서 현재까지의 최대 기둥 높이를 추적합니다.
  2. 반복문 로직:
    • leftMax < rightMax인 경우, 더 낮은 쪽(leftMax)에서 빗물을 계산하며 왼쪽 포인터를 이동합니다.
    • 반대로, rightMax가 작거나 같은 경우 오른쪽에서 빗물을 계산하며 오른쪽 포인터를 이동합니다.
    • 이 방식은 항상 낮은 쪽에서 빗물을 계산하므로 각 단계에서 불필요한 비교를 줄이고 효율적으로 계산할 수 있습니다.
  3. 빗물 계산:
    • 현재 높이(height[l] 또는 height[r])가 최대값(leftMax 또는 rightMax)보다 작다면, 최대값과 현재 높이의 차이가 해당 위치에서 담길 수 있는 빗물의 양입니다.
  4. 반복 종료 조건:
    • 왼쪽 포인터가 오른쪽 포인터를 넘어가면 모든 위치에서의 빗물 계산이 완료됩니다.

최종 코드

 

DP 로직

class Solution:
    def trap(self, height: List[int]) -> int:
        # 현재 위치 i에서 고인 물의 양 =
        # min(왼쪽에서의 최대 높이, 오른쪽에서의 최대 높이) - 현재 위치의 높이

        lenH = len(height)
        maxLeftH = [0] * lenH   # 각 위치에서 왼쪽 방향으로의 최대 높이 저장
        maxRightH = [0] * lenH  # 각 위치에서 오른쪽 방향으로의 최대 높이 저장

        # 왼쪽 최대 높이 배열 채우기
        for l in range(1, lenH):
            # 현재 위치 l의 왼쪽까지 중에서 가장 높은 높이를 저장
            maxLeftH[l] = max(maxLeftH[l - 1], height[l - 1])

        # 오른쪽 최대 높이 배열 채우기
        for r in range(lenH - 2, -1, -1):  # 오른쪽에서 왼쪽으로 이동
            # 현재 위치 r의 오른쪽까지 중에서 가장 높은 높이를 저장
            maxRightH[r] = max(maxRightH[r + 1], height[r + 1])

        area = 0  # 고인 물의 총량

        # 각 위치에서 고인 물의 양 계산
        for i in range(lenH):
            # 현재 위치에 고일 수 있는 물의 높이 계산
            curDrop = min(maxLeftH[i], maxRightH[i]) - height[i]
            # 고인 물이 0보다 크면 더해줌 (음수인 경우는 고이지 않음)
            area += curDrop if curDrop > 0 else 0

        return area  # 총 고인 물의 양 반환

 

✅ 요약 정리

  • 왼쪽/오른쪽 최대 높이 배열을 먼저 채워서,
  • 각 위치에서 고일 수 있는 최대 물 높이를 계산함.
  • 시간 복잡도: O(n)
  • 공간 복잡도: O(n) (두 개의 보조 배열 사용)

 

좋아요! 위에서 작성하신 DP 방식과 **같은 로직(= min(왼쪽 최대, 오른쪽 최대) - 현재 높이)**을 사용하지만,
보조 배열 없이 공간을 줄이는 투 포인터(two-pointer) 버전을 아래에 설명해드릴게요.


✅ 투 포인터 버전 (O(n) 시간, O(1) 공간)

class Solution:
    def trap(self, height: List[int]) -> int:
        if not height:
            return 0

        l, r = 0, len(height) - 1       # 왼쪽, 오른쪽 포인터
        left_max, right_max = 0, 0      # 각 방향에서 현재까지의 최대 높이
        area = 0                        # 고인 물의 총량

        while l < r:
            if height[l] < height[r]:
                # 왼쪽이 더 낮으면, 왼쪽 기준으로 물 높이 계산
                if height[l] >= left_max:
                    left_max = height[l]    # 왼쪽 최대 높이 갱신
                else:
                    area += left_max - height[l]  # 물이 고임
                l += 1
            else:
                # 오른쪽이 더 낮으면, 오른쪽 기준으로 물 높이 계산
                if height[r] >= right_max:
                    right_max = height[r]   # 오른쪽 최대 높이 갱신
                else:
                    area += right_max - height[r]  # 물이 고임
                r -= 1

        return area

 

투 포인터 방식에서 left_max나 right_max를 갱신하는 경우에는 왜 area(고인 물 양)을 계산하지 않는지 설명해드릴게요.


✅ 핵심 개념:

고인 물 = max 높이 - 현재 높이
이때, 고인 물이 생기려면 "현재 높이 < max 높이" 여야 함.

 

🔍 왜 left_max 갱신할 때는 area 계산을 안 할까?

height[l] >= left_max라는 건:

  • 현재 위치가 지금까지 본 가장 높은 막대거나,
  • 최소한 지금까지 본 막대들보다 더 높기 때문에
    👉 물이 고일 수 없는 위치라는 뜻이에요.

✅ 예를 들어:

  • left_max = 2
  • height[l] = 3
    → 이제 left_max를 3으로 갱신해야 하지만, → left_max - height[l] = 3 - 3 = 0 → 물이 고일 수 없음
    그래서 area 계산 생략

✅ 로직 요약 (→ DP 배열 없이도 같은 개념 사용)

  • left_max, right_max는 각각 현재까지의 왼쪽/오른쪽 최대 높이를 저장함.
  • height[l] < height[r]인 경우:
    • 왼쪽 막대 기준으로 고일 수 있는 물을 계산.
  • height[r] <= height[l]인 경우:
    • 오른쪽 막대 기준으로 고일 수 있는 물을 계산.
  • 이 과정을 포인터가 만날 때까지 반복.

그래서, 다시 말하면 이렇게 해도 됨.

결국. 갱신할때는 area 값이 0 이 되서, 실행안한것처럼 됨

class Solution:
    def trap(self, height: List[int]) -> int:
        # 현재 위치 i 에서의 고인 물 양은
        # = min(좌측 최대 높이, 우측 최대 높이) - 현재 높이

        lenH = len(height)
        l, r = 0, lenH - 1      # 왼쪽 포인터, 오른쪽 포인터
        area = 0                # 총 고인 물의 양
        maxL, maxR = 0, 0       # 현재까지의 왼쪽/오른쪽 최대 높이

        while l < r:
            if height[l] < height[r]:
                # 왼쪽 막대가 더 낮으면 왼쪽 기준으로 판단
                maxL = max(maxL, height[l])  # 왼쪽 최대 높이 갱신
                # 현재 위치에 고일 수 있는 물 = maxL - 현재 높이
                area += max(maxL - height[l], 0)
                l += 1
            else:
                # 오른쪽 막대가 더 낮거나 같으면 오른쪽 기준으로 판단
                maxR = max(maxR, height[r])  # 오른쪽 최대 높이 갱신
                # 현재 위치에 고일 수 있는 물 = maxR - 현재 높이
                area += max(maxR - height[r], 0)
                r -= 1

        return area

✅ 로직 핵심 요약

  • 작은 쪽을 기준으로 고인 물을 계산해야 안전함.
  • 왜냐하면 min(maxL, maxR)이 작은 쪽으로 결정되기 때문!
  • 따라서 작은 쪽의 max 값을 갱신하면서 고인 물을 누적 계산.

✅ 시간 & 공간 복잡도

  • 시간 복잡도: O(n) (포인터 한 번씩만 이동)
  • 공간 복잡도: O(1) (추가 배열 사용 안 함)

장점 비교

방식 시간 복잡도 공간 복잡도 특징
DP 배열 방식 O(n) O(n) 코드가 직관적이고 이해 쉬움
투 포인터 O(n) O(1) 공간 효율적, 최적화된 방식