-
Notifications
You must be signed in to change notification settings - Fork 126
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
Changes from 5 commits
90af76f
5874cd1
aced264
a0d9ce0
8443c85
2fd0998
e02dee1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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() |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저는 이 부분의 시간복잡도가 좀 아깝게 느껴졌습니다 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() |
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() |
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)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 제가 좌변까지는 이해했는데 우변으로 이어지는 논리는 이해하지 못했습니다 @lymchgmk 님 ㅜ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
말씀하신 것 처럼 둘이 독립인게 맞습니다. 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() |
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() |
There was a problem hiding this comment.
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차원까지 낮출 수 있을 것 같아요 :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이전에 이 문제를 풀었던 적이 있어서 기존 모범 답안을 그대로 제출했는데, 실행 환경에 따른 GC 호출 차이 때문인지 문자열 슬라이싱을 미리해서 리턴하면 훨씬 메모리가 좋더라고요. 시간복잡도는 투 포인터로 풀면 나아지는건 맞긴한데, 애초에 팰린드롬 문제 자체의 제약사항이 1000자 아래여서 별 차이가 없는 걸 알고있긴 했습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@obzva manacher 알고리즘도 이번에 알게되어서 추가해봤는데 한 번 보시면 도움되실 것 같아 남깁니다 :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
그렇군요 ㅎㅎㅎ 그런 이유라면 에곤님 본래의 풀이가 더 합리적인 선택 같습니다