Skip to content

[seungriyou] Week 15 Solutions #1664

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

Merged
merged 5 commits into from
Jul 13, 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
61 changes: 61 additions & 0 deletions alien-dictionary/seungriyou.py
Original file line number Diff line number Diff line change
@@ -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 ""
Original file line number Diff line number Diff line change
@@ -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))
88 changes: 88 additions & 0 deletions longest-palindromic-substring/seungriyou.py
Original file line number Diff line number Diff line change
@@ -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]
30 changes: 30 additions & 0 deletions rotate-image/seungriyou.py
Original file line number Diff line number Diff line change
@@ -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]
51 changes: 51 additions & 0 deletions subtree-of-another-tree/seungriyou.py
Original file line number Diff line number Diff line change
@@ -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)