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]$.

  1. Итерируемся по массиву от $0$ до $n - 1$.
  2. Если текущий индекс $i > \text{maxReach}$, мы попали в зону разрыва: ни один из предыдущих прыжков не достает до позиции $i$. Дальнейшее движение невозможно — возвращаем false.
  3. Обновляем границу досягаемости: maxReach = max(maxReach, i + nums[i]).
  4. Если 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

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
}

Резюме

  1. Инвариант границы: Достаточно отслеживать максимальную координату досягаемости, не восстанавливая промежуточные шаги.
  2. Ранний выход: Условие maxReach >= lastIdx сокращает количество итераций в среднем на $40\text{–}60\%$.
  3. Граничные условия: Пустые или одноэлементные массивы обрабатываются за $O(1)$.

В следующей статье мы усложним задачу: как найти не просто факт достижимости, а минимальное число прыжков: 4. Jump Game II.