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

[EGON] Week15 Solutions #609

Merged
merged 7 commits into from
Nov 23, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
106 changes: 106 additions & 0 deletions alien-dictionary/EGON.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
from collections import deque
from typing import List
from unittest import TestCase, main


class Solution:
def foreignDictionary(self, words: List[str]) -> str:
return self.solve_topological_sort(words)

"""
LintCode 로그인이 안되어서 https://neetcode.io/problems/foreign-dictionary 에서 실행시키고 통과만 확인했습니다.

Runtime: ? ms (Beats ?%)
Time Complexity:
#0. 복잡도 변수 정의
- words 배열의 길이를 n
- words 배열을 이루는 단어들의 평균 길이를 l
- words 배열을 이루는 단어를 이루는 문자들의 총 갯수를 c (= n * l)
- words 배열을 이루는 단어를 이루는 문자들의 중복 제거 집합의 크기를 s라 하자

#1. 초기화
- words 배열을 이루는 단어를 이루는 문자들을 조회하며 char_set을 초기화하는데 O(c)
- 위상정렬에 사용할 graph 딕셔너리 초기화를 위해 char_set의 크기만큼 조회하므로 O(s)
- 마찬가지로 위상정렬에 사용할 rank 딕셔너리 초기화에 O(s)
> O(c) + O(s) + O(s) ~= O(c + s)

#2. 위상정렬 간선 초기화
- words 배열을 조회하는데 O(n - 1)
- 단어 간 접두사 관계인 경우, 체크하는 startswith 메서드 사용에 * O(l)
- 단어 간 접두사 관계가 아닌 경우, first_char, second_char를 구하는데
- zip 생성에 O(l)
- zip 조회에 * O(l)
> O(n - 1) * (O(l) + O(l) * O(l)) ~= O(n) * O(l ^ 2) ~= O(c * l) ~= O(c)

#3. 위상정렬 실행
- dq 초기화에 rank 딕셔너리의 모든 키를 조회하는데 O(s)
- dq를 이용해서 graph 딕셔너리의 모든 values를 순회하는데, #2에서 각 first_char, second_char마다 1회 value가 추가되었으므로, 중복이 없는 경우 최대 O(n), upper bound
> O(s) + O(n) ~= O(s + n), upper bound

#4. 최종 계산
> O(c + s) + O(c) + O(s + n) ~= O(c + s) + O(s + n) = O(n * l + s) + O(n + s) ~= O(n * l + s), upper bound

Memory: ? MB (Beats ?%)
Space Complexity: O(s + c)
- char_set의 크기에서 O(s)
- graph의 keys는 최대 s개이고 values는 최대 c개이므로 O(s + c), upper bound
- rank의 keys의 크기에서 O(s)
- dq의 최대 크기는 rank의 크기와 같으므로 O(s)
> O(s) + O(s + c) + O(s) + O(s) ~= O(s + c)
"""
def solve_topological_sort(self, words: List[str]) -> str:
if not words:
return ""

char_set = set([char for word in words for char in word])
graph = {char: set() for char in char_set}
rank = {char: 0 for char in char_set}
for i in range(len(words) - 1):
first_word, second_word = words[i], words[i + 1]

if len(first_word) > len(second_word) and first_word.startswith(second_word):
return ""

first_char, second_char = next(((fc, sc) for fc, sc in zip(first_word, second_word) if fc != sc), ("", ""))
if (first_char and second_char) and second_char not in graph[first_char]:
graph[first_char].add(second_char)
rank[second_char] += 1

result = []
dq = deque([char for char in rank if rank[char] == 0])
while dq:
curr_char = dq.popleft()
result.append(curr_char)
for post_char in graph[curr_char]:
rank[post_char] -= 1
if rank[post_char] == 0:
dq.append(post_char)

if len(result) != len(rank):
return ""
else:
return "".join(result)


class _LeetCodeTestCases(TestCase):
def test_1(self):
words = ["z","o"]
output = "zo"
solution = Solution()
self.assertEqual(solution.foreignDictionary(words), output)

