Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions container-with-most-water/unpo88.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
class Solution:
def maxArea(self, height: List[int]) -> int:
left = 0
right = len(height) - 1
max_area = 0

while left < right:
min_height = min(height[left], height[right])
width = right - left
area = min_height * width

max_area = max(max_area, area)

if height[left] < height[right]:
left += 1
else:
right -= 1

return max_area


"""
================================================================================
풀이 과정
================================================================================

[1차 시도] Brute Force로 접근하면?
────────────────────────────────────────────────────────────────────────────────
1. 모든 (i, j) 쌍에 대해 넓이 계산
2. height = [1, 8, 6, 2, 5, 4, 8, 3, 7]
- (0, 1): min(1, 8) × 1 = 1
- (0, 2): min(1, 6) × 2 = 2
- (0, 8): min(1, 7) × 8 = 8
- (1, 8): min(8, 7) × 7 = 49 ← 최대
... 모든 쌍을 확인해야 함

3. 시간복잡도: O(n²) - 시간 초과!
4. 더 효율적인 방법이 필요함 → Two Pointer로 접근

────────────────────────────────────────────────────────────────────────────────
[2차 시도] Two Pointer (양끝에서 좁히기)
────────────────────────────────────────────────────────────────────────────────
5. 핵심 아이디어:
- 넓이 = min(height[left], height[right]) × (right - left)
- 폭(width)은 양끝에서 시작할 때 최대
- 포인터를 안쪽으로 이동하면 폭은 무조건 줄어듦
- 따라서 높이가 커져야 넓이가 커질 가능성이 있음

6. 왜 작은 쪽을 이동하는가?
- 작은 쪽이 높이의 병목 (min이 작은 쪽에 의해 결정됨)
- 큰 쪽을 이동하면? → 높이는 그대로거나 작아짐, 폭은 줄어듦 → 손해만
- 작은 쪽을 이동하면? → 높이가 커질 가능성 있음 → 이득 가능성

7. 동작 예시: height = [1, 8, 6, 2, 5, 4, 8, 3, 7]

Step 1: L=0, R=8
min(1, 7) × 8 = 8
height[L]=1 < height[R]=7 → L 이동

Step 2: L=1, R=8
min(8, 7) × 7 = 49 ← 최대!
height[L]=8 > height[R]=7 → R 이동

Step 3: L=1, R=7
min(8, 3) × 6 = 18
height[L]=8 > height[R]=3 → R 이동

... L과 R이 만날 때까지 반복

8. 시간복잡도: O(n) - 한 번만 순회
9. 공간복잡도: O(1) - 추가 공간 없음
"""
99 changes: 99 additions & 0 deletions design-add-and-search-words-data-structure/unpo88.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
class TrieNode:
def __init__(self):
self.children = {}
self.is_end_word = False


class WordDictionary:

def __init__(self):
self.root = TrieNode()

def addWord(self, word: str) -> None:
node = self.root
for char in word:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.is_end_word = True

def search(self, word: str) -> bool:
return self._dfs(self.root, word, 0)

def _dfs(self, node: TrieNode, word: str, index: int) -> bool:
# 단어 끝에 도달하면 is_end_word 확인
if index == len(word):
return node.is_end_word

char = word[index]

if char == '.':
# '.'는 모든 자식 노드 탐색
for child in node.children.values():
if self._dfs(child, word, index + 1):
return True
return False
else:
# 일반 문자는 해당 자식으로 이동
if char not in node.children:
return False
return self._dfs(node.children[char], word, index + 1)


