Diameter of Binary Tree
레벨 순서로 배열 tree에 저장된 이진 트리가 주어집니다. 루트는 인덱스 0에 있고, 인덱스 i의 노드의 자식은 2*i+1(왼쪽)과 2*i+2(오른쪽)에 있으며, -1은 비어 있는 위치를 나타내고, 배열 끝에 추가 -1 항목이 있을 수 있습니다. 트리의 지름, 즉 임의의 두 노드 사이에서 가장 긴 경로의 간선 수를 반환하세요. 경로는 루트를 지날 수도 있고 하나의 하위 트리 안에 머물 수도 있습니다.
함수
- treeinteger-array
- 레벨 순서로 표시한 이진 트리이며, 빈 자리는 -1로 나타냅니다
- 반환값integer
- 두 노드 사이의 최장 경로에 있는 간선의 수
제약 조건
1 ≤ tree.length ≤ 32767- 각
tree[i]는-1이거나0 ≤ tree[i] ≤ 1000을 만족하는 값입니다. tree[0]은 절대-1이 아니므로 트리에는 노드가 하나 이상 있습니다.- 배열은 마지막 노드 뒤에 추가적인
-1항목이 있을 수 있습니다. - 빈 위치의 두 자식도 모두 비어 있고, 깊이는 최대
14입니다.
예제
- 입력
- tree = [8, 3, 6, 1, 4, -1, -1, -1, -1, 7]
- 출력
- 4
- 설명
- 경로
7,4,3,8,6(인덱스9,4,1,0,2)에는 네 개의 간선으로 연결된 다섯 개의 노드가 있습니다. 루트에서 방향이 바뀝니다. 왼쪽으로 세 개의 간선이 내려가고 오른쪽으로 하나가 내려갑니다.
- 입력
- tree = [2, 5, -1, 1, 9, -1, -1, 3, -1, -1, 4]
- 출력
- 4
- 설명
- 경로
3,1,5,9,4에는 간선이 네 개 있으며 인덱스1의5에서 방향을 바꿉니다. 루트에는 오른쪽 자식이 없으므로 루트를 지나는 경로는 왼쪽으로 내려가는 간선 세 개만 포함합니다.
- 입력
- tree = [6, -1, -1]
- 출력
- 0
- 설명
- 노드 하나에는 간선이 없습니다. 가장 긴 경로는 노드 하나만으로 이루어지며, 길이는
0입니다.
제출 시 숨은 테스트 +12개
후속 질문
경로 자체, 즉 지름의 한쪽 끝에서 다른 쪽 끝까지의 노드 값을 어떻게 반환할 수 있을까요?
힌트
하나씩 열어 보세요. 열 때마다 조금 더 알려 줍니다.
트리의 모든 경로에는 가장 높은 노드가 하나 있으며, 그 지점에서 위로 가던 방향이 아래로 바뀝니다. 그 노드를 알고 있다면, 그 노드를 지나는 경로는 얼마나 길 수 있을까요?
노드
i에서 꺾이는 경로는 왼쪽 하위 트리로 내려간 다음 오른쪽 하위 트리로 내려갑니다. 최선의 경우 이 경로의 길이는 왼쪽 자식의 높이와 오른쪽 자식의 높이를 더한 값입니다. 여기서 높이는 가장 긴 하향 경로에 있는 노드의 수이며, 비어 있는 위치의 높이는0입니다.한 번의 후위 순회에서 아래쪽부터 높이를 계산합니다. 노드의 높이는
1 + max(left, right)입니다. 노드에서left와right를 가지고 있는 동안left + right로 정답을 갱신합니다.
풀이
가장 긴 경로가 루트를 지날 필요는 없으므로, 루트의 양쪽을 측정하는 것만으로는 충분하지 않습니다. 모든 경로에는 가장 높은 노드가 하나 있으며, 그 지점에서 위로 가다가 아래로 방향을 바꿉니다. 노드에서 방향을 바꾸는 가장 긴 경로의 길이는 해당 노드의 왼쪽 높이와 오른쪽 높이를 더한 값입니다. 후위 순회 한 번으로 아래에서 위로 모든 높이를 계산하고, 그 과정에서 모든 방향 전환 지점을 확인하므로 O(n)에 수행됩니다.
모든 노드 쌍을 측정하기
정답이지만 가장 큰 테스트에서는 끝나지 않음
핵심 아이디어
먼저 배열에서 이동하는 방법을 살펴보겠습니다. 인덱스 i에 있는 노드의 왼쪽 자식은 2*i+1에, 오른쪽 자식은 2*i+2에 있으므로 부모는 (i-1)/2에 있으며, 소수점 이하는 버립니다. 위치가 유효하려면 인덱스가 배열 범위 안에 있고 해당 위치의 값이 -1이 아니어야 합니다. [8, 3, 6, 1, 4, -1, -1, -1, -1, 7]에서 인덱스 9의 7은 인덱스 4에 있는 부모를 가지며, 그 4의 부모는 인덱스 1에 있습니다.
지름은 두 노드 사이의 가장 긴 거리이므로 모든 쌍을 측정하면 됩니다. 인덱스 a와 b 사이의 거리를 구하려면, 더 큰 인덱스에서 시작해 두 인덱스가 만날 때까지 한 번에 한 단계씩 루트 쪽으로 올라갑니다. 더 큰 인덱스가 더 높은 레벨에 있을 수는 없으므로, 이 단계에서 만나는 지점을 지나칠 일은 없습니다. 단계 수가 간선의 수입니다. 9와 2의 경우, 9는 4로 올라간 다음 1로 올라가고, 2는 0으로 올라가며, 1은 0으로 올라갑니다. 네 단계입니다.
이 방법은 정확하지만 느립니다. 가장 큰 테스트는 노드가 16383개인 완전 트리로, 약 1.3 × 10^8개의 쌍이 생기며 각 쌍을 처리하는 데 최대 26단계가 걸립니다. 하나의 답을 구하는 데 수십억 단계가 필요하므로 시간 제한을 훨씬 초과합니다.
알고리즘
- 모든 실제 노드의 인덱스를 모읍니다.
- 각 쌍
(a, b)에 대해edges = 0으로 설정하고a == b가 될 때까지 반복합니다. 더 큰 인덱스를 부모로 바꾸고edges에1을 더합니다. - 확인한
edges중 가장 큰 값을 유지하고 반환합니다.
def diameterOfBinaryTree(tree):
nodes = [i for i, value in enumerate(tree) if value != -1]
best = 0
for x in range(len(nodes)):
for y in range(x + 1, len(nodes)):
a, b = nodes[x], nodes[y]
edges = 0
while a != b:
# A larger index is never higher up, so climb from it.
if a > b:
a = (a - 1) // 2
else:
b = (b - 1) // 2
edges += 1
best = max(best, edges)
return best각 노드에서 두 높이를 모두 측정하세요
핵심 아이디어
height(c)가 c에서 시작하는 가장 긴 하향 경로의 노드 수를 세도록 하고, 빈 위치의 높이는 0으로 둡니다. 그러면 노드 i에서 꺾이는 최장 경로의 간선 수는 height(2*i+1) + height(2*i+2)입니다. 각 노드가 해당 경로에 간선 하나씩을 더하기 때문입니다.
따라서 모든 노드를 꺾이는 지점으로 시도하고 최댓값을 유지하세요. 두 번째 예시에서 인덱스 1의 5는 왼쪽 높이가 2 (1, 3)이고 오른쪽 높이도 2 (9, 4)이므로 간선이 네 개인 경로가 됩니다. 루트는 왼쪽 높이가 3이고 오른쪽 높이가 0이므로 간선이 세 개뿐입니다.
height를 호출할 때마다 하위 트리 전체를 순회하고, 각 노드는 위쪽의 모든 조상에 대해 다시 순회되므로 작업량은 O(n·h)입니다. 여기서는 h ≤ 14이므로 충분히 빠르지만, 포인터 트리가 사슬 형태라면 h가 n까지 커질 수 있어 같은 방식의 비용은 O(n²)이 됩니다. 반복되는 height 호출이 바로 마지막 접근법에서 제거하는 낭비입니다.
알고리즘
height(i)를 작성합니다. 빈 위치라면0을, 그렇지 않으면1 + max(height(2*i+1), height(2*i+2))를 반환합니다.- 모든 실제 노드
i에 대해height(2*i+1) + height(2*i+2)를 계산합니다. - 이 합들 중 가장 큰 값을 반환합니다.
def diameterOfBinaryTree(tree):
n = len(tree)
def height(i):
# Nodes on the longest downward path from i; an empty spot has 0.
if i >= n or tree[i] == -1:
return 0
return 1 + max(height(2 * i + 1), height(2 * i + 2))
best = 0
for i in range(n):
if tree[i] != -1:
# The longest path that turns at node i goes down both sides.
best = max(best, height(2 * i + 1) + height(2 * i + 2))
return best높이를 한 번 후위 순회하기
핵심 아이디어
노드의 높이는 두 자식의 높이에만 달려 있으며, 이 두 값은 꺾이는 지점을 확인하는 데 필요한 값이기도 합니다. 따라서 아래에서 위로 올라가며 한 번만 계산합니다. 후위 순회는 부모 노드보다 두 자식 노드를 먼저 처리합니다. 그러면 각 노드에서 left와 right를 얻을 수 있습니다. left + right로 답을 갱신하고, 1 + max(left, right)를 부모 노드에 전달합니다.
첫 번째 예제에서 리프 노드 7은 1을 반환하고, 그 위의 4는 2를 반환하며, 3은 다른 자식 1의 높이가 1이므로 3을 반환합니다. 6은 1을 반환합니다. 루트에서 left + right = 3 + 1 = 4가 답입니다. 다른 노드가 낼 수 있는 최선은 3이며, 1 + 2 = 3입니다.
각 노드는 한 번씩 방문하므로 시간 복잡도는 O(n)이고, 재귀 깊이는 트리의 높이만큼인 O(h)이며, 대략 레벨마다 프레임 하나가 사용됩니다. 호출이 반환하는 값(높이)은 마지막에 필요한 값(경로 길이)과 다르므로 답은 재귀 바깥의 변수에 저장합니다.
알고리즘
best = 0으로 설정하고height(i)를 작성합니다. 비어 있는 위치라면0을 반환합니다.left = height(2*i+1)과right = height(2*i+2)를 계산합니다.best와left + right중 더 큰 값으로best를 설정합니다.1 + max(left, right)를 반환합니다.height(0)을 호출하고best를 반환합니다.
def diameterOfBinaryTree(tree):
n = len(tree)
best = 0
def height(i):
# Returns the height of node i and updates best on the way back up.
nonlocal best
if i >= n or tree[i] == -1:
return 0
left = height(2 * i + 1)
right = height(2 * i + 2)
best = max(best, left + right) # the longest path that turns at node i
return 1 + max(left, right)
height(0)
return best
함정과 경계 사례
대부분의 오답은 잘못된 대상을 세거나 잘못된 노드에서 측정합니다.
- 간선이 아니라 노드를 세는 경우.
7,4,3,8,6으로 이루어진 경로에는 노드가 다섯 개 있고 길이는4이며, 노드가 하나뿐인 경우 지름은0입니다. - 루트를 지나는 경로만 측정하는 경우. 두 번째 예제에서 루트를 지나는 최적 경로의 간선은 세 개이고, 답은 네 개이며
1번 인덱스에서 방향을 바꿉니다. - 재귀 호출에서 지름을 반환하는 경우. 부모 노드는 더 긴 경로를 만들기 위해 자식들의 높이가 필요하므로, 지름은 별도의 변수에 저장해야 합니다.
- 두 가지 높이 정의를 섞어 쓰는 경우. 노드 수를 세는 높이 정의에서 빈 위치의 높이를
0으로 두면left + right가 이미 간선 수입니다. 간선 수를 세는 높이 정의에서는 빈 위치의 높이를-1로 두고left + right + 2를 사용해야 합니다. 두 정의를 반씩 섞어 쓰면 결과가 1 또는 2만큼 어긋납니다. - 끝을 넘어 읽는 경우. 배열의 끝부분에 있는 리프의 자식 인덱스가 마지막 항목을 넘어설 수 있습니다. 끝을 넘어서는 인덱스는 빈 위치로 처리하세요.
- 배열이 1부터 시작하는 Lua와 R에서 오프셋을 혼동하는 경우.
2*i+1계산을 위해 노드 인덱스는 0부터 시작하도록 유지하고tree[i + 1]을 읽으세요.
자주 묻는 질문4
이진 트리의 지름의 시간 복잡도는 얼마인가요?
후위 순회 방식의 해결법은 각 노드를 한 번씩 방문하므로, O(n) 시간이 걸리며 재귀에 O(h)의 추가 공간이 필요합니다. 여기서 h는 높이입니다. 각 노드에서 높이를 따로 계산하면 O(n·h)의 비용이 들며, 트리가 사슬 모양이면 O(n²)이 됩니다.
이진 트리의 지름은 항상 루트를 지날까요?
아니요. 가장 긴 경로는 하나의 하위 트리 안에 완전히 있을 수 있습니다. 예를 들어 루트의 한쪽에는 짧은 가지가 있고 다른 쪽에는 깊고 가지가 많은 하위 트리가 있는 경우입니다. 그래서 루트에서만이 아니라 모든 노드에서 left + right를 확인합니다.
지름은 노드 수로 계산하나요, 아니면 간선 수로 계산하나요?
여기서는 경로상의 연속된 노드 사이의 연결인 간선 수로 세므로, 노드 하나의 지름은 0이고 연결된 노드 두 개의 지름은 1입니다. 일부 책에서는 노드 수로 세기 때문에 값이 하나 더 커집니다. 1을 더하거나 빼기 전에 문제가 어떤 방식을 요구하는지 확인하세요.
재귀 없이 이진 트리의 지름을 어떻게 구하나요?
모든 자식 노드가 부모 노드보다 먼저 오도록 노드를 방문하세요. 한 가지 방법은 루트를 스택에 넣고, 노드를 꺼내 목록에 추가하면서 자식 노드를 스택에 넣은 다음, 목록을 역순으로 순회하는 것입니다. 각 노드의 높이를 배열에 저장하고, 각 노드에서 두 자식의 높이를 읽은 뒤 그 합으로 답을 갱신하세요. 시간 복잡도는 O(n)으로 유지됩니다.
비슷한 문제
같은 아이디어를 쓰는 문제입니다. 두세 개를 풀면 패턴이 몸에 익습니다.
Python
def diameterOfBinaryTree(tree):
# 여기에 코드를 작성하세요케이스 1
케이스 2
케이스 3
입력
tree = [8, 3, 6, 1, 4, -1, -1, -1, -1, 7]
기대값
4