From c86008670893a5f6cb05cc67ccd6b2ac6a298b96 Mon Sep 17 00:00:00 2001 From: 1ilsang <1ilsang@naver.com> Date: Sat, 14 Oct 2023 18:45:09 +0900 Subject: [PATCH] docs: Add leetcode 42 --- .../blog/_posts/algorithm/leetcode/hard/42.md | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 apps/blog/_posts/algorithm/leetcode/hard/42.md diff --git a/apps/blog/_posts/algorithm/leetcode/hard/42.md b/apps/blog/_posts/algorithm/leetcode/hard/42.md new file mode 100644 index 00000000..d8e1d4d0 --- /dev/null +++ b/apps/blog/_posts/algorithm/leetcode/hard/42.md @@ -0,0 +1,117 @@ +--- +title: '[LeetCode] 42. Trapping Rain Water' +description: '2차원 좌표의 영역을 빠르게 구해보자' +tags: ['algorithm', 'leetcode', 'hard', 'stack', 'two-pointers'] +coverImage: 'https://github.com/1ilsang/dev/assets/23524849/e0fcd614-46f9-4221-a284-570b9591a1b8' +date: '2023-10-14T08:10:32.304Z' +ogImage: + url: 'https://github.com/1ilsang/dev/assets/23524849/e0fcd614-46f9-4221-a284-570b9591a1b8' +--- + +cover + +> [문제 링크](https://leetcode.com/problems/trapping-rain-water) + +빗물이 고일 수 있는 모든 영역을 구하면 되는 문제다. + +기본적으로 물은 '높은 곳에서 낮은 곳'으로 흐르기 때문에 우리는 높이를 비교하면서 빗물이 고일 수 있는지 판단해야 한다. + +이 문제는 두 가지 스택과 투포인터 두 가지로 풀 수 있다. 두 방법 모두 알아두면 좋기 때문에 두 가지 해석을 모두 하려고 한다. + +## 접근법 1. Stack O(n) T(n) + +스택 접근은 "가로로 면적을 합해가는" 방식이다. + +빗물이 고이기 위해서는 양쪽으로 벽이 있어야 한다. + +스택을 활용해 양 벽을 계산하는 방법은, 높이가 감소할 때는 스택에 푸쉬하고 높이가 이전 탑보다 높아질 때는 팝을 하면서 얼마만큼의 빗물을 저장할 수 있는지 계산하면 된다. + +![example](https://github.com/1ilsang/dev/assets/23524849/fbb24919-41e2-4ff7-9a66-876b590d0c79) + +0번째 높이부터 순회해 보자. + +1. 모든 원소를 순회할 때마다 스택에 푸쉬한다. 왼쪽의 그림과 같이 높이가 감소해 나갈 때에는 특별한 작업 없이 계속 진행한다. +2. 중앙의 그림(`i = 3`)의 상황일 경우 현재 벽이 스택의 Top 값보다 더 높기 때문에 빗물이 고일 수 있다. 현재 높이와 같거나 클 때까지 스택에 쌓여있는 벽들을 pop 하며 "가로로" 누적값을 더한다. 여기서는 높이 1이 최대이므로 스택에 추가된 높이 2(`i = 0`)까지 계산하지 않고 끝난다. +3. 우측의 그림(`i = 4`)에서도 동일하다. 현재 높이보다 큰 높이가 나올 때까지 팝을 하며 계산한다. 2번에서 이미 높이 1일 때의 경우를 계산했으므로 높이 2일 때의 가로 값만 계산하면 된다. + +마지막 노드까지 위의 방식을 계속해서 하면 모든 면적을 구할 수 있다. 코드와 라인별 해석은 제일 아래에 작성해 두었다. + +## 접근법 2. Two Pointers O(n) T(1) + +투포인터 접근은 "세로로 면적을 합해가는" 방식이다. + +빗물이 고이기 위해서는 양쪽으로 벽이 있어야 한다. + +투포인터는 양끝에 포인터를 설정하고 좌우로 움직이며 높이가 더 높은 쪽을 향해 간다. 높이가 높은 쪽을 향해 양 포인터를 옮기면 반대로 낮은 쪽은 빗물이 고이는 곳이기 때문에 세로로 더해나가면 된다. + +![example](https://github.com/1ilsang/dev/assets/23524849/57196d71-ccc4-480a-bef3-b9d34df762e2) + +L, R을 양 끝에 두고 순회하면서 각각 MAX 값을 구한다. + +1. 최초의 상태. 최대값(가로선)을 설정한다. +2. `L < R`이라면 L을 옮기고 아니면 R을 옮긴다. 여기는 L를 옮겼다. `i = 1`의 높이가 L의 최대높이보다 낮으므로 그 차이를 더한다. +3. `L < R`일 때까지 L을 옮긴 모습이다. +4. R을 옮겼다. `i = 8`의 높이가 R의 최대높이보다 낮으므로 그 차이를 더한다. +5. 계속해서 반복하면 결국 LR은 한곳으로 모이고 그 사이의 모든 세로 값이 더해져 빗물의 면적을 구할 수 있다. + +## 최종 코드 + +```js +// Stack +const trap = (height) => { + let res = 0; + let i = 0; + const stack = []; + + while (i < height.length) { + const curHeight = height[i]; + // 현재 높이가 스택의 마지막 높이보다 높다면 + while (stack.length > 0 && height[stack[stack.length - 1]] < curHeight) { + const lastI = stack.pop(); + // 스택이 비었다는 의미는 자신뿐이므로 빗물이 고일 수 없기 때문에 탈출한다. + if (stack.length === 0) break; + const peekI = stack[stack.length - 1]; + // 현재 위치(i)와 스택의 다음 위치(peekI)의 거리를 구한다. + const dist = i - peekI - 1; + // 현재 위치의 높이와 스택의 다음 위치 높이중 낮은 값을 기준으로 스택의 마지막 높이를 뺀다. + // 마지막 높이만큼은 빗물이 고일 수 없기 때문 + const h = Math.min(curHeight, height[peekI]) - height[lastI]; + res += dist * h; + } + stack.push(i++); + } + + return res; +}; +``` + +```js +// Two-pointers +const trap = (height) => { + let res = 0; + let l = 0; + let r = height.length - 1; + let lMax = 0; + let rMax = 0; + + while (l < r) { + const curLeft = height[l]; + const curRight = height[r]; + // 매번 max 값을 갱신한다. 그래야 자신이 줄어든 값인지 비교할 수 있다. + lMax = Math.max(curLeft, lMax); + rMax = Math.max(curRight, rMax); + + // 현재 높이가 최대값 보다 적다면 고일 수 있으므로 추가한다. + if (curLeft < lMax) { + res += lMax - curLeft; + } + if (curRight < rMax) { + res += rMax - curRight; + } + // 양쪽의 포인터를 비교해서 높이가 더 큰 방향으로 이동한다. + curLeft < curRight ? l++ : r--; + } + + return res; +}; +```