3. Jump Game
«Не пытайся заглянуть во все возможные миры: достаточно твердо знать предел своей досягаемости на каждом шагу».
— Эдсгер Дейкстра
Достижимость и жадный выбор: от O(N²) к O(N)
Задача Jump Game (LeetCode №55) моделирует базовую проблему проверки достижимости целевого состояния.
Условие: Дан массив неотрицательных целых чисел nums, где nums[i] — максимальная длина прыжка вперед из индекса $i$. Изначально вы находитесь на позиции $0$. Требуется определить, возможно ли добраться до последнего индекса $n - 1$.
Динамическое программирование требует $O(N)$ памяти под массив dp и $O(N^2)$ времени в наивной реализации. Жадный подход решает задачу за один линейный проход $O(N)$ времени и строго $O(1)$ памяти.
Архитектурный каркас: инвариант maxReach
Вместо исследования всех траекторий мы поддерживаем строгий инвариант:
Переменная maxReach хранит максимально далекий индекс, достижимый с помощью любой комбинации прыжков из пройденного диапазона $[0, i]$.
- Итерируемся по массиву от $0$ до $n - 1$.
- Если текущий индекс $i > \text{maxReach}$, мы попали в зону разрыва: ни один из предыдущих прыжков не достает до позиции $i$. Дальнейшее движение невозможно — возвращаем
false. - Обновляем границу досягаемости:
maxReach = max(maxReach, i + nums[i]). - Если
maxReach >= n - 1, последний индекс гарантированно достижим — производим ранний выход сtrue.
flowchart TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
A["Итерация i от 0 до n - 1"]:::entry --> B{"i <= maxReach?"}:::warning
B -->|"Нет: Разрыв"| C["Возврат false (зона недостижима)"]:::error
B -->|"Да: Достижим"| D["maxReach = max(maxReach, i + nums[i])"]:::process
D --> E{"maxReach >= n - 1?"}:::warning
E -->|"Да: Цель покрыта"| F["Ранний возврат true"]:::success
E -->|"Нет"| G["Переход к i + 1"]:::process
G --> A
Идиоматичная реализация на Go
package main
// canJump проверяет достижимость последнего индекса за O(N) времени и O(1) памяти.
func canJump(nums []int) bool {
if len(nums) <= 1 {
return true
}
maxReach := 0
lastIdx := len(nums) - 1
for i := 0; i <= lastIdx; i++ {
// Текущий индекс недостижим ни из одной предыдущей точки
if i > maxReach {
return false
}
// Расширяем горизонт досягаемости
reach := i + nums[i]
if reach > maxReach {
maxReach = reach
}
// Ранний выход: цель уже в зоне досягаемости
if maxReach >= lastIdx {
return true
}
}
return false
}
Резюме
- Инвариант границы: Достаточно отслеживать максимальную координату досягаемости, не восстанавливая промежуточные шаги.
- Ранний выход: Условие
maxReach >= lastIdxсокращает количество итераций в среднем на $40\text{–}60\%$. - Граничные условия: Пустые или одноэлементные массивы обрабатываются за $O(1)$.
В следующей статье мы усложним задачу: как найти не просто факт достижимости, а минимальное число прыжков: 4. Jump Game II.