"""
================================================================================
풀이 과정
================================================================================

[1차 시도] 길이별 그룹화 + 브루트포스 매칭
────────────────────────────────────────────────────────────────────────────────
1. 아이디어: 같은 길이 단어끼리 묶고, 하나씩 패턴 비교
words = { 3: ["bad", "dad", "mad"] }
search(".ad") → 모든 길이 3 단어와 비교

2. 문제점: Time Limit Exceeded!
- 같은 길이 단어가 많으면 O(N × L) 반복
- LeetCode 테스트케이스에서 시간 초과

3. 더 효율적인 방법 필요 → Trie로 접근

────────────────────────────────────────────────────────────────────────────────
[2차 시도] Trie (트라이) 자료구조
────────────────────────────────────────────────────────────────────────────────
4. Trie 구조 (bad, dad, mad 저장 후):

root
├── 'b' → 'a' → 'd' (is_end_word=True)
├── 'd' → 'a' → 'd' (is_end_word=True)
└── 'm' → 'a' → 'd' (is_end_word=True)

5. 동작 예시:

search("bad"):
root → 'b' → 'a' → 'd' → is_end_word=True → True

search(".ad"):
root → '.' (모든 자식 탐색)
→ 'b' → 'a' → 'd' → True (첫 번째에서 찾음!)

search("b.."):
root → 'b' → '.' (모든 자식)
→ 'a' → '.' (모든 자식)
→ 'd' → True

6. 왜 Trie가 더 빠른가?
- 정확한 문자: O(1)로 해당 자식만 탐색
- '.': 해당 위치에서만 분기, 이후는 다시 좁혀짐
- 브루트포스: 모든 단어를 처음부터 끝까지 비교

7. 시간복잡도:
- addWord: O(L) - L은 단어 길이
- search: O(L) ~ O(26^m) - m은 '.' 개수 (보통 적음)

8. 공간복잡도: O(N × L) - 모든 단어의 문자 저장

9. 구현 포인트:
- TrieNode 클래스 분리 → 가독성 향상
- _dfs 재귀로 '.' 처리 → 모든 자식 탐색
- is_end_word로 단어 끝 표시 → 접두사와 구분
"""
64 changes: 64 additions & 0 deletions longest-increasing-subsequence/unpo88.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
n = len(nums)
# dp[i] = i번째 원소로 끝나는 LIS의 길이
dp = [1] * n # 최소 자기 자신 1개

for i in range(1, n):
for j in range(i):
# nums[j] < nums[i]이면, j로 끝나는 LIS에 i를 붙일 수 있음
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j] + 1)

return max(dp)

"""
================================================================================
풀이 과정
================================================================================

[1차 시도] 투포인터?
────────────────────────────────────────────────────────────────────────────────
1. 처음엔 투포인터로 풀 수 있을 것 같았음

2. 근데 생각해보니 Subsequence는 연속된 원소가 아니어도 됨
- Subarray(부분 배열): 연속 필수 → 투포인터 가능
- Subsequence(부분 수열): 연속 안 해도 됨 → 투포인터 불가

3. 예시: [10, 9, 2, 5, 3, 7, 101, 18]
- LIS = [2, 3, 7, 101] (인덱스 2, 4, 5, 6)
- 연속되지 않은 위치에서 선택해야 함 → DP로 접근

────────────────────────────────────────────────────────────────────────────────
[2차 시도] DP
────────────────────────────────────────────────────────────────────────────────
4. 정의: dp[i] = nums[i]로 끝나는 LIS의 길이

5. 점화식:
dp[i] = max(dp[j] + 1) for all j < i where nums[j] < nums[i]

해석: i 이전의 모든 j를 보면서
nums[j] < nums[i]이면 (증가 조건 만족)
dp[j]에 나를 붙일 수 있으므로 dp[j] + 1

6. 동작 예시: nums = [10, 9, 2, 5, 3, 7, 101, 18]

초기: dp = [1, 1, 1, 1, 1, 1, 1, 1]

i=0 (10): 이전 원소 없음 → dp[0] = 1
i=1 (9): 10 > 9 (X) → dp[1] = 1
i=2 (2): 10 > 2 (X), 9 > 2 (X) → dp[2] = 1
i=3 (5): 2 < 5 (O) → dp[3] = dp[2] + 1 = 2
i=4 (3): 2 < 3 (O) → dp[4] = dp[2] + 1 = 2
i=5 (7): 2 < 7 (O) → dp[5] = 2
5 < 7 (O) → dp[5] = max(2, dp[3]+1) = 3
3 < 7 (O) → dp[5] = max(3, dp[4]+1) = 3
i=6 (101): 7 < 101 (O) → dp[6] = dp[5] + 1 = 4
i=7 (18): 7 < 18 (O) → dp[7] = dp[5] + 1 = 4

최종: dp = [1, 1, 1, 2, 2, 3, 4, 4]
답: max(dp) = 4

7. 시간복잡도: O(n²) - 이중 for문
8. 공간복잡도: O(n) - dp 배열
"""
61 changes: 61 additions & 0 deletions spiral-matrix/unpo88.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
result = []

