From 13f1335c4823a4d7ec8b604117f92bad122d4f48 Mon Sep 17 00:00:00 2001 From: seungriyou Date: Sun, 29 Jun 2025 23:28:37 +0900 Subject: [PATCH 1/6] solve(w14): 338. Counting Bits --- counting-bits/seungriyou.py | 45 +++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 counting-bits/seungriyou.py diff --git a/counting-bits/seungriyou.py b/counting-bits/seungriyou.py new file mode 100644 index 000000000..8daf9b678 --- /dev/null +++ b/counting-bits/seungriyou.py @@ -0,0 +1,45 @@ +# https://leetcode.com/problems/counting-bits/ + +from typing import List + +class Solution: + def countBits(self, n: int) -> List[int]: + """ + [Complexity] + - TC: O(n) + - SC: O(n) + + [Approach] + 다음과 같은 규칙을 찾을 수 있다. + + 0 ~ 1 bin(0) = 0 + bin(1) = 1 + + 2 ~ 3 bin(2) = 10 = 10 + bin(0) + bin(3) = 11 = 10 + bin(1) + => offset = 10(2) = 2 (-> 1이 1개) + + 4 ~ 7 bin(4) = 100 = 100 + bin(0) + bin(5) = 101 = 100 + bin(1) + bin(6) = 110 = 100 + bin(2) + bin(7) = 111 = 100 + bin(3) + => offset = 100(2) = 4 (-> 1이 1개) + + 8 ~ 15 bin(8) = 1000 = 1000 + bin(0) + ... + bin(15) = 1111 = 1000 + bin(7) + => offset = 1000(2) = 8 (-> 1이 1개) + """ + + dp = [0] * (n + 1) + offset = 1 + + for i in range(1, n + 1): + # i가 2의 제곱이면, offset 2배 + if offset * 2 == i: + offset *= 2 + + # 이전에 구한 값 사용 + dp[i] = dp[i - offset] + 1 + + return dp From eef3a806ad4b7f1e75ac19981600080c1f7926df Mon Sep 17 00:00:00 2001 From: seungriyou Date: Sun, 29 Jun 2025 23:29:33 +0900 Subject: [PATCH 2/6] solve(w14): 213. House Robber II --- house-robber-ii/seungriyou.py | 65 +++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 house-robber-ii/seungriyou.py diff --git a/house-robber-ii/seungriyou.py b/house-robber-ii/seungriyou.py new file mode 100644 index 000000000..4b5c9b0e3 --- /dev/null +++ b/house-robber-ii/seungriyou.py @@ -0,0 +1,65 @@ +# https://leetcode.com/problems/house-robber-ii/ + +from typing import List + +class Solution: + def rob_on(self, nums: List[int]) -> int: + """ + [Complexity] + - TC: O(n) + - SC: O(n) + + [Approach] + 집들이 원을 이루고 있고 인접한 두 집을 모두 방문하면 안 되기 때문에, 다음과 같이 두 가지 상황으로 max money를 구한다. + (원형이라는 점을 고려하지 않으면 첫 번째 집과 마지막 집을 모두 방문하게 될 수도 있기 때문) + (1) 첫 번째 집을 제외 + (2) 마지막 집을 제외 + 그리고 두 값 중 큰 값을 반환한다. + + dp[i] = nums[i]까지 확인했을 때의 max money + dp[i] = max(dp[i - 1], dp[i - 2] + num) + """ + n = len(nums) + + # early stop + if n <= 3: + return max(nums) + + dp1 = [0] * (n - 1) # 마지막 집 제외: nums[0] ~ nums[n - 2] + dp2 = [0] * (n - 1) # 첫 번째 집 제외: nums[1] ~ nums[n - 1] + + # initialize + dp1[0], dp2[0] = nums[0], nums[1] + dp1[1], dp2[1] = max(dp1[0], nums[1]), max(dp2[0], nums[2]) + + for i in range(2, n - 1): + dp1[i] = max(dp1[i - 1], dp1[i - 2] + nums[i]) + dp2[i] = max(dp2[i - 1], dp2[i - 2] + nums[i + 1]) + + return max(dp1[-1], dp2[-1]) + + def rob(self, nums: List[int]) -> int: + """ + [Complexity] + - TC: O(n) + - SC: O(1) + + [Approach] + 이전 O(n) space DP 풀이에서 dp[i] 값을 구하기 위해 dp[i - 1] & dp[i - 2] 값만 참고하므로, + O(1) space로 optimize 할 수 있다. + """ + n = len(nums) + + # early stop + if n <= 3: + return max(nums) + + # p2 = dp[i - 2], p1 = dp[i - 1] + f_p2 = f_p1 = 0 # 마지막 집 제외: nums[0] ~ nums[n - 2] + l_p2 = l_p1 = 0 # 첫 번째 집 제외: nums[1] ~ nums[n - 1] + + for i in range(n - 1): + f_p2, f_p1 = f_p1, max(f_p1, f_p2 + nums[i]) + l_p2, l_p1 = l_p1, max(l_p1, l_p2 + nums[i + 1]) + + return max(f_p1, l_p1) From a49a4be3b21518d7eba99d30ef2a0e326dbe9479 Mon Sep 17 00:00:00 2001 From: seungriyou Date: Sun, 29 Jun 2025 23:30:30 +0900 Subject: [PATCH 3/6] solve(w14): 102. Binary Tree Level Order Traversal --- .../seungriyou.py | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 binary-tree-level-order-traversal/seungriyou.py diff --git a/binary-tree-level-order-traversal/seungriyou.py b/binary-tree-level-order-traversal/seungriyou.py new file mode 100644 index 000000000..7848e55ef --- /dev/null +++ b/binary-tree-level-order-traversal/seungriyou.py @@ -0,0 +1,73 @@ +# https://leetcode.com/problems/binary-tree-level-order-traversal/ + +from typing import Optional, List + +# 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 levelOrder_bfs(self, root: Optional[TreeNode]) -> List[List[int]]: + """ + [Complexity] + - TC: O(n) + - SC: O(width) (최악의 경우 O(n)) (결과 res 제외) + + [Approach] + 레벨 별로 BFS로 접근 + """ + if not root: + return [] + + res, level = [], [root] + + while level: + # res에 현재 level 내 node value 추가 + res.append([node.val for node in level]) + + # next level의 node로 level 업데이트 + level = [child for node in level for child in (node.left, node.right) if child] + # next_level = [] + # for node in level: + # if node.left: + # next_level.append(node.left) + # if node.right: + # next_level.append(node.right) + # level = next_level + + return res + + def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]: + """ + [Complexity] + - TC: O(n) + - SC: O(height) (call stack) (결과 res 제외) + + [Approach] + 레벨 별로 DFS로 접근 + """ + if not root: + return [] + + res = [] + + def dfs(node, level): + # base condition (각 level의 첫 시작) + if len(res) == level: + res.append([]) + + # 현재 level에 node.val 추가 + res[level].append(node.val) + + # node의 child에 대해 다음 level로 진행 + if node.left: + dfs(node.left, level + 1) + if node.right: + dfs(node.right, level + 1) + + dfs(root, 0) + + return res From cc007f483a97ddc8ae0f2198153911ad2e76c25e Mon Sep 17 00:00:00 2001 From: seungriyou Date: Sun, 29 Jun 2025 23:31:17 +0900 Subject: [PATCH 4/6] solve(w14): 253. Meeting Rooms II --- meeting-rooms-ii/seungriyou.py | 66 ++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 meeting-rooms-ii/seungriyou.py diff --git a/meeting-rooms-ii/seungriyou.py b/meeting-rooms-ii/seungriyou.py new file mode 100644 index 000000000..3486630f2 --- /dev/null +++ b/meeting-rooms-ii/seungriyou.py @@ -0,0 +1,66 @@ +# https://leetcode.com/problems/meeting-rooms-ii/ + +from typing import List + +class Solution: + def minMeetingRooms_heap(self, intervals: List[List[int]]) -> int: + """ + [Complexity] + - TC: O(nlogn) (sort & heappop/push) + - SC: O(n) (min heap) + + [Approach] + 기본적으로 회의 시작 시각이 빠른 순서로 살펴보아야 한다. + 시작 시각 이전에 어떤 회의실이 빈다면 해당 회의실을 이어서 사용할 수 있고 + 시작 시각 이후까지 기존 회의가 마치지 않는다면 해당 회의실을 사용할 수 없다. + 이를 매번 빠르게 판단하기 위해서는 그리디하게 + "이전까지 회의실을 사용하던 회의 중 가장 빨리 마치는 회의의 종료 시각"과 "현재 보고 있는 회의 시작 시각" + 을 비교하면 된다. + 항상 회의의 종료 시각 중 가장 작은 값을 고르면 되므로, min heap을 이용하여 최솟값을 O(1)에 조회할 수 있도록 할 수 있다. + """ + import heapq + + # 회의 시작 시각 순으로 오름차순 정렬 + intervals.sort() + + # min heap + rooms = [] + + for s, e in intervals: + # "이전까지 회의실을 사용하던 회의 중 가장 빨리 마치는 회의의 종료 시각"과 "현재 보고 있는 회의 시작 시각"이 겹치지 않는 경우, + # rooms에 존재하는 회의실 중 e가 가장 이른 회의실 pop + if rooms and rooms[0] <= s: + heapq.heappop(rooms) + + # 현재 회의 push + heapq.heappush(rooms, e) + + return len(rooms) + + def minMeetingRooms(self, intervals: List[List[int]]) -> int: + """ + [Complexity] + - TC: O(nlogn) + - SC: O(n) + + [Approach] + start와 end를 나누어 정렬한다면, 각각의 원소를 가리키는 two-pointer를 이용해 + 회의실을 재사용할 수 있는 경우와 새로운 회의실이 필요한 경우를 판단할 수 있다. + starts를 순회하면서 e라는 pointer로 ends의 첫번째 원소부터 비교하면 다음과 같이 케이스를 나눌 수 있다. + - non-overlap(ends[e] <= s): 회의실 재사용 가능 (e++를 통해 다음 회의 살펴보기) + - overlap(ends[e] > s): 새로운 회의실 필요 + """ + starts = sorted(s for s, _ in intervals) + ends = sorted(e for _, e in intervals) + + res = e = 0 + + for s in starts: + # non overlap -> 회의실 재사용 가능 (다음 회의 살펴보기) + if ends[e] <= s: + e += 1 + # overlap -> 새로운 회의실 필요 + else: + res += 1 + + return res From 81cdecdd4f0c7faecff6cb5afd95b9e53b207565 Mon Sep 17 00:00:00 2001 From: seungriyou Date: Sat, 5 Jul 2025 10:47:22 +0900 Subject: [PATCH 5/6] solve(w14): 212. Word Search II --- word-search-ii/seungriyou.py | 180 +++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 word-search-ii/seungriyou.py diff --git a/word-search-ii/seungriyou.py b/word-search-ii/seungriyou.py new file mode 100644 index 000000000..e3330c462 --- /dev/null +++ b/word-search-ii/seungriyou.py @@ -0,0 +1,180 @@ +# https://leetcode.com/problems/word-search-ii/ + +from typing import List + +"""#### [Approach 1] #### +[Complexity] + (k = len(words), l = max(len(word) for word in words)) + - TC: O(m * n * 4 * 3^(l-1)) + - trie 구성: O(k * l) = O(total # of letters in words) + - backtracking: 이론적으로 O(m * n * (4 * 3^(l-1))) + - SC: O(k * l) (고정) + - trie 저장: O(k * l) = O(total # of letters in words) + (word도 저장하므로 2배) + - call stack: O(l) + +[Approach] + 주어진 words에 대해 기본 trie를 구성하고, + board 내의 문자들을 순회하며 해당 문자로 시작하는 단어가 trie에 있는지 backtracking으로 확인한다. + + 이때, words = ["oa", "oaa"]인 경우에 res = ["oa", "oa", "oaa"]가 나오지 않도록 하기 위해서 + word를 찾은 경우 node.word = None으로 중복을 제거하는 로직이 필요하다. (혹은 res를 set()으로 기록) +""" + +from collections import defaultdict + +class TrieNode: + def __init__(self): + self.children = defaultdict(TrieNode) + self.word = None + +class Trie: + def __init__(self): + self.root = TrieNode() + + def insert(self, word): + node = self.root + for w in word: + node = node.children[w] + node.word = word + +class Solution: + def findWords(self, board: List[List[str]], words: List[str]) -> List[str]: + m, n = len(board), len(board[0]) + res = [] + + # 1. trie 구성 + trie = Trie() + for word in words: + trie.insert(word) + + # 2. board 내 문자를 순회하며 trie에서 찾기 (w. backtracking) + def backtrack(r, c, node): + # base condition + if ( + not (0 <= r < m and 0 <= c < n) # (1) 범위를 벗어났거나 + or board[r][c] == "#" # (2) 이미 방문했거나 + or board[r][c] not in node.children # (3) 현재 node의 children에 문자가 없다면 + ): + return + + # visit 처리 + curr_w, board[r][c] = board[r][c], "#" + + # trie 타고 내려가기 + node = node.children[curr_w] + + # 내려간 node가 word 라면 결과에 추가 + if node.word: + res.append(node.word) + node.word = None # -- 중복 제거 + + # recur + backtrack(r - 1, c, node) + backtrack(r + 1, c, node) + backtrack(r, c - 1, node) + backtrack(r, c + 1, node) + + # visit 처리 취소 + board[r][c] = curr_w + + for i in range(m): + for j in range(n): + backtrack(i, j, trie.root) + + return res + + +"""#### [Approach 2] #### +[Complexity] + (k = len(words), l = max(len(word) for word in words)) + - TC: O(m * n * 4 * 3^(l-1)) + - trie 구성: O(k * l) = O(total # of letters in words) + - backtracking: 이론적으로 O(m * n * (4 * 3^(l-1)))이나, + 탐색 공간이 remove로 줄어들기 때문에 (4 * 3^(l-1)) 실제로는 경로가 빠르게 줄어듦 + - remove: O(k * l) (k개의 단어에 대해서) + - SC: O(k * l) (점점 줄어듦) + - trie 저장: O(k * l) = O(total # of letters in words) + (word도 저장하므로 2배) (실행 중에 줄어듦) + - call stack: O(l) + +[Approach] + approach 1과 비교했을 때, trie.remove(node)를 통해 이미 찾은 단어의 경로를 trie에서 제거한다는 점이 다르다. + 따라서 이미 찾은 word의 경로는 더 이상 탐색하지 않으며, trie의 크기가 점차 줄어들어 탐색 공간도 줄어들게 된다. + (TrieNode에 parent, char 추가) +""" + +class TrieNode: + def __init__(self, parent=None, char=None): + self.children = {} + self.word = None + self.parent = parent # for optimization + self.char = char # for optimization + +class Trie: + def __init__(self): + self.root = TrieNode() + + def insert(self, word): + node = self.root + for w in word: + if w not in node.children: # node.children에 w가 없을 때만 새로 만들기 + node.children[w] = TrieNode(parent=node, char=w) + node = node.children[w] + node.word = word + + def remove(self, node): # for optimization (이미 찾은 단어는 trie에서 없애기) + # 중복 제거 + node.word = None + + # trie에서 현재 word를 제거 (단, child.children이 없어야 제거 가능) + child, parent = node, node.parent + while parent and len(child.children) == 0: + del parent.children[child.char] + child, parent = parent, parent.parent + +class Solution: + def findWords(self, board: List[List[str]], words: List[str]) -> List[str]: + m, n = len(board), len(board[0]) + res = [] + + # 1. trie 구성 + trie = Trie() + for word in words: + trie.insert(word) + + # 2. board 내 문자를 순회하며 trie에서 찾기 (w. backtracking) + def backtrack(r, c, node): + # base condition + if ( + not (0 <= r < m and 0 <= c < n) # (1) 범위를 벗어났거나 + or board[r][c] == "#" # (2) 이미 방문했거나 + or board[r][c] not in node.children # (3) 현재 node의 children에 문자가 없다면 + ): + return + + # visit 처리 + curr_w, board[r][c] = board[r][c], "#" + + # trie 타고 내려가기 + node = node.children[curr_w] + + # 내려간 node가 word 라면 결과에 추가 + if node.word: + res.append(node.word) + trie.remove(node) + + # recur + backtrack(r - 1, c, node) + backtrack(r + 1, c, node) + backtrack(r, c - 1, node) + backtrack(r, c + 1, node) + + # visit 처리 취소 + board[r][c] = curr_w + + for i in range(m): + for j in range(n): + backtrack(i, j, trie.root) + + return res From 3ec3b9eabf625635469096aee8ef141facc161b4 Mon Sep 17 00:00:00 2001 From: seungriyou Date: Sat, 5 Jul 2025 21:06:50 +0900 Subject: [PATCH 6/6] solve(w14): 338. Counting Bits (update) --- counting-bits/seungriyou.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/counting-bits/seungriyou.py b/counting-bits/seungriyou.py index 8daf9b678..8db0311e4 100644 --- a/counting-bits/seungriyou.py +++ b/counting-bits/seungriyou.py @@ -3,7 +3,7 @@ from typing import List class Solution: - def countBits(self, n: int) -> List[int]: + def countBits1(self, n: int) -> List[int]: """ [Complexity] - TC: O(n) @@ -43,3 +43,17 @@ def countBits(self, n: int) -> List[int]: dp[i] = dp[i - offset] + 1 return dp + + def countBits(self, n: int) -> List[int]: + """ + [Complexity] + - TC: O(n) + - SC: O(n) + """ + + dp = [0] * (n + 1) + for i in range(1, n + 1): + # dp[i] = dp[i // 2] + (i % 2) + dp[i] = dp[i >> 1] + (i & 1) + + return dp