def test_2(self):
words = ["hrn","hrf","er","enn","rfnn"]
output = "hernf"
solution = Solution()
self.assertEqual(solution.foreignDictionary(words), output)

def test_3(self):
words = ["wrt","wrf","er","ett","rftt","te"]
output = "wertf"
solution = Solution()
self.assertEqual(solution.foreignDictionary(words), output)


if __name__ == '__main__':
main()
53 changes: 53 additions & 0 deletions longest-palindromic-substring/EGON.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from unittest import TestCase, main


class Solution:
def longestPalindrome(self, s: str) -> str:
return self.solve_sliding_window(s)

"""
Runtime: 47 ms (Beats 96.97%)
Time Complexity: O(n ^ 3)
- s의 길이를 n이라 하면, s의 길이 - 1 만큼 조회하는데 O(n - 1)
- 각 문자마다 sliding_window를 2회 호출하는데, 각 호출마다 최대 s의 길이만큼 반복하므로, * 2 * O(n), upper bound
- 반복 후 s를 slicing하는데 최대 * O(n), upper bound
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 이 부분의 시간복잡도가 좀 아깝게 느껴졌습니다
str slicing이 꽤 무거운 연산인 것으로 보이는데, 시작과 끝 인덱스를 반환하는 방식으로 sliding_window함수를 수정하면 시간복잡도를 3차원에서 2차원까지 낮출 수 있을 것 같아요 :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 이 부분의 시간복잡도가 좀 아깝게 느껴졌습니다 str slicing이 꽤 무거운 연산인 것으로 보이는데, 시작과 끝 인덱스를 반환하는 방식으로 sliding_window함수를 수정하면 시간복잡도를 3차원에서 2차원까지 낮출 수 있을 것 같아요 :)

이전에 이 문제를 풀었던 적이 있어서 기존 모범 답안을 그대로 제출했는데, 실행 환경에 따른 GC 호출 차이 때문인지 문자열 슬라이싱을 미리해서 리턴하면 훨씬 메모리가 좋더라고요. 시간복잡도는 투 포인터로 풀면 나아지는건 맞긴한데, 애초에 팰린드롬 문제 자체의 제약사항이 1000자 아래여서 별 차이가 없는 걸 알고있긴 했습니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@obzva manacher 알고리즘도 이번에 알게되어서 추가해봤는데 한 번 보시면 도움되실 것 같아 남깁니다 :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그렇군요 ㅎㅎㅎ 그런 이유라면 에곤님 본래의 풀이가 더 합리적인 선택 같습니다

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 이 부분의 시간복잡도가 좀 아깝게 느껴졌습니다
str slicing이 꽤 무거운 연산인 것으로 보이는데, 시작과 끝 인덱스를 반환하는 방식으로 sliding_window함수를 수정하면 시간복잡도를 3차원에서 2차원까지 낮출 수 있을 것 같아요 :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그럼 공간복잡도도 O(1)으로 자연스레 개선될 것 같아요

> O(n - 1) * (2 * O(n)) * O(n) ~= O(n ^ 3)

Memory: 16.54 MB (Beats 88.85%)
Space Complexity: O(n)
- sliding_window의 결과로 생성되는 문자열의 최대 길이는 n이고, 조회마다 2회 생성되므로 2 * O(n), upper bound
> 2 * O(n) ~= O(n)
"""
def solve_sliding_window(self, s: str) -> str:

def sliding_window(left: int, right: int) -> str:
while 0 <= left and right < len(s) and s[left] == s[right - 1]:
left -= 1
right += 1

return s[left + 1: right - 1]

if len(s) < 2 or s == s[::-1]:
return s

result = ''
for i in range(len(s) - 1):
result = max(result, sliding_window(i, i + 1), sliding_window(i, i + 2), key=len)

return result


class _LeetCodeTestCases(TestCase):
def test_1(self):
s = "babad"
output = "bab"
self.assertEqual(Solution().longestPalindrome(s), output)

