diff --git a/coin-change/seungriyou.py b/coin-change/seungriyou.py new file mode 100644 index 000000000..7b2ae6fcb --- /dev/null +++ b/coin-change/seungriyou.py @@ -0,0 +1,113 @@ +# https://leetcode.com/problems/coin-change/ + +from functools import cache +from typing import List +import math + +class Solution: + def coinChange_n(self, coins: List[int], amount: int) -> int: + """ + [Complexity] + - TC: O(n * amount) + - SC: O(n * amount) + + [Approach] + 각 coin을 무한히 많이 사용할 수 있으므로 unbounded knapsack problem 이다. + 이때, 가치를 최대화하는 것 == 동전의 개수를 최소화 하는 것이다. + 따라서 2D DP로 풀 수 있다. + """ + + INF = amount + 1 + n = len(coins) + + # dp[i][j] = i번째 coin까지 사용했을 때, j 만큼의 amount를 만들 수 있는 coin의 최소 개수 + dp = [[INF] * (amount + 1) for _ in range(n + 1)] + dp[0][0] = 0 + + for i in range(1, n + 1): # -- coin + dp[i][0] = 0 + for j in range(1, amount + 1): # -- amount + if j < coins[i - 1]: + dp[i][j] = dp[i - 1][j] # 현재 coin을 넣을 수 없음 + else: + dp[i][j] = min(dp[i - 1][j], dp[i][j - coins[i - 1]] + 1) # min(현재 coin을 넣지 X 경우, 현재 coin을 넣는 경우) + + return dp[n][amount] if dp[n][amount] != INF else -1 + + def coinChange_1(self, coins: List[int], amount: int) -> int: + """ + [Complexity] + - TC: O(n * amount) + - SC: O(amount) + + [Approach] + 매 단계에서 다음의 두 값만 확인하므로, 2D DP를 rolling array 방식으로 1D DP로 space optimize 할 수 있다. + - dp[i - 1][j] + - dp[i][j - coins[i - 1]] + """ + + INF = amount + 1 + + dp = [INF] * (amount + 1) + dp[0] = 0 + + for coin in coins: + for amnt in range(coin, amount + 1): + dp[amnt] = min(dp[amnt], dp[amnt - coin] + 1) # min(현재 coin을 넣지 X 경우, 현재 coin을 넣는 경우) + + return dp[amount] if dp[amount] != INF else -1 + + def coinChange_b(self, coins: List[int], amount: int) -> int: + """ + [Complexity] + - TC: O(n * amount) (금액 1 ~ amount 각각에 대해 len(coins) 만큼 확인) + - SC: O(amount) (seen & q) + + [Approach] + BFS로 최단거리를 찾듯이 접근해도 된다. 이때의 최단거리란 최소 개수를 의미한다. + """ + from collections import deque + + q = deque([(0, 0)]) # (총 금액, coin 개수) + seen = {0} # 이미 확인한 총 금액 + + while q: + amnt, n = q.popleft() + + # base condition + if amnt == amount: + return n + + # iter + for coin in coins: + if (new_amnt := amnt + coin) <= amount and new_amnt not in seen: + q.append((new_amnt, n + 1)) + seen.add(new_amnt) + + return -1 + + def coinChange(self, coins: List[int], amount: int) -> int: + """ + [Complexity] + - TC: O(n * amount) (금액 0 ~ amount, 각각 len(coins) 만큼 확인) + - SC: O(amount) (@cache 저장 공간, call stack) + + [Approach] + bottom-up이었던 DP 뿐만 아니라, 더 직관적인 top-down 접근도 가능하다. + 이때 @cache를 사용하면 memoization을 통해 더 최적화할 수 있다. + """ + + @cache + def dp(amnt): + # base condition + if amnt == 0: + return 0 + if amnt < 0: + return math.inf + + # recur + return min(dp(amnt - coin) + 1 for coin in coins) + + res = dp(amount) + + return res if res != math.inf else -1 diff --git a/find-minimum-in-rotated-sorted-array/seungriyou.py b/find-minimum-in-rotated-sorted-array/seungriyou.py new file mode 100644 index 000000000..ddb79432d --- /dev/null +++ b/find-minimum-in-rotated-sorted-array/seungriyou.py @@ -0,0 +1,28 @@ +# https://leetcode.com/problems/find-minimum-in-rotated-sorted-array/ + +from typing import List + +class Solution: + def findMin(self, nums: List[int]) -> int: + """ + [Complexity] + - TC: O(logn) + - SC: O(1) + + [Approach] + 기본적으로 rotated sorted array에서 O(logn) time에 minimum element를 찾아야하므로 + binary search를 사용한다. 규칙을 찾아보면 다음과 같다. + - nums[mid] > nums[hi]: min element가 오른쪽 영역인 (mid, hi]에 있음 + - nums[mid] < nums[hi]: min element가 왼쪽 영역인 [lo, mid]에 있음 + """ + + lo, hi = 0, len(nums) - 1 + + while lo < hi: + mid = lo + (hi - lo) // 2 + if nums[mid] > nums[hi]: + lo = mid + 1 # (mid, hi]에 min element 존재 + else: + hi = mid # [lo, mid]에 min element 존재 + + return nums[lo] diff --git a/maximum-depth-of-binary-tree/seungriyou.py b/maximum-depth-of-binary-tree/seungriyou.py new file mode 100644 index 000000000..a9dc8cffa --- /dev/null +++ b/maximum-depth-of-binary-tree/seungriyou.py @@ -0,0 +1,57 @@ +# https://leetcode.com/problems/maximum-depth-of-binary-tree/ + +from typing import Optional + +# Definition for a binary tree node. +class TreeNode: + def __init__(self, val=0, left=None, right=None): + self.val = val + self.left = left + self.right = right + +class Solution: + def maxDepth_recur(self, root: Optional[TreeNode]) -> int: + """ + [Complexity] + - TC: O(n) (모든 node를 한 번씩 방문) + - SC: O(h) (call stack) (h = logn ~ n) + + [Approach] recursive + 재귀적으로 max(left subtree의 depth, right subtree의 depth) + 1 을 구하면 된다. + base condition(= 현재 노드가 None인 경우)에서는 0을 반환한다. + """ + + def get_max_depth(node): + # base condition + if not node: + return 0 + + # recur + return max(get_max_depth(node.right), get_max_depth(node.left)) + 1 + + return get_max_depth(root) + + def maxDepth(self, root: Optional[TreeNode]) -> int: + """ + [Complexity] + - TC: O(n) (모든 node를 한 번씩 방문) + - SC: O(w) (트리의 너비) (w = 1 ~ n / 2) + + [Approach] iterative + BFS 처럼 이진 트리를 레벨 별로 순회하며 depth를 1씩 증가시킬 수 있다. + """ + depth = 0 + curr_level = [root] if root else [] + + while curr_level: + next_level = [] + for node in curr_level: + if node.left: + next_level.append(node.left) + if node.right: + next_level.append(node.right) + + curr_level = next_level + depth += 1 + + return depth diff --git a/merge-two-sorted-lists/seungriyou.py b/merge-two-sorted-lists/seungriyou.py new file mode 100644 index 000000000..668b1232c --- /dev/null +++ b/merge-two-sorted-lists/seungriyou.py @@ -0,0 +1,38 @@ +# https://leetcode.com/problems/merge-two-sorted-lists/ + +from typing import Optional + +# Definition for singly-linked list. +class ListNode: + def __init__(self, val=0, next=None): + self.val = val + self.next = next + +class Solution: + def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]: + """ + [Complexity] + - TC: O(n) + - SC: O(1) + + [Approach] + 이미 list1, list2가 non-decreasing order로 정렬되어 있으므로, 각 linked list를 하나씩 비교하며 더 작은 값을 가진 노드부터 모으면 된다. + 그리고 list1, list2 중에 하나라도 다 본 경우, 남은 linked list를 끝에 붙이면 된다. + """ + res = curr = ListNode() + + while list1 and list2: + if list1.val <= list2.val: + curr.next = list1 + list1 = list1.next + else: + curr.next = list2 + list2 = list2.next + curr = curr.next + + if list1: + curr.next = list1 + if list2: + curr.next = list2 + + return res.next diff --git a/word-search/seungriyou.py b/word-search/seungriyou.py new file mode 100644 index 000000000..4ef166d4a --- /dev/null +++ b/word-search/seungriyou.py @@ -0,0 +1,55 @@ +# https://leetcode.com/problems/word-search/ + +from typing import List + +class Solution: + def exist(self, board: List[List[str]], word: str) -> bool: + """ + [Complexity] + - TC: O(m * n * 4^L) (L = word의 길이) + - 한 경로에서 최대 L번 재귀 호출 + - 각 cell 당 3~4 방향 가능 + - SC: O(m * n + L) (visited + call stack) + - visited 배열 대신 board에 inplace로 표시하면 O(L)으로 최적화 가능 + + [Approach] + 주어진 word를 순차적으로 확인하기 위해 backtracking으로 접근할 수 있다. (가지치기 가능) + 맨 처음에 word를 구성하는 문자가 board에 모두 존재하는지 확인한다면 run time을 줄일 수 있다. + """ + # early stop (word를 구성하는 문자가 board에 모두 존재하는지 확인) + if set(word) - set(l for row in board for l in row): + return False + + m, n = len(board), len(board[0]) + visited = [[False] * n for _ in range(m)] + + def backtrack(r, c, idx): + # base condition + if idx == len(word): + return True + if ( + not (0 <= r < m and 0 <= c < n) # (1) 범위를 벗어나거나 + or visited[r][c] # (2) 이미 방문했거나 + or board[r][c] != word[idx] # (3) 주어진 word와 다른 경우 + ): + return False + + # backtrack + visited[r][c] = True + if ( + backtrack(r - 1, c, idx + 1) + or backtrack(r + 1, c, idx + 1) + or backtrack(r, c - 1, idx + 1) + or backtrack(r, c + 1, idx + 1) + ): + return True + visited[r][c] = False + + return False + + for i in range(m): + for j in range(n): + if backtrack(i, j, 0): + return True + + return False