-
-
Notifications
You must be signed in to change notification settings - Fork 361
[okyungjin] WEEK 04 Solutions (최적화) #2759
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c09ad1b
e045b60
d6459cc
3e0f2b8
5723acb
f0245ab
071f547
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,8 +5,6 @@ | |
| # coins로 amount를 만들 수 없는 경우 -1을 반환 | ||
| # 동전의 종류는 무한대라고 가정하고 문제를 풀 것 | ||
|
|
||
| # 처음에는 greedy로 접근하려고 했는데, 금액이 큰 동전을 먼저 선택하는게 최소 동전의 수가 아니라서 dp로 풀이했다. | ||
|
|
||
| # [복잡도] | ||
| # C: 코인의 개수, # A: amount | ||
| # 시간 복잡도: O(A * C) | ||
|
|
@@ -19,21 +17,19 @@ def coinChange(self, coins: List[int], amount: int) -> int: | |
| # min_coins[i]: i원을 만드는데 필요한 최소 동전의 개수 | ||
| min_coins = [NOT_POSSIBLE] * (amount + 1) | ||
| min_coins[0] = 0 | ||
|
|
||
| # 최적화를 위해 정렬 | ||
| coins.sort() | ||
|
|
||
| # 1부터 목표 금액(amount)까지 상향식으로 최소 동전 개수를 계산 | ||
| for cur_amount in range(1, amount + 1): | ||
| # coin: 선택한 동전의 금액 | ||
| # 모든 동전 종류를 확인하여 현재 금액을 만들 수 있는 조합 탐색 | ||
| for coin in coins: | ||
| # coins가 정렬되어 있으므로, cur_amount 보다 동전의 금액이 더 크면 이후는 확인 불필요 | ||
| if coin > cur_amount: | ||
| break | ||
|
|
||
| # 둘 중 더 작은 값으로 업데이트 | ||
| # 1. 기존에 구한 개수 | ||
| # 2. 현재 동전을 1개 추가해서 만드는 개수 | ||
| min_coins[cur_amount] = min(min_coins[cur_amount], min_coins[cur_amount - coin] + 1) | ||
| # 동전 금액이 현재 금액보다 작거나 같을 때만 유효한 조합으로 간주 | ||
| if coin <= cur_amount: | ||
| # '남은 금액을 만드는 최소 개수' + '현재 동전 1개'를 합산 | ||
|
Comment on lines
+23
to
+27
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 원래는 coins를 정렬한 뒤 if coin > cur_amount: break 조기 종료 조건을 사용했었는데요. 코드를 더 간결하게 다듬기 위해 정렬을 없애고 부등호를 뒤집는 방향으로 수정했습니다. coins 배열 길이가 최대 12라 break 유무에 따른 성능 차이가 크지 않을 것 같아 로직을 간소화해 보았습니다!
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 사담이지만 그 아래 라인에 있는 min 함수를 제거하니 성능이 훨씬 좋아지더라구요 min_coins[cur_amount] = min(min_coins[cur_amount], min_coins[cur_amount - coin] + 1)if문 형태로 필요할 때만 할당하도록 하니 런타임 백분위가 30% 올라갔던 것 같습니다.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 맞아요, max나 min으로 하는게 코드상 보기 좋고 쓰기도 편한게 맞는데 |
||
| new_val = min_coins[cur_amount - coin] + 1 | ||
|
|
||
| # 더 적은 개수의 동전 조합을 찾았다면 해당 금액의 최솟값을 갱신 | ||
| if new_val < min_coins[cur_amount]: | ||
| min_coins[cur_amount] = new_val | ||
|
|
||
| # 초기화된 값 그대로이면 불가능한 조합이므로 -1 반환 | ||
| if min_coins[amount] == NOT_POSSIBLE: | ||
|
|
||
|
okyungjin marked this conversation as resolved.
|
|
okyungjin marked this conversation as resolved.
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,28 +1,81 @@ | ||
| # 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 | ||
|
|
||
| # 시간 복잡도: O(N), 모든 노드를 한번 씩 방문 | ||
| # 공간 복잡도: O(N), 최악의 경우 모든 노드가 큐에 담길 수 있음 | ||
| ''' | ||
| 스택을 활용한 DFS | ||
|
|
||
| 시간 복잡도: O(N) | ||
| 공간 복잡도: O(H), H: 트리의 높이 | ||
| ''' | ||
| class Solution: | ||
| def maxDepth(self, root: Optional[TreeNode]) -> int: | ||
| # 트리가 비어있으면 길이는 0 | ||
| if root is None: | ||
| return 0 | ||
|
|
||
| max_depth = 1 | ||
|
|
||
| # 스택에 (현재 노드, 노드의 깊이) 튜플을 저장 | ||
| stack = [(root, 1)] | ||
|
|
||
| while stack: | ||
| node, cur_depth = stack.pop() | ||
|
|
||
| # 현재 노드의 깊이와 저장된 최댓값을 비교하여 갱신 | ||
| max_depth = max(cur_depth, max_depth) | ||
|
|
||
| # 왼쪽 자식 노드가 있다면 현재 깊이 + 1을 하여 스택에 추가 | ||
| if node.left: | ||
| stack.append((node.left, cur_depth + 1)) | ||
|
|
||
| # 오른쪽 자식 노드가 있다면 현재 깊이 + 1을 하여 스택에 추가 | ||
| if node.right: | ||
| stack.append((node.right, cur_depth + 1)) | ||
|
Comment on lines
+9
to
30
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 다른 파일들 포함해서 주석을 많이 달아주셨는데 코드만으로 충분한 것들이 많은 것 같아요! 코드를 통해서 최대한 의도를 드러내고 꼭 필요한 상황에만 적어주시는 편이 좋을 거 같아요. 주석도 같이 읽어야 해서 인지적 부하도 올라가고 가독성이 떨어지는 부분도 생기기도 해서요. 특히 코드와 주석이 정확히 일치하지 않는 경우가 생기지 않도록 조심해야 하는 거 같아요.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 의견 감사합니다! |
||
|
|
||
| return max_depth | ||
|
|
||
| ''' | ||
| 큐를 활용한 BFS | ||
|
|
||
| 시간 복잡도: O(N) | ||
| 공간 복잡도: O(W), W: 트리의 최대 너비 | ||
| ''' | ||
| class Solution: | ||
| def maxDepth(self, root: Optional[TreeNode]) -> int: | ||
| # 트리가 비어있으면 길이는 0 | ||
| if root is None: | ||
| return 0 | ||
|
|
||
| queue = deque([root]) | ||
| depth = 0 | ||
|
|
||
| while queue: | ||
| # 현재 레벨에 있는 노드의 개수만큼 반복 처리 | ||
| level_size = len(queue) | ||
|
|
||
| for _ in range(level_size): | ||
| node = queue.popleft() | ||
|
|
||
| if node.left: | ||
| queue.append(node.left) | ||
| if node.right: | ||
| queue.append(node.right) | ||
|
|
||
| # 한 레벨의 탐색이 끝나면 깊이 증가 | ||
| depth += 1 | ||
|
|
||
| return depth | ||
|
okyungjin marked this conversation as resolved.
|
||
|
|
||
| ''' | ||
| 재귀 함수 풀이 | ||
|
|
||
| 시간 복잡도: O(N) | ||
| 공간 복잡도: O(H), H: 스택의 높이 | ||
| ''' | ||
| class Solution: | ||
| def maxDepth(self, root: Optional[TreeNode]) -> int: | ||
| if root is None: | ||
| return 0 | ||
|
|
||
| left_depth = self.maxDepth(root.left) | ||
| right_depth = self.maxDepth(root.right) | ||
|
|
||
| # 현재 자신의 노드 하나를 더해서 최대 깊이를 반환 | ||
| return max(left_depth, right_depth) + 1 | ||
|
okyungjin marked this conversation as resolved.
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,110 +1,28 @@ | ||
| # from typing import Optional, List | ||
|
|
||
| # class ListNode: | ||
| # def __init__(self, val=0, next=None): | ||
| # self.val = val | ||
| # self.next = next | ||
|
|
||
| # # 파이썬의 toString 역할을 하는 __repr__을 [1,3,4] 형태로 출력되게 수정 | ||
| # def __repr__(self): | ||
| # nodes = [] | ||
| # curr = self | ||
| # while curr: | ||
| # nodes.append(str(curr.val)) | ||
| # curr = curr.next | ||
| # return "[" + ",".join(nodes) + "]" | ||
|
|
||
| # # 파이썬 list를 ListNode 리스트로 변환해주는 헬퍼 함수 | ||
| # def make_linked_list(arr: List[int]) -> Optional[ListNode]: | ||
| # if not arr: | ||
| # return None | ||
| # dummy = ListNode(0) | ||
| # curr = dummy | ||
| # for val in arr: | ||
| # curr.next = ListNode(val) | ||
| # curr = curr.next | ||
| # return dummy.next | ||
|
|
||
|
|
||
| # list1의 노드 개수를 n, list2의 노드 개수를 m이라 할 때 | ||
| # 시간 복잡도: O(n + m) | ||
| # 공간 복잡도: O(n + m) | ||
| class Solution_01: | ||
| # 공간 복잡도: O(1) -> dummy만 사용, 추가 공간 복잡도는 O(1) | ||
| class Solution: | ||
| def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]: | ||
| # 맨 앞에 더미 노드를 하나 추가한다 | ||
| # 없는 상태로 짰는데 if node: else: 구문 늘어나서 맨 앞에 가상의 노드를 하나 추가해줬다 | ||
| # dummy: 병합된 결과 리스트의 시작점을 보존하기 위한 가상의 헤드 노드 | ||
| # current: 현재 병합 위치를 추적하며 노드를 연결해 나가는 포인터 | ||
| dummy = ListNode(None) | ||
| node = dummy | ||
| current = dummy | ||
|
|
||
| while True: | ||
| # list1이 비어있으면 현재 노드 뒤로 list2를 이어준다. | ||
| if not list1: | ||
| node.next = list2 | ||
| break | ||
|
|
||
| # list2가 비어있으면 현재 노드 뒤로 list1을 이어준다. | ||
| if not list2: | ||
| node.next = list1 | ||
| break | ||
|
|
||
| # 값을 비교해서 작은 값을 next 노드에 추가한다. | ||
| # 두 리스트가 모두 존재하는 동안 값을 비교하며 merge한다 | ||
| while list1 and list2: | ||
| # 더 작은 값을 가진 노드를 결과 리스트에 연결 | ||
| if list1.val <= list2.val: | ||
| node.next = ListNode(list1.val) | ||
| # list1 하나 소비 | ||
| current.next = list1 | ||
| list1 = list1.next | ||
| else: | ||
| node.next = ListNode(list2.val) | ||
| # list2 하나 소비 | ||
| current.next = list2 | ||
| list2 = list2.next | ||
|
|
||
| # 다음 노드를 현재 노드로 할당해준다 | ||
| node = node.next | ||
|
|
||
| return dummy.next | ||
|
|
||
|
|
||
| # [접근법] Solution_01 의 공간 복잡도를 개선했습니다. | ||
| # ListNode를 새로 생성하지 않고 기존 노드를 활용하도록 수정했습니다. | ||
|
|
||
| # list1의 노드 개수를 n, list2의 노드 개수를 m이라 할 때 | ||
| # 시간 복잡도: O(n + m) | ||
| # 공간 복잡도: O(1) -> dummy만 사용, 추가 공간 복잡도는 O(1) | ||
| class Solution: | ||
| def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]: | ||
| # 맨 앞에 더미 노드를 하나 추가한다 | ||
| # 없는 상태로 짰는데 if node: else: 구문 늘어나서 맨 앞에 가상의 노드를 하나 추가해줬다 | ||
| dummy = ListNode(None) | ||
| node = dummy | ||
| # 병합 리스트의 포인터를 다음으로 이동 | ||
| current = current.next | ||
|
|
||
| while True: | ||
| if not list1: # list1이 비어있으면 현재 노드 뒤로 list2를 이어준다. | ||
| node.next = list2 | ||
| break | ||
|
|
||
| if not list2: # list2가 비어있으면 현재 노드 뒤로 list1을 이어준다. | ||
| node.next = list1 | ||
| break | ||
|
|
||
| # 값을 비교해서 작은 값을 next 노드에 추가한다. | ||
| if list1.val <= list2.val: | ||
| node.next = list1 # 새로운 ListNode를 생성하지 않고 list1를 할당해준다. | ||
| list1 = list1.next # list1 하나 소비 | ||
| else: | ||
| node.next = list2 # 새로운 ListNode를 생성하지 않고 list2를 할당해준다. | ||
| list2 = list2.next # list2 하나 소비 | ||
|
|
||
| # 다음 노드를 현재 노드로 할당해준다 | ||
| node = node.next | ||
|
|
||
| # 한쪽 리스트가 소진되면 남은 노드들을 한 번에 연결 | ||
| current.next = list1 if list1 else list2 | ||
|
|
||
| # dummy 노드의 다음부터가 실제 정렬된 리스트의 시작 | ||
| return dummy.next | ||
|
|
||
|
|
||
| # 4. 예시 입력으로 호출 및 테스트 | ||
| # if __name__ == "__main__": | ||
| # list1 = make_linked_list([1, 2, 4]) | ||
| # list2 = make_linked_list([1, 3, 4]) | ||
|
|
||
| # solution = Solution() | ||
| # merged_list = solution.mergeTwoLists(list1, list2) | ||
|
|
||
| # print(merged_list) |
|
okyungjin marked this conversation as resolved.
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,89 +1,61 @@ | ||
| # https://leetcode.com/problems/word-search/ | ||
|
|
||
| # board: 영문 대/소문자 배열, m * n | ||
| # board의 상하좌우로 인접한 문자열을 이어붙여서 word를 완성할 수 있는지 여부를 반환한다. | ||
| # 단, 셀은 한번만 사용 가능하다 | ||
|
|
||
| # dfs를 통해 백트랙킹으로 정답을 찾도록 구현했다. | ||
| # [접근법] | ||
| # dfs로 문자열을 상,하,좌,우로 탐색한다. | ||
|
|
||
| # [복잡도] | ||
| # L: word 의 길이 | ||
| # 시간 복잡도: O(m * n * 4^L) | ||
| # 공간 복잡도: O(m * n + L) | ||
| class SolutionA: | ||
| # 시간 복잡도: O(m * n * 3^L), 이전 문자에서 이동한 방향은 제외하므로 3^L | ||
| # 공간 복잡도: O(L) | ||
|
|
||
| class Solution: | ||
| def exist(self, board: List[List[str]], word: str) -> bool: | ||
| row_size = len(board) | ||
| col_size = len(board[0]) | ||
|
|
||
| # 방문 여부를 기록하는 2차원 배열 | ||
| visited = [[False] * col_size for _ in range(row_size)] | ||
|
|
||
| def dfs(row, col, str_idx) -> bool: | ||
| if str_idx == len(word): | ||
| return True | ||
|
|
||
| # 종료조건1: 보드의 좌표를 넘어가는 경우 | ||
| if row < 0 or row >= row_size or col < 0 or col >= col_size: | ||
| # board에 word를 만들 수 있는 문자열이 충분히 있는지 확인 | ||
| for char in set(word): | ||
| # board 전체에서 해당 알파벳이 몇 개인지 카운트한다. | ||
| board_char_count = sum(row.count(char) for row in board) | ||
| if board_char_count < word.count(char): | ||
| return False | ||
|
|
||
| # 종료조건 2: 이미 방문함 | ||
| # 종료조건 3: 문자열 불일치 | ||
| if visited[row][col] or board[row][col] != word[str_idx]: | ||
| return False | ||
|
|
||
|
|
||
| visited[row][col] = True | ||
|
|
||
| # 차례대로 상, 하, 좌, 우의 좌표를 탐색 | ||
| found = (dfs(row - 1, col, str_idx + 1) or \ | ||
| dfs(row + 1, col, str_idx + 1) or \ | ||
| dfs(row, col - 1, str_idx + 1) or \ | ||
| dfs(row, col + 1, str_idx + 1)) | ||
|
|
||
| visited[row][col] = False # 백트래킹 | ||
|
|
||
| return found | ||
|
|
||
| # 빈도수가 더 적은 글자부터 탐색하도록 단어를 뒤집는다 | ||
| count_first = sum(row.count(word[0]) for row in board) | ||
| count_last = sum(row.count(word[-1]) for row in board) | ||
|
|
||
| if count_first > count_last: | ||
| word = word[::-1] | ||
|
Comment on lines
+27
to
+32
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 인풋의 패턴을 파악해서 최적화 한다는 점에선 좋은 접근인 거 같습니다. 다만 첫글자와 마지막 글자의 빈도만 비교해서 최적화하는건 특수한 케이스만 커버가 될 거 같아요. 리트코드 채점 데이터셋엔 이 경우가 들어간 거 같더라구요.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오 넵넵 감사합니다 : ) |
||
|
|
||
| # 시작점을 찾는다 | ||
| for r in range(row_size): | ||
| for c in range(col_size): | ||
| if board[r][c] == word[0] and dfs(r, c, 0): | ||
| return True | ||
|
|
||
| return False | ||
|
|
||
| # SolutionA 의 공간복잡도 최적화 버전, visited 제거 | ||
| # L: word 의 길이 | ||
| # 시간 복잡도: O(m * n * 4^L) | ||
| # 공간 복잡도: O(L) | ||
| class Solution: | ||
| def exist(self, board: List[List[str]], word: str) -> bool: | ||
| VISITED_MARK = '#' | ||
|
|
||
| row_size = len(board) | ||
| col_size = len(board[0]) | ||
|
|
||
| def dfs(row, col, str_idx) -> bool: | ||
| def dfs(row: int, col: int, str_idx: int) -> bool: | ||
| if str_idx == len(word): | ||
| return True | ||
|
|
||
| # 종료조건1: 보드의 좌표를 넘어가는 경우 | ||
| if row < 0 or row >= row_size or col < 0 or col >= col_size: | ||
| if not (0 <= row < row_size and 0 <= col < col_size): | ||
| return False | ||
|
|
||
| # 종료조건 2: 이미 방문함 | ||
| # 종료조건 2: 이미 방문함 (#: 방문했음을 표시하는 마크) | ||
| # 종료조건 3: 문자열 불일치 | ||
| if board[row][col] == VISITED_MARK or board[row][col] != word[str_idx]: | ||
| return False | ||
|
|
||
|
|
||
| # VISITED_MARK로 교체하여 방문을 기록 | ||
| # 방문을 표시하는 마크(#)로 임시 교체 | ||
| origin_char = board[row][col] | ||
| board[row][col] = VISITED_MARK | ||
|
|
||
| # 차례대로 상, 하, 좌, 우의 좌표를 탐색 | ||
| found = (dfs(row - 1, col, str_idx + 1) or \ | ||
| dfs(row + 1, col, str_idx + 1) or \ | ||
| dfs(row, col - 1, str_idx + 1) or \ | ||
| dfs(row, col + 1, str_idx + 1)) | ||
| # 좌표를 탐색한다 | ||
| found = ( | ||
| dfs(row - 1, col, str_idx + 1) # 상 | ||
| or dfs(row + 1, col, str_idx + 1) # 하 | ||
| or dfs(row, col - 1, str_idx + 1) # 좌 | ||
| or dfs(row, col + 1, str_idx + 1) # 우 | ||
| ) | ||
|
|
||
| # 원본 문자열로 복원 | ||
| board[row][col] = origin_char | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.