Skip to content
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

[SunaDu] Week 10 #1013

Merged
merged 5 commits into from
Feb 16, 2025
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
184 changes: 184 additions & 0 deletions course-schedule/dusunax.py
Original file line number Diff line number Diff line change
@@ -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))
65 changes: 65 additions & 0 deletions invert-binary-tree/dusunax.py
Original file line number Diff line number Diff line change
@@ -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
45 changes: 45 additions & 0 deletions search-in-rotated-sorted-array/dusunax.py
Original file line number Diff line number Diff line change
@@ -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