diff --git a/course-schedule/dusunax.py b/course-schedule/dusunax.py new file mode 100644 index 000000000..40d79aed6 --- /dev/null +++ b/course-schedule/dusunax.py @@ -0,0 +1,184 @@ +''' +# 207 +참고 영상: https://www.youtube.com/watch?v=EgI5nU9etnU +문제 풀이: https://www.algodale.com/problems/course-schedule/ + +## 문제 정리 +👉 prerequisites란? 필수 선수 과목이다. +👉 방향성이 있는 연결 관계이므로, Directed Graph다. +👉 Cycle 발생 시, 코스를 이수할 수 없다.(서로 의존하는 순환이 있어서 끝없이 돌게 되는 경우) + +## 해결 방식 두가지 +1. BFS, Queue, Topological Sort: 위상 정렬 +2. DFS, Cycle Detection: 순환 탐지 + +### 위상 정렬(Topological Sort) - BFS, Queue +- 진입 차수(indegree): 노드로 들어오는 화살표 수 +- 그래프로 인접 리스트 구성 +- Queue에 넣기 +- Queue BFS 탐색 +- 모든 과목을 들었는지 확인 + +### 순환 탐지(Cycle Detection) - DFS +- 그래프로 인접 리스트 구성 +- 방문 상태 배열 초기화 +- dfs 함수 +- 모든 노드에 대해 dfs 실행 + +## TC & SC +- 시간 복잡도와 공간 복잡도는 O(V + E)로 동일하다. +``` +V: 노드 수(과목 수) +E: 간선 수(선수 과목 관계 수) +``` + +### TC is O(V + E) + +두 방법 모두, 그래프의 모든 노드와 간선을 한 번씩 확인함 +- BFS: 모든 V를 순회하면서, 각 노드에서 나가는 E를 따라가며 차수를 줄임 +- DFS: 모든 V를 순회하면서, 각 노드에서 연결된 E를 따라가며 깊이 탐색 + +### SC is O(V + E) +- O(V+E): V + E를 저장하는 인접 리스트 그래프 +- O(V)'s: 방문 상태 저장, 진입 차수 배열, BFS 큐, DFS 호출 스택 + +## 위상정렬(BFS) vs 순환탐지(DFS)🤔 + +### BFS를 사용했을 때 +- 반복문을 사용한 BFS가 indegree(진입차수) 개념이 보다 직관적이므로 => "순서대로 처리할 수 있는지 확인"할 때 명확하게 사용할 수 있다. 진입 차수가 0인 노드부터 시작해서 처리 +- 선수 과목을 다 들은 과목은 진입 차수가 0이 되므로 들어갈 수 있는 과목이라는 점이 명확함 +``` +키워드: 처리 순서를 출력, 순서대로 처리할 수 있는지 +``` + +### DFS를 사용했을 때 +- DFS 순환 탐지는 "순환 여부"가 핵심일 때 자연스럽다. +- 상태(Status)를 사용해서, 방문 중인 노드 상태를 다시 방문한다면 순환이 있음을 바로 알 수 있다. +- 순환이 발견되면 바로 중단하므로, 순환 탐지에 자연스럽다. +``` +키워드: 사이클이 있는지 판단 +``` + +### +a) `@cache`를 활용해보자. +- 파이선 3.9~ 메모이제이션 함수 +- 순수 함수 + 재귀 최적화에 사용 (외부 의존성, 부수효과에 주의할 것) +''' +from enum import Enum + +class Status(Enum): # use it to dfs + INITIAL = 1 + IN_PROGRESS = 2 + FINISHED = 3 + +class Solution: + ''' + 1. BFS + 위상 정렬 + ''' + def canFinishTopologicalSort(self, numCourses: int, prerequisites: List[List[int]]) -> bool: + indegree = [0] * numCourses + graph = defaultdict(list) + + for dest, src in prerequisites: + graph[src].append(dest) + indegree[dest] += 1 + + queue = deque([i for i in range(numCourses) if indegree[i] == 0]) + processed_count = 0 + + while queue: + node = queue.popleft() + processed_count += 1 + for neighbor in graph[node]: + indegree[neighbor] -= 1 + if indegree[neighbor] == 0: + queue.append(neighbor) + + return processed_count == numCourses + + ''' + 2. DFS + 순환 탐지 + ''' + def canFinishCycleDetection(self, numCourses: int, prerequisites: List[List[int]]) -> bool: + graph = defaultdict(list) + + for dest, src in prerequisites: + graph[src].append(dest) + + statuses = {i: Status.INITIAL for i in range(numCourses)} + + def dfs(node): + if statuses[node] == Status.IN_PROGRESS: + return False + if statuses[node] == Status.FINISHED: + return True + + statuses[node] = Status.IN_PROGRESS + for neighbor in graph[node]: + if not dfs(neighbor): + return False + statuses[node] = Status.FINISHED + return True + + return all(dfs(crs) for crs in range(numCourses)) + + ''' + 3. @cache + + 파이썬 3.9 이상에서 사용하는 메모이제이션 데코레이터 + - 동일 입력 -> 동일 출력을 보장한다. + - 128개 까지만 저장하는 @lru_cache도 있다. + ''' + def canFinishWithCache(self, numCourses: int, prerequisites: List[List[int]]) -> bool: + graph = defaultdict(list) + + for dest, src in prerequisites: + graph[src].append(dest) + + traversing = set() + + @cache + def dfs(node): + if node in traversing: + return False + + traversing.add(node) + result = all(dfs(pre) for pre in graph[node]) + traversing.remove(node) + return result + + return all(dfs(node) for node in range(numCourses)) + + ''' + 4. visited과 함께 사용하기 + + @cache 데코레이터는 메모이제이션, 같은 입력값에 따라 같은 결과를 반환하게함 + 결과가 변하지 않을 때 유용함 => dfs(node)는 외부 상태 순환 traversing에 의존해서 동작이 달라질 수 있다. + 따라서 visited set이 더 자연스러울 수 있다 + ''' + def canFinishWithVisited(self, numCourses: int, prerequisites: List[List[int]]) -> bool: + graph = defaultdict(list) + + for dest, src in prerequisites: + graph[src].append(dest) + + traversing = set() + visited = set() + + def dfs(node): + if node in traversing: + return False + if node in visited: + return True + + traversing.add(node) + for pre in graph[node]: + if not dfs(pre): + return False + traversing.remove(node) + + visited.add(node) + return True + + return all(dfs(i) for i in range(numCourses)) diff --git a/invert-binary-tree/dusunax.py b/invert-binary-tree/dusunax.py new file mode 100644 index 000000000..7cd9800d5 --- /dev/null +++ b/invert-binary-tree/dusunax.py @@ -0,0 +1,65 @@ +''' +# 226. Invert Binary Tree + +switch left and right child of each node + +## TC: O(N) + +visit each node once + +## SC: O(h) + +h is height of tree + +- best case: O(logN), balanced tree +- worst case: O(N), skewed tree +''' +class Solution: + ''' + DFS + ''' + def invertTreeRecursive(self, root: Optional[TreeNode]) -> Optional[TreeNode]: + if not root: + return None + + root.left, root.right = root.right, root.left + + self.invertTree(root.left) + self.invertTree(root.right) + + return root + + ''' + BFS + - 직관적인 stack 풀이 + ''' + def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]: + stack = [root] + + while stack: + node = stack.pop() + if not node: + continue + + node.left, node.right = node.right, node.left + stack.append(node.left) + stack.append(node.right) + + return root + + ''' + - 참고용 deque 풀이 + ''' + def invertTreeDeque(self, root: Optional[TreeNode]) -> Optional[TreeNode]: + dq = deque([root]) + + while dq: + node = dq.popleft() + if not node: + continue + + node.left, node.right = node.right, node.left + dq.append(node.left) + dq.append(node.right) + + return root diff --git a/search-in-rotated-sorted-array/dusunax.py b/search-in-rotated-sorted-array/dusunax.py new file mode 100644 index 000000000..3348dfbe5 --- /dev/null +++ b/search-in-rotated-sorted-array/dusunax.py @@ -0,0 +1,45 @@ +''' +# 33. Search in Rotated Sorted Array + +binary search + condition check + +이진 탐색 중, 왼쪽이 정렬되었을 때 +- 타겟이 정렬된 왼쪽에 있는 경우, 왼쪽 탐색 (left부터 mid - 1 사이에서 타겟을 탐색) +- 타겟이 정렬된 왼쪽에 없을 경우, 오른쪽 탐색 (mid + 1부터 right 사이에서 타겟을 탐색) + +이진 탐색 중, 오른쪽이 정렬되었을 때 +- 타겟이 정렬된 오른쪽에 있는 경우, 오른쪽 탐색 (mid + 1부터 right 사이에서 타겟을 탐색) +- 타겟이 정렬된 오른쪽에 없을 경우, 왼쪽 탐색 (left부터 mid - 1 사이에서 타겟을 탐색) + +## TC: O(log n) + +binary search + +## SC: O(1) + +no extra space + +''' +class Solution: + def search(self, nums: List[int], target: int) -> int: + left = 0 + right = len(nums) - 1 + + while left <= right: + mid = left + (right - left) // 2 + + if nums[mid] == target: + return mid + + if nums[left] <= nums[mid]: # is_left_sorted + if nums[left] <= target < nums[mid]: # is_target_left + right = mid - 1 + else: + left = mid + 1 + else: + if nums[mid] < target <= nums[right]: # is_target_right + left = mid + 1 + else: + right = mid - 1 + + return -1