def test_2(self):
s = "cbbd"
output = "bb"
self.assertEqual(Solution().longestPalindrome(s), output)


if __name__ == '__main__':
main()
47 changes: 47 additions & 0 deletions rotate-image/EGON.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from typing import List
from unittest import TestCase, main


class Solution:
def rotate(self, matrix: List[List[int]]) -> None:
return self.solve(matrix)

"""
Runtime: 0 ms (Beats 100.00%)
Time Complexity: O(n ^ 2)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기막힌 풀이네요 ㅋㅋㅋ 잘 배웠습니다!!

- 행렬의 행과 열을 교환하기 위해 이중 for문 사용에 O(n ^ 2)
- 행렬의 각 행을 뒤집기 위해, 행을 조회하는데 O(n)
- 각 행을 뒤집는데 * O(n)
> O(n ^ 2) + O(n) * O(n) ~= O(n ^ 2) + O(n ^ 2) ~= O(n ^ 2)

Memory: 16.76 MB (Beats 14.84%)
Space Complexity: O(1)
> in-place 풀이이므로 상수 변수 할당을 제외한 메모리 사용 없음, O(1)
"""
def solve(self, matrix: List[List[int]]) -> None:
N = len(matrix)

for i in range(N):
for j in range(i, N):
matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]

for row in matrix:
row.reverse()


class _LeetCodeTestCases(TestCase):
def test_1(self):
matrix = [[1,2,3],[4,5,6],[7,8,9]]
output = [[7,4,1],[8,5,2],[9,6,3]]
Solution().rotate(matrix)
self.assertEqual(matrix, output)

