diff --git a/course-schedule/seungriyou.py b/course-schedule/seungriyou.py new file mode 100644 index 000000000..5452cd694 --- /dev/null +++ b/course-schedule/seungriyou.py @@ -0,0 +1,96 @@ +# https://leetcode.com/problems/course-schedule/ + +from typing import List + +class Solution: + def canFinish_topo(self, numCourses: int, prerequisites: List[List[int]]) -> bool: + """ + [Complexity] + - TC: O(v + e) (v = numCourses, e = len(prerequisites)) + - SC: O(v + e) (graph) + + [Approach] + course schedule은 directed graph이므로, topological sort(BFS)를 이용해 방문한 노드의 개수가 numCourses와 같은지 확인한다. + """ + from collections import deque + + # directed graph + graph = [[] for _ in range(numCourses)] + indegree = [0] * numCourses + + for a, b in prerequisites: + graph[b].append(a) # b -> a + indegree[a] += 1 + + def topo_sort(): + # topological sort로 방문한 course 개수 + cnt = 0 + + # indegree가 0인 course 부터 시작 + q = deque([i for i in range(numCourses) if indegree[i] == 0]) + + while q: + pos = q.popleft() + + # 방문한 course 개수 세기 + cnt += 1 + + for npos in graph[pos]: + # npos의 indegree 감소 + indegree[npos] -= 1 + # indegree[npos] == 0, 즉, npos를 방문하기 위한 prerequisite이 모두 방문되었다면, q에 npos 추가 + if indegree[npos] == 0: + q.append(npos) + + return cnt + + return numCourses == topo_sort() + + def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool: + """ + [Complexity] + - TC: O(v + e) (모든 노드 & 간선은 한 번 씩 방문, 재귀 호출도 첫 방문 시에만 수행) + - SC: O(v + e) (graph) + + [Approach] + course schedule은 directed graph이므로, 모든 course를 끝낼 수 없다는 것은 directed graph에 cycle이 존재한다는 것이다. + directed graph의 cycle 여부를 판단하기 위해 3-state DFS를 사용할 수 있다. + 1) 이전에 이미 방문한 상태 (visited) + 2) 현재 보고 있는 경로에 이미 존재하는 상태 (current_path) -> 재귀 실행 전에 추가 & 후에 제거 + 3) 아직 방문하지 않은 상태 + """ + + graph = [[] for _ in range(numCourses)] # directed graph + visited = set() # 이전에 이미 방문한 노드 기록 + current_path = set() # 현재 경로에 이미 존재하는 노드를 만났다면, cycle이 존재하는 것 + + for a, b in prerequisites: + graph[b].append(a) # b -> a + + def is_cyclic(pos): + # base condition + # 1) pos가 current_path에 이미 존재한다면, cycle 발견 + if pos in current_path: + return True + # 2) pos가 이전에 이미 방문한 (+ cycle이 존재하지 않는 경로 위의) 노드라면, cycle 발견 X + if pos in visited: + return False + + # recur (backtracking) + current_path.add(pos) # 현재 경로에 추가 + for npos in graph[pos]: + if is_cyclic(npos): + return True + current_path.remove(pos) # 현재 경로에서 제거 + + # 방문 처리 (+ cycle이 존재하지 않는 경로 위에 있음을 표시) + visited.add(pos) + + return False + + for i in range(numCourses): + # course schedule에 cycle이 존재한다면, 전체 course를 완료할 수 없음 + if is_cyclic(i): + return False + + return True diff --git a/invert-binary-tree/seungriyou.py b/invert-binary-tree/seungriyou.py new file mode 100644 index 000000000..db9899825 --- /dev/null +++ b/invert-binary-tree/seungriyou.py @@ -0,0 +1,77 @@ +# https://leetcode.com/problems/invert-binary-tree/ + +from typing import Optional + +# 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 invertTree_recur1(self, root: Optional[TreeNode]) -> Optional[TreeNode]: + """ + [Complexity] + - TC: O(n) (모든 노드 방문) + - SC: O(height) (call stack) + + [Approach] + DFS 처럼 recursive 하게 접근한다. + """ + + def invert(node): + # base condition + if not node: + return + + # recur (& invert the children) + node.left, node.right = invert(node.right), invert(node.left) + + return node + + return invert(root) + + def invertTree_recur(self, root: Optional[TreeNode]) -> Optional[TreeNode]: + """ + [Complexity] + - TC: O(n) + - SC: O(height) (call stack) + + [Approach] + recursive 한 방법에서 base condition 처리 로직을 더 짧은 코드로 나타낼 수 있다. + """ + + def invert(node): + if node: + # recur (& invert the children) + node.left, node.right = invert(node.right), invert(node.left) + return node + + return invert(root) + + def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]: + """ + [Complexity] + - TC: O(n) + - SC: O(width) (queue) + + [Approach] + BFS 처럼 iterative 하게 접근한다. + """ + from collections import deque + + q = deque([root]) + + while q: + node = q.popleft() + + if node: + # invert the children + node.left, node.right = node.right, node.left + + # add to queue + q.append(node.left) + q.append(node.right) + + return root diff --git a/jump-game/seungriyou.py b/jump-game/seungriyou.py new file mode 100644 index 000000000..fe03a3e60 --- /dev/null +++ b/jump-game/seungriyou.py @@ -0,0 +1,81 @@ +# https://leetcode.com/problems/jump-game/ + +from typing import List + +class Solution: + def canJump_slow_dp(self, nums: List[int]) -> bool: + """ + [Complexity] + - TC: O(n^2) + - SC: O(n) + + [Approach] + dp[i] = i-th idx에서 마지막 칸까지 도달 가능한지 여부 + 맨 오른쪽 칸까지의 도달 가능 여부를 확인해야 하므로, nums[i] 만큼 오른쪽으로 가봐야 한다. + 따라서 맨 오른쪽부터 dp table을 채워나가면 되고, nums[i] 만큼 오른쪽으로 가보다가 True일 때가 나오면 빠르게 break 한다. + """ + n = len(nums) + dp = [False] * n + dp[n - 1] = True + + # i 보다 오른쪽 값이 필요하므로, 오른쪽에서부터 dp table 채워나가기 + for i in range(n - 2, -1, -1): + # i에서 nums[i] 만큼 오른쪽으로 가보기 + for di in range(nums[i] + 1): + # 중간에 마지막 칸까지 도달 가능한 칸이 나온다면, dp[i] = True & break + if dp[i + di]: + dp[i] = True + break + + return dp[0] + + def canJump_greedy(self, nums: List[int]) -> bool: + """ + [Complexity] + - TC: O(n) + - SC: O(1) + + [Approach] + 왼쪽 idx부터 확인하며, 특정 idx에서 오른쪽 방향으로 최대한 갈 수 있는 max_step을 greedy 하게 트래킹한다. + 이때, 다음 idx로 넘어갈 때마다 최대한 갈 수 있는 max_step에서 -1을 해주어야 하며, + 중간에 max_step이 음수가 되는 경우라면 마지막 idx까지 진행할 수 없는 것이므로 False를 반환한다. + """ + max_step = 0 + + for n in nums: + # max_step이 음수가 되는 경우라면, 마지막 idx까지 진행할 수 없음 + if max_step < 0: + return False + + # max_step 업데이트 + if max_step < n: + max_step = n + + # 다음 idx로 넘어가기 위해 max_step-- + max_step -= 1 + + return True + + def canJump(self, nums: List[int]) -> bool: + """ + [Complexity] + - TC: O(n) + - SC: O(1) + + [Approach] + 맨 오른쪽 idx에 도달할 수 있는 idx(= idx_can_reach_end)를 트래킹함으로써 DP 풀이를 optimize 할 수 있다. + nums의 오른쪽 원소부터 확인하면서, i + nums[i]가 idx_can_reach_end 보다 gte 이면 + **idx_can_reach_end를 거쳐서 맨 오른쪽 idx에 도달할 수 있는 것**이므로 idx_can_reach_end를 i로 업데이트 한다. + 모든 순회가 끝나고, idx_can_reach_end == 0인지 여부를 반환하면 된다. + """ + n = len(nums) + # 맨 오른쪽 idx에 도달할 수 있는 idx + idx_can_reach_end = n - 1 + + # 오른쪽에서부터 확인 + for i in range(n - 2, -1, -1): + # 현재 idx에서 idx_can_reach_end를 거쳐서 맨 오른쪽 idx에 도달할 수 있는 경우, idx_can_reach_end 업데이트 + if i + nums[i] >= idx_can_reach_end: + idx_can_reach_end = i + + return idx_can_reach_end == 0 diff --git a/merge-k-sorted-lists/seungriyou.py b/merge-k-sorted-lists/seungriyou.py new file mode 100644 index 000000000..2aa9dab30 --- /dev/null +++ b/merge-k-sorted-lists/seungriyou.py @@ -0,0 +1,51 @@ +# https://leetcode.com/problems/merge-k-sorted-lists/ + +from typing import List, Optional + + +# Definition for singly-linked list. +class ListNode: + def __init__(self, val=0, next=None): + self.val = val + self.next = next + +class Solution: + def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]: + """ + [Complexity] + - TC: O(mlogn) (n = len(lists), m = node의 전체 개수) + - SC: O(n) (min heap) + + [Approach] + 각각 이미 sorted인 linked list들을 하나의 sorted linked list로 merge 하는 것이므로, + 주어진 각각의 linked list에서 node 하나씩을 꺼내어 보며 그 값을 비교하면 된다. + 이때, 길이가 n(= len(lists))인 min heap을 사용하면 각 linked list에서의 node 하나씩을 담아 최솟값을 가진 node를 O(logn)에 pop 할 수 있게 된다. + 다만, min heap에 node만 넣으면 비교할 수 없으므로, 다음과 같이 구성된 tuple을 min heap에 넣는다. + (* 파이썬에서 tuple 끼리 비교할 때는 앞 원소부터 차례로 비교되며, 앞 원소에서 값이 동일하면 다음 원소로 넘어간다.) + (value, index in lists, node) + - value: 가장 먼저 value를 비교하도록 한다. + - index in lists: 만약 value가 서로 같은 상황이라면 더이상 최솟값을 고르기 위한 비교가 진행되지 않도록 unique한 값인 lists에서의 index로 비교하도록 한다. + 실제로 사용되는 값은 아니나, node 끼리 비교가 불가능하므로 사용한다. + - node: 결과 merged linked-list에 넣기 위해 실물 node가 필요하며, next node를 min heap에 넣을 때 필요하다. + """ + import heapq + + # 주어진 각 linked list의 첫 node를 min heap에 넣기 + q = [(node.val, i, node) for i, node in enumerate(lists) if node] + heapq.heapify(q) # list를 먼저 완성하고 heapify하면 O(n) + + res = curr = ListNode() + + while q: + # 최솟값을 가진 node 추출 + value, i, node = heapq.heappop(q) + + # res에 node 추가 + curr.next = node + curr = curr.next + + # node의 다음 노드를 min heap에 넣어주기 + if node.next: + heapq.heappush(q, (node.next.val, i, node.next)) + + return res.next diff --git a/search-in-rotated-sorted-array/seungriyou.py b/search-in-rotated-sorted-array/seungriyou.py new file mode 100644 index 000000000..f90884b32 --- /dev/null +++ b/search-in-rotated-sorted-array/seungriyou.py @@ -0,0 +1,81 @@ +# https://leetcode.com/problems/search-in-rotated-sorted-array/ + +from typing import List + +class Solution: + def search_1(self, nums: List[int], target: int) -> int: + """ + [Complexity] + - TC: O(logn) + - SC: O(1) + + [Approach] + sorted array를 다루면서 O(log n) time에 수행되어야 하므로 binary search를 사용해야 한다. + 기본적으로 sorted array이므로, rotated 되었더라도 mid를 기준으로 한 쪽은 무조건 sorted이다. + 그리고 sorted인 부분에 target이 존재하는지 여부는 양끝 값과만 비교하더라도 알 수 있다. + 따라서 다음의 두 가지 경우로 나누어 볼 수 있다. + 1) 왼쪽이 sorted + -> target이 왼쪽에 포함되면 왼쪽으로, 아니라면 오른쪽으로 + 2) 오른쪽이 sorted + -> target이 오른쪽에 포함되면 오른쪽으로, 아니라면 왼쪽으로 + """ + lo, hi = 0, len(nums) - 1 + + while lo < hi: + mid = (lo + hi) // 2 + + # 1) 왼쪽이 sorted + if nums[lo] <= nums[mid]: + # target이 왼쪽에 포함되면 왼쪽 살펴보기 + if nums[lo] <= target <= nums[mid]: + hi = mid + # 아니라면 오른쪽 살펴보기 + else: + lo = mid + 1 + # 2) 오른쪽이 sorted + else: + # target이 오른쪽에 포함되면 오른쪽 살펴보기 + if nums[mid] < target <= nums[hi]: + lo = mid + 1 + # 아니라면 왼쪽 살펴보기 + else: + hi = mid + + return hi if nums[hi] == target else -1 + + def search(self, nums: List[int], target: int) -> int: + """ + [Complexity] + - TC: O(logn) + - SC: O(1) + + [Approach] + 앞의 풀이에서 더 명시적으로 경계 조건을 판단하도록 수정할 수 있다. (nums[mid] == target이라면 바로 반환) + """ + lo, hi = 0, len(nums) - 1 + + while lo <= hi: + mid = (lo + hi) // 2 + + # nums[mid] == target인 경우 곧바로 반환 (명시적) + if nums[mid] == target: + return mid + + # 1) 왼쪽이 sorted + if nums[lo] <= nums[mid]: + # target이 왼쪽에 포함되면 왼쪽 살펴보기 + if nums[lo] <= target < nums[mid]: + hi = mid - 1 + # 아니라면 오른쪽 살펴보기 + else: + lo = mid + 1 + # 2) 오른쪽이 sorted + else: + # target이 오른쪽에 포함되면 오른쪽 살펴보기 + if nums[mid] < target <= nums[hi]: + lo = mid + 1 + # 아니라면 왼쪽 살펴보기 + else: + hi = mid - 1 + + return -1