Jump Game
Ты находишься на индексе 0 массива nums. С индекса i можно прыгнуть вперёд на любое количество шагов от 1 до nums[i] включительно, поэтому nums[i] — это длина самого длинного прыжка отсюда, а 0 означает, что двигаться нельзя. Верни true, если какая-либо последовательность прыжков приводит к последнему индексу, и false в противном случае.
Функция
- numsinteger-array
- самый длинный прыжок, который можно сделать из каждого индекса
- Возвращаетboolean
- true, если вы можете попасть на последний индекс, начиная с индекса 0, иначе false
Ограничения
1 ≤ nums.length ≤ 1040 ≤ nums[i] ≤ 105- Прыжок может быть короче, чем
nums[i], поэтому длинный прыжок никогда не заставит вас перескочить через последний индекс.
Примеры
- Ввод
- nums = [2, 0, 3, 1, 0, 2]
- Вывод
- true
- Пояснение
- Из индекса 0 можно перейти к индексу 1 или 2. В индексе 1 находится 0, и это тупик, а в индексе 2 находится 3, откуда можно перейти к индексу 5 — последнему индексу.
- Ввод
- nums = [1, 3, 0, 0, 0, 2]
- Вывод
- false
- Пояснение
- Индекс 0 может перейти только к индексу 1, а индекс 1 достигает максимум индекса 4. Индексы 2, 3 и 4 содержат 0, поэтому ничто никогда не проходит дальше индекса 4 к индексу 5.
- Ввод
- nums = [0]
- Вывод
- true
- Пояснение
- В массиве один элемент, поэтому ты начинаешь с последнего индекса, и переход не нужен.
+18 скрытых тестов при отправке
Дополнительный вопрос
Подсчитайте количество различных последовательностей прыжков, которые приводят на последний индекс, по модулю 10^9+7, всё ещё за время O(n).
Подсказки
Открывайте по одной. Каждая подсказывает чуть больше.
0становится ловушкой только тогда, когда ничто перед ним не может перепрыгнуть через него. Что нужно знать об индексах перед ним, чтобы это определить?Если вы можете достичь индекса
i, то можете достичь каждого индекса отiдоi+nums[i], потому что разрешены более короткие прыжки. Поэтому достижимые индексы всегда образуют один непрерывный блок, начинающийся с индекса 0.Иди слева направо и храни в
farthestправый конец этого блока. Если текущий индекс находится дальшеfarthest, до него невозможно добраться. Иначе увеличьfarthestдоi+nums[i], если это значение больше. Если удастся пройти весь массив, до последнего индекса можно добраться.
Решение
Количество возможных маршрутов растёт экспоненциально, поэтому проверка маршрутов по одному не сработает для длинных массивов. Ключевой факт состоит в том, что индексы, которых можно достичь, всегда образуют один непрерывный блок, начинающийся с индекса 0. Одно число — правый конец этого блока — содержит всё необходимое, а одного прохода достаточно, чтобы определить ответ.
Попробуйте каждый прыжок
Верно, но не успевает на самых больших тестах
Идея
Самая очевидная идея — разыграть всё на практике. Встаньте на индекс 0 и по очереди попробуйте каждую позицию приземления, до которой позволяет допрыгнуть. Повторите то же самое для каждой позиции приземления. Если какая-либо ветвь приводит к последнему индексу, ответ — true. Если каждая ветвь заканчивается тупиком, ответ — false.
В первом примере в индексе 0 находится 2, поэтому вы пробуете индекс 1 и индекс 2. В индексе 1 находится 0 — это тупик, поэтому вы возвращаетесь назад и пробуете индекс 2. В индексе 2 находится 3, и оттуда можно добраться до индекса 5, последнего индекса, поэтому поиск завершается ответом true.
Поиск корректен, потому что проверяет каждый маршрут. В этом и заключается его проблема: он не запоминает уже исследованные индексы, поэтому исследует один и тот же индекс снова для каждого ведущего к нему маршрута. Когда ответ — false, ему приходится исключить все маршруты. В [4, 3, 2, 1, 0, 5] из каждого индекса перед 0 можно добраться до 0, поэтому к нему ведут 8 разных маршрутов. Если таких индексов 30, маршрутов будет больше 500 миллионов, а самые большие тесты содержат 10,000 элементов. Такой длинный маршрут также может привести к переполнению стека вызовов в некоторых языках: Python по умолчанию останавливается после 1,000 вложенных вызовов.
Алгоритм
- Напиши вспомогательную функцию
reach(i), которая отвечает на вопрос: можно ли добраться из индексаiдо последнего индекса? - Если
i— последний индекс, верниtrue. - В противном случае попробуй каждое место приземления
nextотi+1доmin(i+nums[i], n-1)и верниtrue, как только это сделаетreach(next). - Если ни одно место приземления не подходит, верни
false. - Ответ —
reach(0).
def canJump(nums):
last = len(nums) - 1
def reach(i):
# Can you get from index i to the last index?
if i == last:
return True
for nxt in range(i + 1, min(i + nums[i], last) + 1):
if reach(nxt):
return True
return False
return reach(0)Запомните, какие индексы могут завершить
Верно, но не успевает на самых больших тестах
Идея
В приведённом выше поиске снова и снова задаётся один и тот же вопрос: «может ли индекс j завершить путь?». Ответ для j никогда не меняется, поэтому вычисли его один раз и сохрани. Считай индекс хорошим, если из него можно добраться до последнего индекса. Последний индекс — хороший. Любой другой индекс i хороший, если хорош хотя бы один индекс, на который можно попасть из него: от i+1 до i+nums[i].
Каждый индекс зависит только от индексов справа от него, поэтому заполняй таблицу good справа налево. В первом примере индекс 5 — хороший. В индексе 4 хранится 0, поэтому он не хороший. Из индекса 3 можно попасть только в индекс 4: он не хороший. Из индекса 2 можно попасть в индексы 3, 4 и 5, а индекс 5 хороший, значит, индекс 2 тоже хороший. В индексе 1 хранится 0: он не хороший. Из индекса 0 можно попасть в индексы 1 и 2, а индекс 2 хороший, поэтому ответ — true.
Теперь решение для каждого индекса вычисляется один раз, но при этом всё ещё может потребоваться просмотреть до n ячеек. В [9998, 9997, …, 1, 0, 7] из каждого индекса можно добраться до 0, но нельзя пройти дальше, поэтому каждый индекс просматривает весь свой диапазон и не находит ничего хорошего. Для 10 000 элементов это около 5 × 10^7 проверок, и самые большие тесты устроены именно так. Объём работы растёт пропорционально квадрату длины, поэтому на таких тестах алгоритму не хватает времени.
Алгоритм
- Создайте булев массив
goodдлиныnи присвойтеgood[n-1]значение true. - Пройдите по
iотn-2до 0. - Переберите
jотi+1доmin(i+nums[i], n-1). Если какое-либоgood[j]равно true, присвойтеgood[i]значение true и прекратите перебор. - Верните
good[0].
def canJump(nums):
n = len(nums)
good = [False] * n # good[i]: from i you can reach the last index
good[n - 1] = True
for i in range(n - 2, -1, -1):
for j in range(i + 1, min(i + nums[i], n - 1) + 1):
if good[j]:
good[i] = True
break
return good[0]Отследите самый дальний достижимый индекс
Идея
Смотрите на индексы, до которых можно добраться, а не на маршруты. Из индекса i можно попасть на любой индекс от i+1 до i+nums[i], без пропусков. Поэтому, как только индекс i становится достижимым, достижим и каждый индекс вплоть до i+nums[i]. Начните только с индекса 0 и продолжайте добавлять такие отрезки. Каждый новый отрезок начинается внутри уже имеющегося блока, поэтому достижимые индексы всегда образуют один непрерывный блок, [0, farthest].
Именно поэтому достаточно одного числа. Проходите по i слева направо. Пока i ≤ farthest, индекс i достижим, поэтому расширяйте farthest до max(farthest, i+nums[i]). Если i когда-либо превысит farthest, ни один достижимый индекс не перепрыгнет до i. Блок не может вырасти через этот разрыв, поэтому ничто правее него недостижимо, включая последний индекс. Если проход дойдёт до конца без разрыва, последний индекс достижим.
Во втором примере farthest равен 0, затем становится равен 1 после индекса 0, а потом — 4 после индекса 1. В индексах 2, 3 и 4 находятся нули, поэтому значение остаётся равным 4. Индекс 5 находится за пределами 4, поэтому ответ — false. В первом примере индекс 2 увеличивает farthest до 5, и ни один индекс не оказывается за его пределами, поэтому ответ — true.
Почему безопасно хранить только максимальную дальность? Вы не выбираете конкретный прыжок заранее. Блок содержит каждый индекс, достижимый по любому маршруту, а каждая более короткая точка приземления находится внутри него. Если отбросить всё, кроме правой границы, никакая информация не теряется.
Алгоритм
- Задайте
farthest = 0. - Для каждого индекса
iслева направо: еслиi > farthest, вернитеfalse. - Иначе задайте
farthest = max(farthest, i+nums[i]). - Если цикл завершится, значит, можно было достичь каждого индекса, поэтому верните
true.
def canJump(nums):
farthest = 0 # every index up to farthest can be reached
for i, jump in enumerate(nums):
if i > farthest:
return False # nothing reachable jumps to i
farthest = max(farthest, i + jump)
return True
Ловушки и крайние случаи
Большинство неверных ответов возникает из-за того, что nums[i] воспринимают как единственный возможный прыжок или путают порядок двух проверок внутри цикла.
- Всегда прыгают ровно на
nums[i]шагов или всегда выбирают самый длинный прыжок. В массиве[2, 5, 0, 0]прыжок на всю длину из индекса 0 приземляет на 0, а прыжок на 1 шаг до индекса 1 позволяет добраться до конца. - Возвращают
false, как только видят 0. Ноль имеет значение только тогда, когда ничто до него не перепрыгивает через него:[2, 0, 1]позволяет перепрыгнуть через 0, поэтому ответ —true. - Обновляют
farthestдо проверкиi > farthest. Недостижимый индекс не должен расширять блок, поэтому сначала выполните проверку, а затем обновление. - Считают массив из одного элемента неудачей. Вы уже на последнем индексе, поэтому ответ —
true, даже если этот элемент равен 0. - Используют рекурсию для длинных массивов. Путь может состоять из 10,000 прыжков, что приводит к переполнению стека вызовов в нескольких языках. Однопроходный алгоритм не использует рекурсию.
Частые вопросы4
Какова временная сложность задачи Jump Game?
При проходе до самой дальней достижимой позиции каждый индекс посещается один раз, поэтому он выполняется за время O(n) и использует O(1) дополнительной памяти. В худшем случае табличный подход требует O(n²), а перебор всех маршрутов имеет экспоненциальную сложность.
Почему жадный подход работает для игры Jump Game?
Поскольку разрешены прыжки меньшей длины, достижение индекса i означает, что можно достичь каждого индекса вплоть до i+nums[i]. Эти отрезки всегда перекрываются с уже достигнутой частью, поэтому достижимые индексы образуют один блок, начинающийся с 0. Жадный проход отслеживает только правую границу этого блока, которая описывает весь блок, поэтому он никогда не отбрасывает маршрут, который мог бы сработать.
Является ли игра Jump Game задачей динамического программирования?
Это можно решить с помощью динамического программирования: помечайте каждый индекс как хороший, если хороша хотя бы одна из позиций, на которые можно попасть с него, заполняя таблицу справа налево. Это занимает O(n²). Заметьте, что имеет значение только самый левый хороший индекс, поскольку любой индекс, с которого можно попасть на хороший индекс, также позволяет попасть на самый левый. Храните только этот индекс, goal, и перемещайте его на i, когда i+nums[i] ≥ goal. Ответ зависит от того, окажется ли goal в итоге равен 0: это проход за O(n), повторяющий жадный алгоритм.
Как найти минимальное количество прыжков?
Используй ту же идею максимальной дальности, рассматривая массив по слоям. Отслеживай конец блока, до которого можно добраться с текущим количеством прыжков, и самый дальний индекс, до которого можно допрыгнуть следующим прыжком. Когда i проходит конец текущего блока, нужен ещё один прыжок, и следующий блок заканчивается на этом самом дальнем индексе. Это по-прежнему один проход за O(n).
Похожие задачи
Задачи на те же идеи. Решите две или три, и приём запомнится.
Python
def canJump(nums):
# Напишите код здесьСлучай 1
Случай 2
Случай 3
Ввод
nums = [2, 0, 3, 1, 0, 2]
Ожидается
true