Skip to content

Commit c2d5756

Browse files
authored
Merge pull request #1540 from seungriyou/main
[seungriyou] Week 10 Solutions
2 parents fc5692c + 028dd0f commit c2d5756

File tree

5 files changed

+386
-0
lines changed

5 files changed

+386
-0
lines changed

course-schedule/seungriyou.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# https://leetcode.com/problems/course-schedule/
2+
3+
from typing import List
4+
5+
class Solution:
6+
def canFinish_topo(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
7+
"""
8+
[Complexity]
9+
- TC: O(v + e) (v = numCourses, e = len(prerequisites))
10+
- SC: O(v + e) (graph)
11+
12+
[Approach]
13+
course schedule은 directed graph이므로, topological sort(BFS)를 이용해 방문한 노드의 개수가 numCourses와 같은지 확인한다.
14+
"""
15+
from collections import deque
16+
17+
# directed graph
18+
graph = [[] for _ in range(numCourses)]
19+
indegree = [0] * numCourses
20+
21+
for a, b in prerequisites:
22+
graph[b].append(a) # b -> a
23+
indegree[a] += 1
24+
25+
def topo_sort():
26+
# topological sort로 방문한 course 개수
27+
cnt = 0
28+
29+
# indegree가 0인 course 부터 시작
30+
q = deque([i for i in range(numCourses) if indegree[i] == 0])
31+
32+
while q:
33+
pos = q.popleft()
34+
35+
# 방문한 course 개수 세기
36+
cnt += 1
37+
38+
for npos in graph[pos]:
39+
# npos의 indegree 감소
40+
indegree[npos] -= 1
41+
# indegree[npos] == 0, 즉, npos를 방문하기 위한 prerequisite이 모두 방문되었다면, q에 npos 추가
42+
if indegree[npos] == 0:
43+
q.append(npos)
44+
45+
return cnt
46+
47+
return numCourses == topo_sort()
48+
49+
def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
50+
"""
51+
[Complexity]
52+
- TC: O(v + e) (모든 노드 & 간선은 한 번 씩 방문, 재귀 호출도 첫 방문 시에만 수행)
53+
- SC: O(v + e) (graph)
54+
55+
[Approach]
56+
course schedule은 directed graph이므로, 모든 course를 끝낼 수 없다는 것은 directed graph에 cycle이 존재한다는 것이다.
57+
directed graph의 cycle 여부를 판단하기 위해 3-state DFS를 사용할 수 있다.
58+
1) 이전에 이미 방문한 상태 (visited)
59+
2) 현재 보고 있는 경로에 이미 존재하는 상태 (current_path) -> 재귀 실행 전에 추가 & 후에 제거
60+
3) 아직 방문하지 않은 상태
61+
"""
62+
63+
graph = [[] for _ in range(numCourses)] # directed graph
64+
visited = set() # 이전에 이미 방문한 노드 기록
65+
current_path = set() # 현재 경로에 이미 존재하는 노드를 만났다면, cycle이 존재하는 것
66+
67+
for a, b in prerequisites:
68+
graph[b].append(a) # b -> a
69+
70+
def is_cyclic(pos):
71+
# base condition
72+
# 1) pos가 current_path에 이미 존재한다면, cycle 발견
73+
if pos in current_path:
74+
return True
75+
# 2) pos가 이전에 이미 방문한 (+ cycle이 존재하지 않는 경로 위의) 노드라면, cycle 발견 X
76+
if pos in visited:
77+
return False
78+
79+
# recur (backtracking)
80+
current_path.add(pos) # 현재 경로에 추가
81+
for npos in graph[pos]:
82+
if is_cyclic(npos):
83+
return True
84+
current_path.remove(pos) # 현재 경로에서 제거
85+
86+
# 방문 처리 (+ cycle이 존재하지 않는 경로 위에 있음을 표시)
87+
visited.add(pos)
88+
89+
return False
90+
91+
for i in range(numCourses):
92+
# course schedule에 cycle이 존재한다면, 전체 course를 완료할 수 없음
93+
if is_cyclic(i):
94+
return False
95+
96+
return True

invert-binary-tree/seungriyou.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# https://leetcode.com/problems/invert-binary-tree/
2+
3+
from typing import Optional
4+
5+
# Definition for a binary tree node.
6+
class TreeNode:
7+
def __init__(self, val=0, left=None, right=None):
8+
self.val = val
9+
self.left = left
10+
self.right = right
11+
12+
class Solution:
13+
def invertTree_recur1(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
14+
"""
15+
[Complexity]
16+
- TC: O(n) (모든 노드 방문)
17+
- SC: O(height) (call stack)
18+
19+
[Approach]
20+
DFS 처럼 recursive 하게 접근한다.
21+
"""
22+
23+
def invert(node):
24+
# base condition
25+
if not node:
26+
return
27+
28+
# recur (& invert the children)
29+
node.left, node.right = invert(node.right), invert(node.left)
30+
31+
return node
32+
33+
return invert(root)
34+
35+
def invertTree_recur(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
36+
"""
37+
[Complexity]
38+
- TC: O(n)
39+
- SC: O(height) (call stack)
40+
41+
[Approach]
42+
recursive 한 방법에서 base condition 처리 로직을 더 짧은 코드로 나타낼 수 있다.
43+
"""
44+
45+
def invert(node):
46+
if node:
47+
# recur (& invert the children)
48+
node.left, node.right = invert(node.right), invert(node.left)
49+
return node
50+
51+
return invert(root)
52+
53+
def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
54+
"""
55+
[Complexity]
56+
- TC: O(n)
57+
- SC: O(width) (queue)
58+
59+
[Approach]
60+
BFS 처럼 iterative 하게 접근한다.
61+
"""
62+
from collections import deque
63+
64+
q = deque([root])
65+
66+
while q:
67+
node = q.popleft()
68+
69+
if node:
70+
# invert the children
71+
node.left, node.right = node.right, node.left
72+
73+
# add to queue
74+
q.append(node.left)
75+
q.append(node.right)
76+
77+
return root

jump-game/seungriyou.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# https://leetcode.com/problems/jump-game/
2+
3+
from typing import List
4+
5+
class Solution:
6+
def canJump_slow_dp(self, nums: List[int]) -> bool:
7+
"""
8+
[Complexity]
9+
- TC: O(n^2)
10+
- SC: O(n)
11+
12+
[Approach]
13+
dp[i] = i-th idx에서 마지막 칸까지 도달 가능한지 여부
14+
맨 오른쪽 칸까지의 도달 가능 여부를 확인해야 하므로, nums[i] 만큼 오른쪽으로 가봐야 한다.
15+
따라서 맨 오른쪽부터 dp table을 채워나가면 되고, nums[i] 만큼 오른쪽으로 가보다가 True일 때가 나오면 빠르게 break 한다.
16+
"""
17+
n = len(nums)
18+
dp = [False] * n
19+
dp[n - 1] = True
20+
21+
# i 보다 오른쪽 값이 필요하므로, 오른쪽에서부터 dp table 채워나가기
22+
for i in range(n - 2, -1, -1):
23+
# i에서 nums[i] 만큼 오른쪽으로 가보기
24+
for di in range(nums[i] + 1):
25+
# 중간에 마지막 칸까지 도달 가능한 칸이 나온다면, dp[i] = True & break
26+
if dp[i + di]:
27+
dp[i] = True
28+
break
29+
30+
return dp[0]
31+
32+
def canJump_greedy(self, nums: List[int]) -> bool:
33+
"""
34+
[Complexity]
35+
- TC: O(n)
36+
- SC: O(1)
37+
38+
[Approach]
39+
왼쪽 idx부터 확인하며, 특정 idx에서 오른쪽 방향으로 최대한 갈 수 있는 max_step을 greedy 하게 트래킹한다.
40+
이때, 다음 idx로 넘어갈 때마다 최대한 갈 수 있는 max_step에서 -1을 해주어야 하며,
41+
중간에 max_step이 음수가 되는 경우라면 마지막 idx까지 진행할 수 없는 것이므로 False를 반환한다.
42+
"""
43+
max_step = 0
44+
45+
for n in nums:
46+
# max_step이 음수가 되는 경우라면, 마지막 idx까지 진행할 수 없음
47+
if max_step < 0:
48+
return False
49+
50+
# max_step 업데이트
51+
if max_step < n:
52+
max_step = n
53+
54+
# 다음 idx로 넘어가기 위해 max_step--
55+
max_step -= 1
56+
57+
return True
58+
59+
def canJump(self, nums: List[int]) -> bool:
60+
"""
61+
[Complexity]
62+
- TC: O(n)
63+
- SC: O(1)
64+
65+
[Approach]
66+
맨 오른쪽 idx에 도달할 수 있는 idx(= idx_can_reach_end)를 트래킹함으로써 DP 풀이를 optimize 할 수 있다.
67+
nums의 오른쪽 원소부터 확인하면서, i + nums[i]가 idx_can_reach_end 보다 gte 이면
68+
**idx_can_reach_end를 거쳐서 맨 오른쪽 idx에 도달할 수 있는 것**이므로 idx_can_reach_end를 i로 업데이트 한다.
69+
모든 순회가 끝나고, idx_can_reach_end == 0인지 여부를 반환하면 된다.
70+
"""
71+
n = len(nums)
72+
# 맨 오른쪽 idx에 도달할 수 있는 idx
73+
idx_can_reach_end = n - 1
74+
75+
# 오른쪽에서부터 확인
76+
for i in range(n - 2, -1, -1):
77+
# 현재 idx에서 idx_can_reach_end를 거쳐서 맨 오른쪽 idx에 도달할 수 있는 경우, idx_can_reach_end 업데이트
78+
if i + nums[i] >= idx_can_reach_end:
79+
idx_can_reach_end = i
80+
81+
return idx_can_reach_end == 0

merge-k-sorted-lists/seungriyou.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# https://leetcode.com/problems/merge-k-sorted-lists/
2+
3+
from typing import List, Optional
4+
5+
6+
# Definition for singly-linked list.
7+
class ListNode:
8+
def __init__(self, val=0, next=None):
9+
self.val = val
10+
self.next = next
11+
12+
class Solution:
13+
def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
14+
"""
15+
[Complexity]
16+
- TC: O(mlogn) (n = len(lists), m = node의 전체 개수)
17+
- SC: O(n) (min heap)
18+
19+
[Approach]
20+
각각 이미 sorted인 linked list들을 하나의 sorted linked list로 merge 하는 것이므로,
21+
주어진 각각의 linked list에서 node 하나씩을 꺼내어 보며 그 값을 비교하면 된다.
22+
이때, 길이가 n(= len(lists))인 min heap을 사용하면 각 linked list에서의 node 하나씩을 담아 최솟값을 가진 node를 O(logn)에 pop 할 수 있게 된다.
23+
다만, min heap에 node만 넣으면 비교할 수 없으므로, 다음과 같이 구성된 tuple을 min heap에 넣는다.
24+
(* 파이썬에서 tuple 끼리 비교할 때는 앞 원소부터 차례로 비교되며, 앞 원소에서 값이 동일하면 다음 원소로 넘어간다.)
25+
(value, index in lists, node)
26+
- value: 가장 먼저 value를 비교하도록 한다.
27+
- index in lists: 만약 value가 서로 같은 상황이라면 더이상 최솟값을 고르기 위한 비교가 진행되지 않도록 unique한 값인 lists에서의 index로 비교하도록 한다.
28+
실제로 사용되는 값은 아니나, node 끼리 비교가 불가능하므로 사용한다.
29+
- node: 결과 merged linked-list에 넣기 위해 실물 node가 필요하며, next node를 min heap에 넣을 때 필요하다.
30+
"""
31+
import heapq
32+
33+
# 주어진 각 linked list의 첫 node를 min heap에 넣기
34+
q = [(node.val, i, node) for i, node in enumerate(lists) if node]
35+
heapq.heapify(q) # list를 먼저 완성하고 heapify하면 O(n)
36+
37+
res = curr = ListNode()
38+
39+
while q:
40+
# 최솟값을 가진 node 추출
41+
value, i, node = heapq.heappop(q)
42+
43+
# res에 node 추가
44+
curr.next = node
45+
curr = curr.next
46+
47+
# node의 다음 노드를 min heap에 넣어주기
48+
if node.next:
49+
heapq.heappush(q, (node.next.val, i, node.next))
50+
51+
return res.next
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# https://leetcode.com/problems/search-in-rotated-sorted-array/
2+
3+
from typing import List
4+
5+
class Solution:
6+
def search_1(self, nums: List[int], target: int) -> int:
7+
"""
8+
[Complexity]
9+
- TC: O(logn)
10+
- SC: O(1)
11+
12+
[Approach]
13+
sorted array를 다루면서 O(log n) time에 수행되어야 하므로 binary search를 사용해야 한다.
14+
기본적으로 sorted array이므로, rotated 되었더라도 mid를 기준으로 한 쪽은 무조건 sorted이다.
15+
그리고 sorted인 부분에 target이 존재하는지 여부는 양끝 값과만 비교하더라도 알 수 있다.
16+
따라서 다음의 두 가지 경우로 나누어 볼 수 있다.
17+
1) 왼쪽이 sorted
18+
-> target이 왼쪽에 포함되면 왼쪽으로, 아니라면 오른쪽으로
19+
2) 오른쪽이 sorted
20+
-> target이 오른쪽에 포함되면 오른쪽으로, 아니라면 왼쪽으로
21+
"""
22+
lo, hi = 0, len(nums) - 1
23+
24+
while lo < hi:
25+
mid = (lo + hi) // 2
26+
27+
# 1) 왼쪽이 sorted
28+
if nums[lo] <= nums[mid]:
29+
# target이 왼쪽에 포함되면 왼쪽 살펴보기
30+
if nums[lo] <= target <= nums[mid]:
31+
hi = mid
32+
# 아니라면 오른쪽 살펴보기
33+
else:
34+
lo = mid + 1
35+
# 2) 오른쪽이 sorted
36+
else:
37+
# target이 오른쪽에 포함되면 오른쪽 살펴보기
38+
if nums[mid] < target <= nums[hi]:
39+
lo = mid + 1
40+
# 아니라면 왼쪽 살펴보기
41+
else:
42+
hi = mid
43+
44+
return hi if nums[hi] == target else -1
45+
46+
def search(self, nums: List[int], target: int) -> int:
47+
"""
48+
[Complexity]
49+
- TC: O(logn)
50+
- SC: O(1)
51+
52+
[Approach]
53+
앞의 풀이에서 더 명시적으로 경계 조건을 판단하도록 수정할 수 있다. (nums[mid] == target이라면 바로 반환)
54+
"""
55+
lo, hi = 0, len(nums) - 1
56+
57+
while lo <= hi:
58+
mid = (lo + hi) // 2
59+
60+
# nums[mid] == target인 경우 곧바로 반환 (명시적)
61+
if nums[mid] == target:
62+
return mid
63+
64+
# 1) 왼쪽이 sorted
65+
if nums[lo] <= nums[mid]:
66+
# target이 왼쪽에 포함되면 왼쪽 살펴보기
67+
if nums[lo] <= target < nums[mid]:
68+
hi = mid - 1
69+
# 아니라면 오른쪽 살펴보기
70+
else:
71+
lo = mid + 1
72+
# 2) 오른쪽이 sorted
73+
else:
74+
# target이 오른쪽에 포함되면 오른쪽 살펴보기
75+
if nums[mid] < target <= nums[hi]:
76+
lo = mid + 1
77+
# 아니라면 왼쪽 살펴보기
78+
else:
79+
hi = mid - 1
80+
81+
return -1

0 commit comments

Comments
 (0)