diff --git a/alien-dictionary/seungriyou.py b/alien-dictionary/seungriyou.py new file mode 100644 index 000000000..8cbe5437a --- /dev/null +++ b/alien-dictionary/seungriyou.py @@ -0,0 +1,61 @@ +# https://leetcode.com/problems/alien-dictionary/ + +from typing import List + +class Solution: + def alienOrder(self, words: List[str]) -> str: + """ + [Complexity] + (V = words 내 unique 문자 개수, E = edge 개수, L = words내 모든 문자 개수) + - TC: O(V + L) (topo sort에 O(V + E) 소요 -> E < L 이므로) + - SC: O(V + E) (graph) + + [Approach] + topo sort를 이용하여, words에 포함된 전체 문자 개수와 topo sort 순으로 방문한 문자 개수를 비교하여 결과를 반환한다. + 1) directed graph를 구성할 때, words에서 두 인접한 단어를 비교하며 첫 번째로 다른 문자가 나올 때 graph & indegree를 기록한다. (sorted lexicographically) + 이때, 순서 resolving이 불가능한 경우를 판단하여 빠르게 반환할 수 있다. + (인접한 두 단어를 순서대로 w1, w2라고 할 때, (1) w2보다 w1이 더 길면서 (2) w2가 w1에 포함되는 경우, w1의 나머지 부분에 있는 문자는 순서를 알 수 없다.) + 2) 이렇게 기록한 graph & indegree를 기반으로 topo sort로 방문한 문자 개수와 전체 문자 개수를 비교하여 cycle이 발생하지 않는지 확인하고, + cycle이 발생하지 않았다면 결과를 반환, cycle이 발생했다면 빈 문자열을 반환한다. + """ + from collections import defaultdict, deque + + # directed graph + graph, indegree = {}, {} + for word in words: + for w in word: + graph[w] = set() + indegree[w] = 0 + + for i in range(len(words) - 1): + w1, w2 = words[i], words[i + 1] + # 두 인접한 단어를 비교하면서, 첫 번째로 다른 문자가 나올 때 graph & indegree 기록 + for j in range(min(len(w1), len(w2))): + c1, c2 = w1[j], w2[j] + # 첫 번째로 다른 문자 발견 시, 기록 후 다음 단어로 넘어가기 + if c1 != c2: + if c2 not in graph[c1]: + graph[c1].add(c2) + indegree[c2] += 1 + break + # 순서를 resolve 할 수 없는 경우, 빠르게 리턴 ***** + # ex) words = ["abc", "ab"] (w1 = "abc", w2 = "ab") + # -> (1) w2보다 w1이 더 길고 (2) w2가 w1에 포함된다면 (=현재 j가 w2의 마지막 문자를 가리키고 있다면) + # w1[j + 1] 이후의 문자에 대해서는 순서를 resolve 할 수 없음 + elif len(w1) > len(w2) and j == min(len(w1), len(w2)) - 1: + return "" + + # topo sort + q = deque([w for w, ind in indegree.items() if ind == 0 and w in graph]) + res = [] + + while q: + w = q.popleft() + res.append(w) + + for nw in graph[w]: + indegree[nw] -= 1 + if indegree[nw] == 0: + q.append(nw) + + return "".join(res) if len(res) == len(graph) else "" diff --git a/construct-binary-tree-from-preorder-and-inorder-traversal/seungriyou.py b/construct-binary-tree-from-preorder-and-inorder-traversal/seungriyou.py new file mode 100644 index 000000000..ccf4d02e1 --- /dev/null +++ b/construct-binary-tree-from-preorder-and-inorder-traversal/seungriyou.py @@ -0,0 +1,115 @@ +# https://leetcode.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/ + +from typing import List, 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 + +""" +트리를 유일하게 복원하기 위해서는 inorder가 필요하다. inorder는 left/right child를 구분할 수 있기 때문이다. + - preorder + inorder: root를 먼저 알고, inorder로 구조 구분 + - postorder + postorder: root를 나중에 알고, inorder로 구조 구분 +preorder + postorder라면, 구조가 다른 트리를 구분할 수 없는 경우가 있다. +""" + +class Solution: + def buildTree1(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]: + """ + [Complexity] + - TC: O(n^2) + - SC: O(n^2) + (* pop(0), index(), slicing이 모두 O(n)) + + [Approach] + preorder의 경우, root - left - right 순서로 방문하므로, root를 제일 먼저 찾을 수 있다. + inorder의 경우, left - root - right 순서로 방문하므로, + preorder의 매 단계에서 찾은 root에 대해 left와 right subtree를 다음과 같이 정의할 수 있다. (root 제외) + - left subtree = inorder[:root_idx] + - right subtree = inorder[root_idx + 1:] + """ + root = None + + if inorder: + # preorder root + root_idx = inorder.index(preorder.pop(0)) + root = TreeNode(val=inorder[root_idx]) + # preorder left + root.left = self.buildTree(preorder, inorder[:root_idx]) + # preorder right + root.right = self.buildTree(preorder, inorder[root_idx + 1:]) + + return root + + def buildTree2(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]: + """ + [Complexity] + - TC: O(n^2) + - SC: O(n^2) + (* index(), slicing이 모두 O(n)) + + [Approach] + 이전 코드에서 pop(0)에 드는 O(n)을 줄이기 위해, + preorder.reverse() 후 pop()으로 변경할 수 있다. + """ + preorder.reverse() + + def solve(preorder, inorder): + if inorder: + # preorder root + root_idx = inorder.index(preorder.pop()) + root = TreeNode(inorder[root_idx]) + # preorder left + root.left = solve(preorder, inorder[:root_idx]) + # preorder right + root.right = solve(preorder, inorder[root_idx + 1:]) + + return root + + return solve(preorder, inorder) + + def buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]: + """ + [Complexity] + - TC: O(n) + - SC: O(n) (call stack) + + [Approach] + 병목이었던 index()와 slicing을 O(1)로 최적화 할 수 있다. + - index(): inorder의 index를 dict로 캐싱 + - slicing: start/end index로 추적 + 이와 더불어 preorder의 root를 가리키는 index인 pre_idx를 사용하면 pop(0) 또는 pop()을 대체할 수도 있다. + """ + + # instead of index() + inorder_idx = {num: i for i, num in enumerate(inorder)} + + # preorder root (instead of pop()) + pre_idx = 0 + + # instead of inorder[in_left:in_right] slicing + def solve(in_left, in_right): + nonlocal pre_idx + + # base condition + if in_left >= in_right: + return + + # preorder root + root_val = preorder[pre_idx] + root = TreeNode(root_val) + + # update indices + root_idx = inorder_idx[root_val] + pre_idx += 1 + + # recur + root.left = solve(in_left, root_idx) + root.right = solve(root_idx + 1, in_right) + + return root + + return solve(0, len(inorder)) diff --git a/longest-palindromic-substring/seungriyou.py b/longest-palindromic-substring/seungriyou.py new file mode 100644 index 000000000..0dd36c47d --- /dev/null +++ b/longest-palindromic-substring/seungriyou.py @@ -0,0 +1,88 @@ +# https://leetcode.com/problems/longest-palindromic-substring/ + +class Solution: + def longestPalindrome_dp(self, s: str) -> str: + """ + [Complexity] + - TC: O(n^2) + - SC: O(n^2) + + [Approach] + (1) lo == hi: len 1짜리 substring -> True + (2) lo + 1 == hi: len 2짜리 substring -> s[lo] == s[hi] + (3) 그 외: (a) 내부 string인 s[lo + 1] ~ s[hi - 1]이 palindrome이면서, + (b) s[lo] == s[hi]이면 True + + => (3-a)로 인해 2D DP를 사용할 수 있으며 (dp[lo][hi] = s[lo] ~ s[hi]가 palindrome인지 여부), + dp[lo + 1][hi - 1]을 살펴봐야 하므로 lo는 **오른쪽부터 거꾸로 순회**해야 한다. (dp[lo][..]를 채우기 위해 dp[lo + 1][..]을 봐야 함) + """ + + n = len(s) + res_lo = res_hi = 0 + dp = [[False] * n for _ in range(n)] + + # early stop + if n < 2 or s == s[::-1]: + return s + + for lo in range(n - 1, -1, -1): + for hi in range(lo, n): + # (1) lo == hi + if lo == hi: + dp[lo][hi] = True + + # (2) lo + 1 == hi + elif lo + 1 == hi: + dp[lo][hi] = s[lo] == s[hi] + + # (3) otherwise (a) dp[lo + 1][hi - 1] && (b) s[lo] == s[hi] + else: + dp[lo][hi] = dp[lo + 1][hi - 1] and s[lo] == s[hi] + + # update res (s[lo:hi + 1]가 palindrome이면서, 최대 길이인 경우) + if dp[lo][hi] and res_hi - res_lo < hi - lo: + res_lo, res_hi = lo, hi + + return s[res_lo:res_hi + 1] + + def longestPalindrome(self, s: str) -> str: + """ + [Complexity] + - TC: O(n^2) (각 center에서 양쪽으로 expand) + - SC: O(1) + + [Approach] + palindrome은 다음의 두 케이스로 구분되므로, two-pointer로도 풀 수 있다. + (1) substring 길이가 홀수: 가운데 원소는 확인할 필요 X + (2) substring 길이가 짝수: 모두 확인 + """ + + n, res_lo, max_len = len(s), 0, 1 + + # early stop + if n < 2 or s == s[::-1]: + return s + + # expanding from center + def expand(lo, hi): + # 양쪽으로 1씩 늘려가며 가장 긴 palindrome 찾기 + while lo >= 0 and hi < n and s[lo] == s[hi]: + lo -= 1 + hi += 1 + return lo + 1, hi # s[lo + 1:hi] + + # 모든 위치에서 (1) 홀수 길이 palindrome, (2) 짝수 길이 palindrome의 max len 값 찾기 + for i in range(n - 1): + # 홀수 길이 팰린드롬 (중심이 i) + lo1, hi1 = expand(i, i) + if hi1 - lo1 > max_len: + res_lo = lo1 + max_len = hi1 - lo1 + + # 짝수 길이 팰린드롬 (중심이 i, i+1) + lo2, hi2 = expand(i, i + 1) + if hi2 - lo2 > max_len: + res_lo = lo2 + max_len = hi2 - lo2 + + return s[res_lo:res_lo + max_len] diff --git a/rotate-image/seungriyou.py b/rotate-image/seungriyou.py new file mode 100644 index 000000000..684cb9f01 --- /dev/null +++ b/rotate-image/seungriyou.py @@ -0,0 +1,30 @@ +# https://leetcode.com/problems/rotate-image/ + +from typing import List + +class Solution: + def rotate(self, matrix: List[List[int]]) -> None: + """ + Do not return anything, modify matrix in-place instead. + """ + """ + [Complexity] + - TC: O(n^2) (diagonal = n * (n - 1) / 2) + - SC: O(1) + + [Approach] + clockwise rotation을 in-place로 하려면 다음의 순서로 수행한다. + 1. matrix reverse (by row) + 2. matrix transpose (diagonal) + """ + + n = len(matrix) + + # 1. matrix reverse (by row) + for r in range(n // 2): + matrix[r], matrix[n - r - 1] = matrix[n - r - 1], matrix[r] + + # 2. matrix transpose (diagonal) + for r in range(n): + for c in range(r): + matrix[r][c], matrix[c][r] = matrix[c][r], matrix[r][c] diff --git a/subtree-of-another-tree/seungriyou.py b/subtree-of-another-tree/seungriyou.py new file mode 100644 index 000000000..f01b557a9 --- /dev/null +++ b/subtree-of-another-tree/seungriyou.py @@ -0,0 +1,51 @@ +# https://leetcode.com/problems/subtree-of-another-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 isSubtree(self, root: Optional[TreeNode], subRoot: Optional[TreeNode]) -> bool: + """ + [Complexity] + - TC: O(n * m) (n = root 내 노드 개수, m = subRoot 내 노드 개수) + - SC: O(n + m) (call stack) + + [Approach] + 다음과 같이 동작을 나눌 수 있다. + (1) tree를 타고 내려가는 로직 : dfs + (2) 두 tree가 동일한지 판단하는 로직 : check + """ + + def check(node1, node2): + # base condition + if not node1 and not node2: + return True + if not node1 or not node2: + return False + + # recur + return ( + node1.val == node2.val # 두 노드의 값이 같고 + and check(node1.left, node2.left) # 하위 children도 모두 같아야 함 + and check(node1.right, node2.right) + ) + + def dfs(node, sub_tree): + # base condition + if not node: + return False + + # recur + return ( + check(node, sub_tree) # 현재 node부터 sub_tree와 동일한지 확인 + or dfs(node.left, sub_tree) # 동일하지 않다면, node.left로 타고 내려가서 확인 + or dfs(node.right, sub_tree) # 동일하지 않다면, node.right로도 타고 내려가서 확인 + ) + + return dfs(root, subRoot)