def test_2(self):
matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]
output = [[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]
Solution().rotate(matrix)
self.assertEqual(matrix, output)


if __name__ == '__main__':
main()
116 changes: 116 additions & 0 deletions subtree-of-another-tree/EGON.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
from typing import Optional
from unittest import TestCase, main


# 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:
return self.solve_dfs(root, subRoot)

"""
Runtime: 35 ms (Beats 90.24%)
Time Complexity: O(n)
- root 트리의 크기를 n이라 하면, root 트리의 모든 노드를 조회하는데 O(n)
- 각 노드마다 is_same_tree 실행하는데, subRoot 트리의 크기를 m이라 하면, 최대 subRoot의 노드의 크기만큼 조회하므로 * O(m)
> O(n) * O(m) ~= O(n * m)

Memory: 17.09 (Beats 9.93%)
Space Complexity: O(max(n, m))
- stack의 최대 크기는 root 트리가 편향된 경우이며, 이는 root 트리의 노드의 총 갯수와 같으므로 O(n), upper bound
- is_same_tree 함수의 재귀 스택의 최대 깊이는 subRoot 트리가 편향된 경우이며, 이는 subRoot 트리의 노드의 총 갯수와 같으므로 O(m), upper bound
> O(n) + O(m) ~= O(max(n, m))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 좌변까지는 이해했는데 우변으로 이어지는 논리는 이해하지 못했습니다 @lymchgmk 님 ㅜ
혹시 O(N + M) ~= O(max(N, M)) 이 되는 이유에 대해 설명 부탁드려도 될까요?
제 생각엔, 스택의 공간복잡도와 is_same_tree의 공간복잡도는 독립적인 것 같아서 O(N + M)이라고 봐야 할 것 같아서요 😵‍💫

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 좌변까지는 이해했는데 우변으로 이어지는 논리는 이해하지 못했습니다 @lymchgmk 님 ㅜ 혹시 O(N + M) ~= O(max(N, M)) 이 되는 이유에 대해 설명 부탁드려도 될까요? 제 생각엔, 스택의 공간복잡도와 is_same_tree의 공간복잡도는 독립적인 것 같아서 O(N + M)이라고 봐야 할 것 같아서요 😵‍💫

말씀하신 것 처럼 둘이 독립인게 맞습니다. O(n + m)으로 수정하겠습니다. 감사합니다.

"""
def solve_dfs(self, root: Optional[TreeNode], subRoot: Optional[TreeNode]) -> bool:

def is_same_tree(p: Optional[TreeNode], q: Optional[TreeNode]) -> bool:
if p is None and q is None:
return True
elif (p is not None and q is not None) and (p.val == q.val):
return is_same_tree(p.left, q.left) and is_same_tree(p.right, q.right)
else:
return False

result = False
stack = [root]
while stack:
curr = stack.pop()
if (curr and subRoot) and curr.val == subRoot.val:
result = result or is_same_tree(curr, subRoot)

if curr.left:
stack.append(curr.left)

if curr.right:
stack.append(curr.right)

return result


class _LeetCodeTestCases(TestCase):
def test_1(self):
root = TreeNode(3)
root_1 = TreeNode(4)
root_2 = TreeNode(5)
root.left = root_1
root.right = root_2
root_3 = TreeNode(1)
root_4 = TreeNode(2)
root.left.left = root_3
root.left.right = root_4

subRoot = TreeNode(4)
sub_1 = TreeNode(1)
sub_2 = TreeNode(2)
subRoot.left = sub_1
subRoot.right = sub_2

output = True
self.assertEqual(Solution.isSubtree(Solution(), root, subRoot), output)

def test_2(self):
root = TreeNode(3)
root_1 = TreeNode(4)
root_2 = TreeNode(5)
root.left = root_1
root.right = root_2
root_3 = TreeNode(1)
root_4 = TreeNode(2)
root.left.left = root_3
root.left.right = root_4
root_5 = TreeNode(0)
root_4.left = root_5

subRoot = TreeNode(4)
sub_1 = TreeNode(1)
sub_2 = TreeNode(2)
subRoot.left = sub_1
subRoot.right = sub_2

output = False
self.assertEqual(Solution.isSubtree(Solution(), root, subRoot), output)

def test_3(self):
root = TreeNode(1)
root.right = TreeNode(1)
root.right.right = TreeNode(1)
root.right.right.right = TreeNode(1)
root.right.right.right.right = TreeNode(1)
root.right.right.right.right.right = TreeNode(2)

subRoot = TreeNode(1)
subRoot.right = TreeNode(1)
subRoot.right.right = TreeNode(2)

output = True
self.assertEqual(Solution.isSubtree(Solution(), root, subRoot), output)


if __name__ == '__main__':
main()
63 changes: 63 additions & 0 deletions validate-binary-search-tree/EGON.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from typing import Optional
from unittest import TestCase, main


# 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 isValidBST(self, root: Optional[TreeNode]) -> bool:
return self.solve_dfs(root)

"""
Runtime: 0 ms (Beats 100.00%)
Time Complexity: O(n)
> root 트리의 노드의 갯수를 n이라 하면, 모든 node를 조회하므로 O(n)

Memory: 18.91 MB (Beats 5.70%)
Space Complexity: O(n)
> root 트리의 높이만큼 is_valid_bst 함수가 재귀 호출 스택에 쌓이나, 문제의 제약조건에서 이진트리라고만 했으므로 편향될 수 있으므로, O(n), upper bound
"""
def solve_dfs(self, root: Optional[TreeNode]) -> bool:

def is_valid_bst(node: Optional[TreeNode], lower_bound=float('-inf'), upper_bound=float('inf')):
if not node:
return True

if lower_bound < node.val < upper_bound:
return is_valid_bst(node.left, lower_bound, node.val) and is_valid_bst(node.right, node.val, upper_bound)
else:
return False

return is_valid_bst(root)


class _LeetCodeTestCases(TestCase):
def test_1(self):
root = TreeNode(2)
root.left = TreeNode(1)
root.right = TreeNode(3)

output = True

self.assertEqual(Solution().isValidBST(root), output)

def test_2(self):
root = TreeNode(5)
root.left = TreeNode(4)
root.right = TreeNode(6)
root.right.left = TreeNode(3)
root.right.right = TreeNode(7)

output = False

self.assertEqual(Solution().isValidBST(root), output)


if __name__ == '__main__':
main()