Climbing Stairs
Ты стоишь у подножия лестницы с n ступеньками. За один ход можно подняться на 1 или 2 ступеньки. Два способа подъёма считаются разными, если последовательности ходов в них отличаются, поэтому 1, 2 и 2, 1 — это два разных способа. Твоя функция получает n и возвращает количество различных способов добраться до вершины.
Функция
- ninteger
- количество ступеней в лестнице
- Возвращаетinteger
- количество различных последовательностей из шагов длиной 1 и 2, позволяющих достичь ступеньки n
Ограничения
1 ≤ n ≤ 45- Ответ помещается в 32-разрядное целое число со знаком:
n = 45даёт1836311903.
Примеры
- Ввод
- n = 3
- Вывод
- 3
- Пояснение
- На три ступеньки можно подняться так:
1, 1, 1, так:1, 2или так:2, 1, всего есть 3 способа.
- Ввод
- n = 5
- Вывод
- 8
- Пояснение
- Каждый подъем к ступеньке 5 заканчивается шагом на 1 ступеньку со ступеньки 4 (5 способов добраться туда) или шагом на 2 ступеньки со ступеньки 3 (3 способа), поэтому ответ —
5 + 3 = 8.
+13 скрытых тестов при отправке
Дополнительный вопрос
Что, если некоторые ступеньки сломаны и на них нельзя наступать? Как изменится рекуррентное соотношение и чему равен счёт для сломанной ступеньки?
Подсказки
Открывайте по одной. Каждая подсказывает чуть больше.
Посмотрите на последний шаг любого подъёма до ступени
n. Где вы могли находиться непосредственно перед ним?Каждый подъём на ступеньку
nзаканчивается шагом в 1 ступеньку со ступенькиn-1или шагом в 2 ступеньки со ступенькиn-2, но не обоими сразу. Поэтому количество способов дляnравно количеству способов дляn-1плюс количеству способов дляn-2.Начни со значений для 1 шага (1 способ) и 2 шагов (2 способа) и двигайся дальше. Тебе всегда нужны только два последних значения, а каждое новое значение — их сумма.
Решение
Перечислить все варианты подъёма не получится: у лестницы из 45 ступеней их 1836311903. Подсказка — последний шаг. Перед завершением любой подъём на ступень n проходит через ступень n-1 или n-2, откуда получается ways(n) = ways(n-1) + ways(n-2) — рекуррентное соотношение Фибоначчи. Вычисляйте значения снизу вверх, и вам хватит двух переменных.
Обычная рекурсия по последнему ходу
Верно, но не успевает на самых больших тестах
Идея
Разбейте подъёмы на ступень n по их последнему шагу. При подъёме, заканчивающемся шагом на 1 ступень, перед ним находились на ступени n-1, и таких подъёмов ways(n-1). При подъёме, заканчивающемся шагом на 2 ступени, находились на ступени n-2, и таких подъёмов ways(n-2). Каждый подъём заканчивается одним из этих двух способов, и ни один не заканчивается обоими способами, поэтому ways(n) = ways(n-1) + ways(n-2).
Рекурсии нужны два базовых случая. На одну ступень есть один подъём, а на две ступени — два (1, 1 и 2). В обоих случаях ответ равен n, поэтому функция возвращает n при n ≤ 2, а в остальных случаях — сумму.
Ответ правильный, но объём вычислений стремительно растёт. Вызов climbStairs(5) дважды запрашивает ступень 3 и трижды ступень 2 — всего 9 вызовов, а количество вызовов растёт примерно так же, как сами ответы. При n = 45 функция делает 2269806339 вызовов, около 2.3 × 10^9, что намного превышает допустимое время выполнения. Глубина рекурсии составляет всего n уровней, поэтому стек занимает O(n) памяти.
Алгоритм
- Если
n ≤ 2, вернитеn. - Посчитайте количество подъёмов, достигающих ступеньки
n-1, с помощью рекурсивного вызова. - Посчитайте количество подъёмов, достигающих ступеньки
n-2, с помощью второго рекурсивного вызова. - Верните сумму двух количеств.
def climbStairs(n):
if n <= 2:
return n # 1 step: one way, 2 steps: two ways
return climbStairs(n - 1) + climbStairs(n - 2)Рекурсия с мемоизацией
Идея
Рекурсия медленная только потому, что она забывает. Каждый подсчёт зависит только от k, поэтому, узнав количество для шага k, ты уже знаешь его навсегда. Используй мемоизацию — массив с одной ячейкой на каждый шаг — и записывай туда количество при первом вычислении. При каждом последующем запросе того же шага значение считывается из ячейки, вместо того чтобы снова запускать рекурсию.
Теперь каждое количество для шагов от 3 до шага n вычисляется один раз, с одним сложением. Для n = 5 вызовы доходят до шага 2 один раз, затем ответы возвращаются обратно как 3, 5 и 8, а при повторном запросе шага 3 значение считывается из ячейки. Это время O(n) вместо миллиардов вызовов.
В мемоизации хранятся n + 1 чисел, а глубина рекурсии по-прежнему составляет n уровней, поэтому пространственная сложность — O(n). 0 в ячейке означает, что значение ещё неизвестно; это безопасно, потому что каждое реальное количество не меньше 1.
Алгоритм
- Создай мемо-массив из
n + 1ячеек, заполненных нулями. - В рекурсивной вспомогательной функции возвращай
k, еслиk ≤ 2. - Если ячейка мемо-массива для
kравна 0, заполни её суммой результатов вспомогательной функции дляk-1иk-2. - Верни значение ячейки мемо-массива.
- Вызови вспомогательную функцию для
n.
def climbStairs(n):
memo = [0] * (n + 1) # memo[k] = ways to reach step k, 0 = not known yet
def ways(k):
if k <= 2:
return k
if memo[k] == 0:
memo[k] = ways(k - 1) + ways(k - 2)
return memo[k]
return ways(n)Снизу вверх с двумя переменными
Идея
Разверните рекурсию. Вместо того чтобы начинать сверху и двигаться вниз, начните снизу и стройте решение вверх. Когда вы вычисляете количество способов для шага k, количества для k-1 и k-2 уже известны, и к более ранним значениям больше не обращаются. Поэтому вместо всего кэша достаточно двух переменных.
Пусть prev хранит количество способов для шага k-2, а curr — для шага k-1. Начните с prev = 1 и curr = 2 — это количества способов для шагов 1 и 2. На каждом шаге сложите их в next, а затем сдвиньте пару вперёд. Для n = 5 пара меняется с (1, 2) на (2, 3), (3, 5) и (5, 8), и curr = 8 — это ответ.
Цикл выполняется n-2 раз, каждый раз выполняя одно сложение, поэтому время работы — O(n), а используются три целых числа, то есть требуется O(1) памяти. Вычислите next, прежде чем перезаписать prev, иначе при сложении будет использовано неверное значение.
Алгоритм
- Если
n ≤ 2, вернитеn. - Задайте
prev = 1иcurr = 2. - Для
kот 3 доnвычислитеnext = prev + curr, затем задайтеprev = currиcurr = next. - Верните
curr.
def climbStairs(n):
if n <= 2:
return n
prev, curr = 1, 2 # ways to reach steps 1 and 2
for _ in range(n - 2):
prev, curr = curr, prev + curr
return curr
Ловушки и крайние случаи
Рекуррентная формула короткая, поэтому большинство ошибок связано с базовыми случаями, временем выполнения и ограничением в 32 бита.
- Сдать решение с обычной рекурсией. Оно проходит небольшие тесты, но для
n = 45требует около2.3 × 10^9вызовов. Сохраняй каждое количество один раз. - Неправильные базовые случаи. Для двух ступенек есть два способа подняться:
1, 1и2. Если возвращать 1 дляn = 2, все последующие ответы сместятся: дляn = 3получится 2 вместо 3. - Считать варианты, а не последовательности.
1, 2и2, 1— это два способа подняться. Если считать только количество шагов по 2 ступеньки, получитсяn/2 + 1, то есть 3 дляn = 5вместо 8. - Заполнять таблицу без проверки. При
n = 1в таблице изn + 1 = 2ячеек нет места для количества способов подняться на 2 ступеньки. Сразу возвращайn, еслиn ≤ 2. - Вычислить на один шаг дальше, чем нужно. Количество способов подняться на 45 ступенек, 1836311903, помещается в 32 бита, а количество для 46 ступенек — 2971215073 — уже не помещается. Цикл, вычисляющий ещё одно значение, приводит к переполнению и отрицательному числу в Java, C или C#.
Частые вопросы4
Почему задача «Подъём по лестнице» — это задача о числах Фибоначчи?
Каждый подъём на ступеньку n заканчивается шагом на 1 ступеньку с n-1 или на 2 ступеньки с n-2, поэтому ways(n) = ways(n-1) + ways(n-2). Это правило Фибоначчи. При ways(1) = 1 и ways(2) = 2 последовательность значений выглядит так: 1, 2, 3, 5, 8, 13 — это последовательность Фибоначчи, сдвинутая на одну позицию: ways(n) = F(n+1).
Какова временная сложность задачи «Подъём по лестнице»?
Цикл снизу вверх выполняет n-2 сложений, поэтому работает за время O(n) и использует дополнительную память O(1). Обычная рекурсия имеет экспоненциальную сложность: число вызовов увеличивается примерно в 1.618 раза на каждом шаге и достигает 2269806339, то есть примерно 2.3 × 10^9, при n = 45. Мемоизация снижает сложность рекурсии до O(n) по времени и O(n) по памяти.
В чём разница между мемоизацией и решением снизу вверх?
Мемоизация сохраняет рекурсивную функцию и кэширует каждый результат при первом вычислении, поэтому она работает сверху вниз и требует стека вызовов и таблицы. Цикл снизу вверх вычисляет количества в возрастающем порядке, поэтому все необходимые значения уже известны и рекурсия не используется. Оба подхода требуют O(n) операций. Цикл также позволяет отказаться от таблицы и хранить два числа.
Как решить задачу «Подъём по лестнице» с шагами в 1, 2 или 3 ступеньки?
Снова разделите подъемы по их последнему шагу: ways(n) = ways(n-1) + ways(n-2) + ways(n-3). Начните с ways(0) = 1 (пустой подъем), ways(1) = 1 и ways(2) = 2 и храните последние три значения вместо двух. Время выполнения остается O(n), а объем памяти — O(1).
Похожие задачи
Задачи на те же идеи. Решите две или три, и приём запомнится.
Python
def climbStairs(n):
# Напишите код здесьСлучай 1
Случай 2
Ввод
n = 3
Ожидается
3