while matrix:
# 1. 맨 위 행을 통째로 추가
result += matrix.pop(0)

# 2. 나머지를 90도 반시계 회전
matrix = list(zip(*matrix))[::-1]

return result


"""
================================================================================
풀이 과정
================================================================================

[접근] 행 추출 + 회전
────────────────────────────────────────────────────────────────────────────────
1. 아이디어:
- 맨 위 행을 떼어내고
- 남은 행렬을 반시계로 90도 회전
- 반복하면 자연스럽게 spiral 순서가 됨

2. 동작 예시: matrix = [[1,2,3],[4,5,6],[7,8,9]]

Step 1: pop [1,2,3] → result = [1,2,3]
[[4,5,6], 회전 [[6,9],
[7,8,9]] ────────→ [5,8],
[4,7]]

Step 2: pop [6,9] → result = [1,2,3,6,9]
[[5,8], 회전 [[8,7],
[4,7]] ────────→ [5,4]]

Step 3: pop [8,7] → result = [1,2,3,6,9,8,7]
[[5,4]] 회전 [[4],
────────→ [5]]

Step 4: pop [4] → result = [1,2,3,6,9,8,7,4]
[[5]] 회전 [[5]]
────────→

Step 5: pop [5] → result = [1,2,3,6,9,8,7,4,5]
[] → 종료

결과: [1,2,3,6,9,8,7,4,5]

3. 회전 원리: list(zip(*matrix))[::-1]
- zip(*matrix): 열 단위로 묶기 (전치)
- [::-1]: 뒤집기 (반시계 회전)

[[4,5,6], zip(*) [(4,7), [::-1] [(6,9),
[7,8,9]] ────────→ (5,8), ────────→ (5,8),
(6,9)] (4,7)]

4. 시간복잡도: O(m × n) - 모든 원소 한 번씩 처리
5. 공간복잡도: O(m × n) - 회전할 때마다 새 배열 생성
"""
75 changes: 75 additions & 0 deletions valid-parentheses/unpo88.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
class Solution:
def isValid(self, s: str) -> bool:
stack = []
pairs = {')': '(', '}': '{', ']': '['}

for c in s:
if c in pairs:
if not stack or stack.pop() != pairs[c]:
return False
else:
stack.append(c)

return not stack


"""
================================================================================
풀이 과정
================================================================================

[1차 시도] Stack으로 직접 비교
────────────────────────────────────────────────────────────────────────────────
1. 괄호 판독기
2. Stack으로 괄호 넣고, 매칭되는것이 나왔을 때, 바로 pop하면서 비교
3. 하나라도 틀린게 있으면 False
4. 아니면 True

stack = []
for c in s:
if c == ')':
if len(stack) == 0 or stack.pop() != '(':
return False
elif c == '}':
if len(stack) == 0 or stack.pop() != '{':
return False
elif c == ']':
if len(stack) == 0 or stack.pop() != '[':
return False
else:
stack.append(c)

return True if len(stack) == 0 else False

5. 문제점: 각 닫는 괄호마다 동일한 패턴이 반복됨
6. 개선 포인트: 딕셔너리로 매핑하면 중복 제거 가능

────────────────────────────────────────────────────────────────────────────────
[2차 시도] 딕셔너리 매핑으로 개선
────────────────────────────────────────────────────────────────────────────────
7. pairs = {')': '(', '}': '{', ']': '['}
- 닫는 괄호 → 여는 괄호 매핑

8. 개선된 로직:
- c가 닫는 괄호면 (c in pairs) → stack에서 pop해서 매칭 확인
- c가 여는 괄호면 → stack에 push

stack = []
pairs = {')': '(', '}': '{', ']': '['}

for c in s:
if c in pairs:
if not stack or stack.pop() != pairs[c]:
return False
else:
stack.append(c)

return not stack

9. 추가 개선:
- len(stack) == 0 → not stack
- True if ... else False → not stack

10. 시간복잡도: O(n) - 문자열 한 번 순회
11. 공간복잡도: O(n) - 최악의 경우 모든 문자가 여는 괄호
"""