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имеет четыре ребра и поворачивает у5с индексом1. У корня нет правого дочернего узла, поэтому путь через корень включает только три ребра вниз по его левой стороне.
- Ввод
- tree = [6, -1, -1]
- Вывод
- 0
- Пояснение
- У отдельного узла нет рёбер. Самый длинный путь состоит только из этого узла и имеет длину
0.
+12 скрытых тестов при отправке
Дополнительный вопрос
Как вернуть сам путь — значения узлов от одного конца диаметра до другого?
Подсказки
Открывайте по одной. Каждая подсказывает чуть больше.
У каждого пути в дереве есть одна самая высокая вершина — место, где путь перестаёт идти вверх и начинает идти вниз. Если бы вы знали эту вершину, какой длины мог бы быть путь, проходящий через неё?
Путь, который поворачивает в узле
i, проходит вниз по левому поддереву и вниз по правому. В лучшем случае его длина равна высоте левого потомка плюс высота правого потомка, где высота — это количество узлов на самом длинном нисходящем пути, а пустое место имеет высоту0.Вычислите высоты снизу вверх за один обход в порядке post-order: высота узла равна
1 + max(left, right). Когда у вас есть значенияleftиrightдля узла, обновите ответ значениемleft + right.
Решение
Самый длинный путь не обязательно проходит через корень, поэтому измерить две стороны корня недостаточно. У каждого пути есть один самый верхний узел, где он меняет направление с движения вверх на движение вниз, а самый длинный путь, проходящий через узел, равен сумме высот его левого и правого поддеревьев. Один проход в порядке post-order вычисляет все высоты снизу вверх и проверяет каждую точку поворота по пути за O(n).
Измерьте каждую пару узлов
Верно, но не успевает на самых больших тестах
Идея
Сначала разберёмся, как перемещаться по массиву. У узла с индексом i левый потомок находится по индексу 2*i+1, а правый — по индексу 2*i+2, поэтому его родитель находится по индексу (i-1)/2 с округлением вниз. Место считается занятым, только если его индекс находится в пределах массива, а значение в нём не равно -1. В массиве [8, 3, 6, 1, 4, -1, -1, -1, -1, 7] у узла 7 с индексом 9 родитель находится по индексу 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: заменяйте больший индекс его родителем и прибавляйте1кedges. - Сохраните наибольшее значение
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) рёбер — по одному ребру на каждую из этих вершин.
Поэтому попробуй каждую вершину в качестве точки поворота и сохрани лучший результат. Во втором примере вершина 5 с индексом 1 имеет высоту 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Один обход высот в порядке post-order
Идея
Высота узла зависит только от высот двух его потомков, и именно эти два числа нужны для проверки точки поворота. Поэтому вычислите их один раз, снизу вверх. При обходе в постфиксном порядке оба потомка обрабатываются раньше родителя. Затем в каждом узле у вас есть значения left и right: обновите ответ значением left + right, а родителю передайте 1 + max(left, right).
В первом примере лист 7 возвращает 1, расположенный выше него узел 4 возвращает 2, а узел 3 возвращает 3, поскольку его другой потомок 1 имеет высоту 1. Узел 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большее изbestиleft + right. - Верни
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. Сочетание этих соглашений приводит к ошибке на единицу или два. - Выходят за конец массива. Индексы дочерних узлов листа вблизи конца массива могут оказаться за последним элементом. Считайте индекс за концом массива пустым местом.
- Путают смещение в Lua и R, где массивы начинаются с 1. Оставляйте индексы узлов с отсчётом от 0 для вычисления
2*i+1, а считывайте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