Skip to content

Commit

Permalink
Merge pull request #550 from haklee/main
Browse files Browse the repository at this point in the history
[haklee] week 11
  • Loading branch information
haklee authored Oct 27, 2024
2 parents af10a3a + 12ee5b5 commit 82029fc
Show file tree
Hide file tree
Showing 5 changed files with 438 additions and 0 deletions.
62 changes: 62 additions & 0 deletions binary-tree-maximum-path-sum/haklee.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""TC: O(n), SC: O(h)
아이디어:
- 각 노드를 부모, 혹은 자식 노드의 관점에서 분석할 수 있다.
- 부모 노드의 관점에서 경로를 만들때:
- 부모 노드는 양쪽 자식 노드에 연결된 경로를 잇는 다리 역할을 할 수 있다.
- 이때 자식 노드는
- 경로에 포함되지 않아도 된다. 이 경우 path에 0만큼 기여하는 것으로 볼 수 있다.
- 자식 노드의 두 자식 노드 중 한 쪽의 경로와 부모 노드를 이어주는 역할을 한다.
아래서 좀 더 자세히 설명.
- 자식 노드의 관점에서 경로를 만들때:
- 자식 노드는 부모 노드와 연결될 수 있어야 한다.
- 그렇기 때문에 자신의 자식 노드 중 한 쪽과만 연결되어있을 수 있다. 만약 부모 노드와
본인의 양쪽 자식 노드 모두와 연결되어 있으면 이 노드가 세 갈림길이 되어서 경로를 만들
수 없기 때문.
- 위의 분석을 통해 최대 경로를 만들고 싶다면, 다음의 함수를 root를 기준으로 재귀적으로 실행한다.
- 특정 node가 부모 노드가 되었다고 했을때 본인의 값에 두 자식의 max(최대 경로, 0) 값을 더해서
경로를 만들어본다. 이 값이 기존 solution보다 클 경우 solution을 업데이트.
- 특정 node가 자식 노드가 될 경우 본인의 두 자식 중 더 큰 경로를 부모에 제공해야 한다.
본인의 값에 max(왼쪽 경로, 오른쪽 경로)을 더해서 리턴.
SC:
- solution값을 관리한다. O(1).
- 호출 스택은 트리의 높이만큼 쌓일 수 있다. O(h).
- 종합하면 O(h).
TC:
- 각 노드에서 O(1) 시간이 소요되는 작업 수행.
- 모든 노드에 접근하므로 O(n).
"""


# 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 maxPathSum(self, root: Optional[TreeNode]) -> int:
sol = [-1001] # 노드의 최소값보다 1 작은 값. 현 문제 세팅에서 -inf 역할을 함.

def try_get_best_path(node):
if node is None:
# 노드가 비어있을때 경로 없음. 이때 이 노드로부터 얻을 수 있는 최대 경로 값을
# 0으로 칠 수 있다.
return 0

# 왼쪽, 오른쪽 노드로부터 얻을 수 있는 최대 경로 값.
l = max(try_get_best_path(node.left), 0)
r = max(try_get_best_path(node.right), 0)

# 현 노드를 다리 삼아서 양쪽 자식 노드의 경로를 이었을때 나올 수 있는 경로 값이
# 최대 경로일 수도 있다. 이 값을 현 솔루션과 비교해서 업데이트 해준다.
sol[0] = max(node.val + l + r, sol[0])

# 현 노드의 부모 노드가 `이 노드를 통해 얻을 수 있는 최대 경로 값`으로 사용할 값을 리턴.
return node.val + max(l, r)

try_get_best_path(root)
return sol[0]
160 changes: 160 additions & 0 deletions graph-valid-tree/haklee.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
"""
아이디어:
- 트리여야 하므로 엣지 개수가 n-1개여야 한다.
- 엣지 개수가 n-1개이므로 만약 중간에 사이클이 있다면 트리가 연결이 안 된다.
- 연결이 안 되었으니 트리는 아니고... 몇 조각으로 쪼개진 그래프가 된다. 여튼, valid tree가
아니게 된다.
- spanning tree 만들 때를 생각해보자. 엣지 하나를 더할 때마다 노드가 하나씩 트리에 추가되어야
엣지 n-1개로 노드 n개를 겨우 만들 수 있는데, 중간에 새로운 노드를 추가 안하고 엄한 곳에
엣지를 써서 사이클을 만들거나 하면 모든 노드를 연결할 방법이 없다.
- 위의 예시보다 좀 더 일반적으로는 union-find 알고리즘에서 설명하는 union 시행으로도 설명이
가능하다. union-find에서는 처음에 n개의 노드들의 parent가 자기 자신으로 세팅되어 있는데,
즉, 모든 노드들이 n개의 그룹으로 나뉘어있는데, 여기서 union을 한 번 시행할 때마다 그룹이 1개
혹은 0개 줄어들 수 있다. 그런데 위 문제에서는 union을 엣지 개수 만큼, 즉, n-1회 시행할 수 있으므로,
만약 union 시행에서 그룹의 개수가 줄어들지 않는 경우(즉, 엣지 연결을 통해 사이클이 생길 경우)가
한 번이라도 발생하면 union 시행 후 그룹의 개수가 2 이상이 되어 노드들이 서로 연결되지 않아 트리를
이루지 못한다.
"""

"""TC: O(n * α(n)), SC: O(n)
n은 주어진 노드의 개수, e는 주어진 엣지의 개수.
아이디어(이어서):
- union-find 아이디어를 그대로 활용한다.
- 나이브한 접근:
- union을 통해서 엣지로 연결된 두 집합을 합친다.
- find를 통해서 0번째 노드와 모든 노드들이 같은 집합에 속해있는지 확인한다.
- 더 좋은 구현:
- union 시행 중 같은 집합에 속한 두 노드를 합치려고 하는 것을 발견하면 False 리턴
- union-find는 [Disjoint-set data structure - Wikipedia](https://en.wikipedia.org/wiki/Disjoint-set_data_structure)
를 기반으로 구현했다. 여기에 time complexity 관련 설명이 자세하게 나오는데 궁금하면 참고.
SC:
- union-find에서 쓸 parent 정보만 관리한다. 각 노드마다 parent 노드(인덱스), rank를 관리하므로 O(n).
TC:
- union 과정에 union by rank 적용시 O(α(n)) 만큼의 시간이 든다. 이때 α(n)은 inverse Ackermann function
으로, 매우 느린 속도로 늘어나므로 사실상 상수라고 봐도 무방하다.
- union 시행을 최대 e번 진행하므로 O(e * α(n)).
- e = n-1 이므로 O(n * α(n)).
"""


class Solution:
"""
@param n: An integer
@param edges: a list of undirected edges
@return: true if it's a valid tree, or false
"""

def valid_tree(self, n, edges):
# write your code here

# union find
parent = list(range(n))
rank = [0] * n

def find(x: int) -> bool:
if x == parent[x]:
return x

parent[x] = find(parent[x]) # path-compression
return parent[x]

def union(a: int, b: int) -> bool:
# 원래는 값을 리턴하지 않아도 되지만, 같은 집합에 속한 노드를
# union하려는 상황을 판별하기 위해 값 리턴.

pa = find(a)
pb = find(b)

# union by rank
if pa == pb:
# parent가 같음. rank 작업 안 해도 된다.
return True

if rank[pa] < rank[pb]:
pa, pb = pb, pa

parent[pb] = pa

if rank[pa] == rank[pb]:
rank[pa] += 1

return False

if len(edges) != n - 1:
# 트리에는 엣지가 `(노드 개수) - 1`개 만큼 있다.
# 이 조건 만족 안하면 커팅.
return False

# 나이브한 구현:
# - 모든 엣지로 union 시행
# - find로 모든 노드가 0번 노드와 같은 집합에 속해있는지 확인

# for e in edges:
# union(*e)

# return all(find(0) == find(i) for i in range(n))

# 더 좋은 구현:
# - union 시행 중 같은 집합에 속한 두 노드를 합치려고 하는 것을 발견하면 False 리턴
for e in edges:
if union(*e):
return False

return True


"""TC: O(n), SC: O(n)
n은 주어진 노드의 개수, e는 주어진 엣지의 개수.
아이디어(이어서):
- 트리를 잘 이뤘는지 확인하려면 한 노드에서 시작해서 dfs를 돌려서 모든 노드들에 도달 가능한지
체크하면 되는데, 이게 시간복잡도에 더 유리하지 않을까?
SC:
- adjacency list를 관리한다. O(e).
- 호출 스택은 탐색을 시작하는 노드로부터 사이클이 나오지 않는 경로의 최대 길이만큼 깊어질 수 있다.
최악의 경우 O(n).
- 이때 e = n-1 이므로 종합하면 O(n).
TC:
- 각 노드에 접근하는 과정에 O(1). 이런 노드를 최악의 경우 n개 접근해야 하므로 O(n).
"""


class Solution:
"""
@param n: An integer
@param edges: a list of undirected edges
@return: true if it's a valid tree, or false
"""

def valid_tree(self, n, edges):
# write your code here
if len(edges) != n - 1:
# 트리에는 엣지가 `(노드 개수) - 1`개 만큼 있다.
# 이 조건 만족 안하면 커팅.
return False

adj_list = [[] for _ in range(n)]
for a, b in edges:
adj_list[a].append(b)
adj_list[b].append(a)

visited = [False for _ in range(n)]

def dfs(node):
visited[node] = True
for adj in adj_list[node]:
if not visited[adj]:
dfs(adj)

# 한 노드에서 출발해서 모든 노드가 visted 되어야 주어진 엣지들로 트리를 만들 수 있다.
# 아무 노드에서나 출발해도 되는데 0번째 노드를 선택하자.
dfs(0)

return all(visited)
61 changes: 61 additions & 0 deletions insert-interval/haklee.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""TC: O(n), SC: O(1)
n은 intervals로 주어진 인터벌의 개수.
아이디어:
- 주어진 인터벌들을 앞에서부터 순회하면서 새 인터벌(newInterval)과 겹치는지 보고,
- 겹치면 합친다. 합친 인터벌로 newInterval을 업데이트 한다.
- 안 겹치면 newInterval과 현재 확인 중인 인터벌(curInterval) 중에 필요한 인터벌을
결과 리스트에 넣어주어야 한다.
- 안 겹치면,
- 이때, curInterval이 newInterval보다 앞에 있으면 이후 인터벌들 중 newInterval과 합쳐야 하는
인터벌이 존재할 수 있다. newInterval은 건드리지 않고 curInterval만 결과 리스트에 넣는다.
- curInterval이 newInterval보다 뒤에 있으면 newInterval을 결과 리스트에 더해주고, 그 다음
curInterval도 결과 리스트에 더해주어야 한다.
- 그런데 curInterval이 들어있는 리스트가 정렬되어 있으므로, 이후에 순회할 curInterval
중에는 더 이상 newInterval과 겹칠 인터벌이 없다. newInterval은 이제 더 이상 쓰이지
않으므로 None으로 바꿔준다.
SC:
- newInterval 값만 업데이트 하면서 관리. O(1).
TC:
- intervals에 있는 아이템을 순회하면서 매번 체크하는 시행이 O(1).
- 위의 시행을 intervals에 있는 아이템 수만큼 진행하므로 O(n).
"""


class Solution:
def insert(
self, intervals: List[List[int]], newInterval: List[int]
) -> List[List[int]]:
res = []
for curInterval in intervals:
if newInterval:
# 아직 newInterval이 None으로 변경되지 않았다.
if curInterval[1] < newInterval[0]:
# cur, new가 겹치지 않고, curInterval이 더 앞에 있음.
res.append(curInterval)
elif curInterval[0] > newInterval[1]:
# cur, new가 겹치지 않고, newInterval이 더 앞에 있음.
res.append(newInterval)
res.append(curInterval)
newInterval = None
else:
# 겹치는 부분 존재. newInterval을 확장한다.
newInterval = [
min(curInterval[0], newInterval[0]),
max(curInterval[1], newInterval[1]),
]
else:
# 더 이상 newInterval과 연관된 작업을 하지 않는다. 순회 중인
# curInterval을 결과 리스트에 더하고 끝.
res.append(curInterval)

if newInterval:
# intervals에 있는 마지막 아이템이 newInterval과 겹쳤을 경우 아직
# 결과 리스트에 newInterval이 더해지지 않고 앞선 순회가 종료되었을
# 수 있다. 이 경우 newInterval이 아직 None이 아니므로 리스트에 더해준다.
res.append(newInterval)

return res
29 changes: 29 additions & 0 deletions maximum-depth-of-binary-tree/haklee.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""TC: O(n), SC: O(h)
h는 주어진 트리의 높이, n은 주어진 트리의 노드 개수.
아이디어:
특정 노드의 깊이는 `max(오른쪽 깊이, 왼쪽 깊이) + 1`이다. 이렇게 설명하자니 부모 노드의 깊이 값이
자식의 깊이 값보다 더 큰 것이 이상하긴 한데... 큰 맥락에서 무슨 말을 하고 싶은지는 이해가 가능하다고
본다.
SC:
- 호출 스택은 트리의 높이(...혹은 깊이)만큼 커진다. O(h).
TC:
- 모든 노드를 방문한다. O(n).
"""


# 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 maxDepth(self, root: Optional[TreeNode]) -> int:
def get_depth(node: Optional[TreeNode]) -> int:
return max(get_depth(node.left), get_depth(node.right)) + 1 if node else 0

return get_depth(root)
Loading

0 comments on commit 82029fc

Please sign in to comment.