Skip to content

Commit 73b2fc1

Browse files
authored
Merge branch 'DaleStudy:main' into main
2 parents c6d6330 + 212bfe6 commit 73b2fc1

File tree

16 files changed

+855
-0
lines changed

16 files changed

+855
-0
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// TC: O(n)
2+
// visit all nodes to find maximum path
3+
// SC: O(h)
4+
// h means the high of binary tree
5+
class Solution {
6+
private int output = Integer.MIN_VALUE;
7+
public int maxPathSum(TreeNode root) {
8+
max(root);
9+
return output;
10+
}
11+
12+
private int max(TreeNode node) {
13+
if (node == null) return 0;
14+
15+
int leftSum = Math.max(max(node.left), 0);
16+
int rightSum = Math.max(max(node.right), 0);
17+
18+
int currentMax = node.val + leftSum + rightSum;
19+
20+
output = Math.max(output, currentMax);
21+
22+
return node.val + Math.max(leftSum, rightSum);
23+
}
24+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""TC: O(n), SC: O(h)
2+
3+
4+
아이디어:
5+
- 각 노드를 부모, 혹은 자식 노드의 관점에서 분석할 수 있다.
6+
- 부모 노드의 관점에서 경로를 만들때:
7+
- 부모 노드는 양쪽 자식 노드에 연결된 경로를 잇는 다리 역할을 할 수 있다.
8+
- 이때 자식 노드는
9+
- 경로에 포함되지 않아도 된다. 이 경우 path에 0만큼 기여하는 것으로 볼 수 있다.
10+
- 자식 노드의 두 자식 노드 중 한 쪽의 경로와 부모 노드를 이어주는 역할을 한다.
11+
아래서 좀 더 자세히 설명.
12+
- 자식 노드의 관점에서 경로를 만들때:
13+
- 자식 노드는 부모 노드와 연결될 수 있어야 한다.
14+
- 그렇기 때문에 자신의 자식 노드 중 한 쪽과만 연결되어있을 수 있다. 만약 부모 노드와
15+
본인의 양쪽 자식 노드 모두와 연결되어 있으면 이 노드가 세 갈림길이 되어서 경로를 만들
16+
수 없기 때문.
17+
- 위의 분석을 통해 최대 경로를 만들고 싶다면, 다음의 함수를 root를 기준으로 재귀적으로 실행한다.
18+
- 특정 node가 부모 노드가 되었다고 했을때 본인의 값에 두 자식의 max(최대 경로, 0) 값을 더해서
19+
경로를 만들어본다. 이 값이 기존 solution보다 클 경우 solution을 업데이트.
20+
- 특정 node가 자식 노드가 될 경우 본인의 두 자식 중 더 큰 경로를 부모에 제공해야 한다.
21+
본인의 값에 max(왼쪽 경로, 오른쪽 경로)을 더해서 리턴.
22+
23+
SC:
24+
- solution값을 관리한다. O(1).
25+
- 호출 스택은 트리의 높이만큼 쌓일 수 있다. O(h).
26+
- 종합하면 O(h).
27+
28+
TC:
29+
- 각 노드에서 O(1) 시간이 소요되는 작업 수행.
30+
- 모든 노드에 접근하므로 O(n).
31+
"""
32+
33+
34+
# Definition for a binary tree node.
35+
# class TreeNode:
36+
# def __init__(self, val=0, left=None, right=None):
37+
# self.val = val
38+
# self.left = left
39+
# self.right = right
40+
class Solution:
41+
def maxPathSum(self, root: Optional[TreeNode]) -> int:
42+
sol = [-1001] # 노드의 최소값보다 1 작은 값. 현 문제 세팅에서 -inf 역할을 함.
43+
44+
def try_get_best_path(node):
45+
if node is None:
46+
# 노드가 비어있을때 경로 없음. 이때 이 노드로부터 얻을 수 있는 최대 경로 값을
47+
# 0으로 칠 수 있다.
48+
return 0
49+
50+
# 왼쪽, 오른쪽 노드로부터 얻을 수 있는 최대 경로 값.
51+
l = max(try_get_best_path(node.left), 0)
52+
r = max(try_get_best_path(node.right), 0)
53+
54+
# 현 노드를 다리 삼아서 양쪽 자식 노드의 경로를 이었을때 나올 수 있는 경로 값이
55+
# 최대 경로일 수도 있다. 이 값을 현 솔루션과 비교해서 업데이트 해준다.
56+
sol[0] = max(node.val + l + r, sol[0])
57+
58+
# 현 노드의 부모 노드가 `이 노드를 통해 얻을 수 있는 최대 경로 값`으로 사용할 값을 리턴.
59+
return node.val + max(l, r)
60+
61+
try_get_best_path(root)
62+
return sol[0]
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package leetcode_study
2+
3+
import io.kotest.matchers.shouldBe
4+
import org.junit.jupiter.api.Test
5+
import kotlin.math.max
6+
7+
class `binary-tree-maximum-path-sum` {
8+
9+
/**
10+
* TC: O(n), SC: O(log n)
11+
*/
12+
fun maxPathSum(root: TreeNode?): Int {
13+
if (root == null) return 0
14+
var max = root.`val` // 부모 노드와 2개의 자식 노드의 합을 전역 변수로 갱신한다.
15+
16+
fun dfs(node: TreeNode?): Int {
17+
if (node == null) return 0
18+
19+
val left = max(dfs(node.left), 0)
20+
val right = max(dfs(node.right), 0)
21+
22+
max = max(node.`val` + left + right, max)
23+
return node.`val` + max(left, right) // 현재 노드와 2개의 자식 노드 중 최대의 값을 반환한다.
24+
}
25+
26+
dfs(root)
27+
return max
28+
}
29+
30+
@Test
31+
fun `이진 트리의 최대 경로 합을 반환한다`() {
32+
maxPathSum(TreeNode.of(-10,9,20,null,null,15,7)) shouldBe 42
33+
maxPathSum(TreeNode.of(1,9,20,null,null,15,7)) shouldBe 45
34+
}
35+
}

graph-valid-tree/TonyKim9401.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// TC: O(n * m)
2+
// n = the length of edges, m = the length of each element list of edges
3+
// SC: O(n)
4+
// n = recursively visit everywhere
5+
public class Solution {
6+
public boolean validTree(int n, int[][] edges) {
7+
if (edges.length != n - 1) return false;
8+
9+
List<List<Integer>> graph = new ArrayList<>();
10+
for (int i = 0; i < n; i++) {
11+
graph.add(new ArrayList<>());
12+
}
13+
14+
for (int[] edge : edges) {
15+
graph.get(edge[0]).add(edge[1]);
16+
graph.get(edge[1]).add(edge[0]);
17+
}
18+
19+
boolean[] visited = new boolean[n];
20+
if (!dfs(0, -1, graph, visited)) return false;
21+
22+
for (boolean v : visited) if(!v) return false;
23+
24+
return true;
25+
}
26+
27+
private boolean dfs(int node, int parent, List<List<Integer>> graph, boolean[] visited) {
28+
visited[node] = true;
29+
30+
for (int neighbor : graph.get(node)) {
31+
if (neighbor == parent) continue;
32+
if (visited[neighbor]) return false;
33+
if (!dfs(neighbor, node, graph, visited)) return false;
34+
}
35+
return true;
36+
}
37+
}

graph-valid-tree/haklee.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
"""
2+
아이디어:
3+
- 트리여야 하므로 엣지 개수가 n-1개여야 한다.
4+
- 엣지 개수가 n-1개이므로 만약 중간에 사이클이 있다면 트리가 연결이 안 된다.
5+
- 연결이 안 되었으니 트리는 아니고... 몇 조각으로 쪼개진 그래프가 된다. 여튼, valid tree가
6+
아니게 된다.
7+
- spanning tree 만들 때를 생각해보자. 엣지 하나를 더할 때마다 노드가 하나씩 트리에 추가되어야
8+
엣지 n-1개로 노드 n개를 겨우 만들 수 있는데, 중간에 새로운 노드를 추가 안하고 엄한 곳에
9+
엣지를 써서 사이클을 만들거나 하면 모든 노드를 연결할 방법이 없다.
10+
- 위의 예시보다 좀 더 일반적으로는 union-find 알고리즘에서 설명하는 union 시행으로도 설명이
11+
가능하다. union-find에서는 처음에 n개의 노드들의 parent가 자기 자신으로 세팅되어 있는데,
12+
즉, 모든 노드들이 n개의 그룹으로 나뉘어있는데, 여기서 union을 한 번 시행할 때마다 그룹이 1개
13+
혹은 0개 줄어들 수 있다. 그런데 위 문제에서는 union을 엣지 개수 만큼, 즉, n-1회 시행할 수 있으므로,
14+
만약 union 시행에서 그룹의 개수가 줄어들지 않는 경우(즉, 엣지 연결을 통해 사이클이 생길 경우)가
15+
한 번이라도 발생하면 union 시행 후 그룹의 개수가 2 이상이 되어 노드들이 서로 연결되지 않아 트리를
16+
이루지 못한다.
17+
"""
18+
19+
"""TC: O(n * α(n)), SC: O(n)
20+
21+
n은 주어진 노드의 개수, e는 주어진 엣지의 개수.
22+
23+
아이디어(이어서):
24+
- union-find 아이디어를 그대로 활용한다.
25+
- 나이브한 접근:
26+
- union을 통해서 엣지로 연결된 두 집합을 합친다.
27+
- find를 통해서 0번째 노드와 모든 노드들이 같은 집합에 속해있는지 확인한다.
28+
- 더 좋은 구현:
29+
- union 시행 중 같은 집합에 속한 두 노드를 합치려고 하는 것을 발견하면 False 리턴
30+
- union-find는 [Disjoint-set data structure - Wikipedia](https://en.wikipedia.org/wiki/Disjoint-set_data_structure)
31+
를 기반으로 구현했다. 여기에 time complexity 관련 설명이 자세하게 나오는데 궁금하면 참고.
32+
33+
SC:
34+
- union-find에서 쓸 parent 정보만 관리한다. 각 노드마다 parent 노드(인덱스), rank를 관리하므로 O(n).
35+
36+
TC:
37+
- union 과정에 union by rank 적용시 O(α(n)) 만큼의 시간이 든다. 이때 α(n)은 inverse Ackermann function
38+
으로, 매우 느린 속도로 늘어나므로 사실상 상수라고 봐도 무방하다.
39+
- union 시행을 최대 e번 진행하므로 O(e * α(n)).
40+
- e = n-1 이므로 O(n * α(n)).
41+
"""
42+
43+
44+
class Solution:
45+
"""
46+
@param n: An integer
47+
@param edges: a list of undirected edges
48+
@return: true if it's a valid tree, or false
49+
"""
50+
51+
def valid_tree(self, n, edges):
52+
# write your code here
53+
54+
# union find
55+
parent = list(range(n))
56+
rank = [0] * n
57+
58+
def find(x: int) -> bool:
59+
if x == parent[x]:
60+
return x
61+
62+
parent[x] = find(parent[x]) # path-compression
63+
return parent[x]
64+
65+
def union(a: int, b: int) -> bool:
66+
# 원래는 값을 리턴하지 않아도 되지만, 같은 집합에 속한 노드를
67+
# union하려는 상황을 판별하기 위해 값 리턴.
68+
69+
pa = find(a)
70+
pb = find(b)
71+
72+
# union by rank
73+
if pa == pb:
74+
# parent가 같음. rank 작업 안 해도 된다.
75+
return True
76+
77+
if rank[pa] < rank[pb]:
78+
pa, pb = pb, pa
79+
80+
parent[pb] = pa
81+
82+
if rank[pa] == rank[pb]:
83+
rank[pa] += 1
84+
85+
return False
86+
87+
if len(edges) != n - 1:
88+
# 트리에는 엣지가 `(노드 개수) - 1`개 만큼 있다.
89+
# 이 조건 만족 안하면 커팅.
90+
return False
91+
92+
# 나이브한 구현:
93+
# - 모든 엣지로 union 시행
94+
# - find로 모든 노드가 0번 노드와 같은 집합에 속해있는지 확인
95+
96+
# for e in edges:
97+
# union(*e)
98+
99+
# return all(find(0) == find(i) for i in range(n))
100+
101+
# 더 좋은 구현:
102+
# - union 시행 중 같은 집합에 속한 두 노드를 합치려고 하는 것을 발견하면 False 리턴
103+
for e in edges:
104+
if union(*e):
105+
return False
106+
107+
return True
108+
109+
110+
"""TC: O(n), SC: O(n)
111+
112+
n은 주어진 노드의 개수, e는 주어진 엣지의 개수.
113+
114+
아이디어(이어서):
115+
- 트리를 잘 이뤘는지 확인하려면 한 노드에서 시작해서 dfs를 돌려서 모든 노드들에 도달 가능한지
116+
체크하면 되는데, 이게 시간복잡도에 더 유리하지 않을까?
117+
118+
SC:
119+
- adjacency list를 관리한다. O(e).
120+
- 호출 스택은 탐색을 시작하는 노드로부터 사이클이 나오지 않는 경로의 최대 길이만큼 깊어질 수 있다.
121+
최악의 경우 O(n).
122+
- 이때 e = n-1 이므로 종합하면 O(n).
123+
124+
TC:
125+
- 각 노드에 접근하는 과정에 O(1). 이런 노드를 최악의 경우 n개 접근해야 하므로 O(n).
126+
"""
127+
128+
129+
class Solution:
130+
"""
131+
@param n: An integer
132+
@param edges: a list of undirected edges
133+
@return: true if it's a valid tree, or false
134+
"""
135+
136+
def valid_tree(self, n, edges):
137+
# write your code here
138+
if len(edges) != n - 1:
139+
# 트리에는 엣지가 `(노드 개수) - 1`개 만큼 있다.
140+
# 이 조건 만족 안하면 커팅.
141+
return False
142+
143+
adj_list = [[] for _ in range(n)]
144+
for a, b in edges:
145+
adj_list[a].append(b)
146+
adj_list[b].append(a)
147+
148+
visited = [False for _ in range(n)]
149+
150+
def dfs(node):
151+
visited[node] = True
152+
for adj in adj_list[node]:
153+
if not visited[adj]:
154+
dfs(adj)
155+
156+
# 한 노드에서 출발해서 모든 노드가 visted 되어야 주어진 엣지들로 트리를 만들 수 있다.
157+
# 아무 노드에서나 출발해도 되는데 0번째 노드를 선택하자.
158+
dfs(0)
159+
160+
return all(visited)

graph-valid-tree/jdalma.kt

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package leetcode_study
2+
3+
import io.kotest.matchers.shouldBe
4+
import org.junit.jupiter.api.Test
5+
6+
class `graph-valid-tree` {
7+
8+
/**
9+
* TC: O(n), SC: O(n)
10+
*/
11+
fun validTree(nodeSize: Int, edges: Array<IntArray>): Boolean {
12+
if (nodeSize - 1 != edges.size) {
13+
return false
14+
}
15+
16+
val visited = mutableSetOf<Int>()
17+
val adj = List(nodeSize) { mutableListOf<Int>() }
18+
for (e in edges) {
19+
adj[e[0]].add(e[1])
20+
adj[e[1]].add(e[0])
21+
}
22+
23+
val queue = ArrayDeque<Int>().apply {
24+
this.add(0)
25+
}
26+
27+
while (queue.isNotEmpty()) {
28+
val now = queue.removeFirst()
29+
visited.add(now)
30+
for (next in adj[now]) {
31+
if (!visited.contains(next)) {
32+
queue.add(next)
33+
}
34+
}
35+
}
36+
37+
return nodeSize == visited.size
38+
}
39+
40+
@Test
41+
fun `노드가 트리의 조건을 만족하는지 여부를 반환한다`() {
42+
validTree(5,
43+
arrayOf(
44+
intArrayOf(0,1),
45+
intArrayOf(0,2),
46+
intArrayOf(0,3),
47+
intArrayOf(1,4)
48+
)
49+
) shouldBe true
50+
51+
validTree(5,
52+
arrayOf(
53+
intArrayOf(0,1),
54+
intArrayOf(0,2),
55+
intArrayOf(0,3),
56+
intArrayOf(1,4),
57+
intArrayOf(2,3),
58+
)
59+
) shouldBe false
60+
}
61+
}

0 commit comments

Comments
 (0)