Climbing Stairs
Stoisz u podnóża schodów o n stopniach. Każdy ruch pozwala wejść o 1 lub 2 stopnie. Dwa sposoby wejścia są różne, jeśli różnią się sekwencją ruchów, więc 1, 2 i 2, 1 to dwa sposoby. Twoja funkcja otrzymuje n i zwraca liczbę różnych sposobów dotarcia na górę.
Funkcja
- ninteger
- liczba stopni schodów
- Zwracainteger
- liczba różnych sekwencji kroków 1- i 2-krokowych, które prowadzą do kroku n
Ograniczenia
1 ≤ n ≤ 45- Odpowiedź mieści się w 32-bitowej liczbie całkowitej ze znakiem:
n = 45daje1836311903.
Przykłady
- Wejście
- n = 3
- Wyjście
- 3
- Wyjaśnienie
- Można wejść na trzy stopnie, stawiając kroki po
1, 1, 1,1, 2lub2, 1, więc są 3 sposoby.
- Wejście
- n = 5
- Wyjście
- 8
- Wyjaśnienie
- Każde wejście na stopień 5 kończy się krokiem o 1 stopień ze stopnia 4 (5 sposobów, by tam dotrzeć) lub krokiem o 2 stopnie ze stopnia 3 (3 sposoby), więc odpowiedź to
5 + 3 = 8.
+13 ukrytych testów przy wysłaniu
Pytanie dodatkowe
Co jeśli niektóre stopnie są zepsute i nie da się na nich stanąć? Jak zmienia się rekurencja i jaka jest liczba dla zepsutego stopnia?
Podpowiedzi
Otwieraj je po kolei. Każda zdradza trochę więcej.
Spójrz na ostatni ruch dowolnego podejścia do stopnia
n. Gdzie mogłeś się znajdować tuż przed nim?Każde wejście na stopień
nkończy się krokiem o długości 1 ze stopnian-1albo krokiem o długości 2 ze stopnian-2, nigdy oboma naraz. Zatem liczba sposobów wejścia na stopieńnto liczba sposobów wejścia na stopieńn-1plus liczba sposobów wejścia na stopieńn-2.Zacznij od liczby sposobów dla 1 kroku (1 sposób) i 2 kroków (2 sposoby), a następnie zwiększaj liczbę kroków. Wystarczą Ci zawsze tylko dwie ostatnie liczby sposobów, a każda kolejna jest ich sumą.
Rozwiązanie
Wypisanie każdego sposobu wspinania się nie zadziała: schody o 45 stopniach mają ich 1836311903. Kluczem jest ostatni ruch. Każda droga na stopień n prowadzi tuż przed końcem przez stopień n-1 albo n-2, co daje ways(n) = ways(n-1) + ways(n-2), czyli rekurencję Fibonacciego. Obliczaj wartości od dołu, a wystarczą ci dwie zmienne.
Prosta rekurencja dotycząca ostatniego ruchu
Poprawne, ale nie kończy się na największych testach
Intuicja
Podziel wspinaczki na stopień n ze względu na ich ostatni ruch. Wspinaczka kończąca się krokiem o 1 stopień wcześniej znajdowała się na stopniu n-1, a takich wspinaczek jest ways(n-1). Wspinaczka kończąca się krokiem o 2 stopnie wcześniej znajdowała się na stopniu n-2, a takich wspinaczek jest ways(n-2). Każda wspinaczka kończy się na jeden albo drugi sposób, a żadna nie kończy się na oba sposoby, więc ways(n) = ways(n-1) + ways(n-2).
Rekurencja wymaga dwóch przypadków bazowych. Na jeden stopień prowadzi jedna wspinaczka, a na dwa stopnie — dwie wspinaczki (1, 1 i 2). W obu przypadkach odpowiedź jest równa n, więc funkcja zwraca n, gdy n ≤ 2, a w przeciwnym razie sumę.
Odpowiedź jest poprawna, ale liczba obliczeń gwałtownie rośnie. climbStairs(5) dwukrotnie wywołuje obliczenie dla stopnia 3 i trzykrotnie dla stopnia 2 — łącznie 9 wywołań — a ich liczba rośnie podobnie jak same odpowiedzi. Dla n = 45 funkcja wykonuje 2269806339 wywołań, czyli około 2.3 × 10^9, zdecydowanie za dużo, by zmieścić się w limicie czasu. Rekurencja ma głębokość zaledwie n, więc stos zajmuje O(n) miejsca.
Algorytm
- Jeśli
n ≤ 2, zwróćn. - Policz wspinaczki docierające do stopnia
n-1za pomocą wywołania rekurencyjnego. - Policz wspinaczki docierające do stopnia
n-2za pomocą drugiego wywołania rekurencyjnego. - Zwróć sumę tych dwóch liczb.
def climbStairs(n):
if n <= 2:
return n # 1 step: one way, 2 steps: two ways
return climbStairs(n - 1) + climbStairs(n - 2)Rekurencja z pamięcią
Intuicja
Rekurencja jest powolna tylko dlatego, że zapomina. Każda liczba zależy wyłącznie od k, więc gdy znasz już liczbę dla kroku k, nigdy się ona nie zmienia. Zachowaj pamięć podręczną — tablicę z jednym miejscem na każdy krok — i zapisz w niej każdą liczbę przy pierwszym obliczeniu. Każde kolejne żądanie dotyczące tego samego kroku odczytuje wartość z tego miejsca, zamiast ponownie wywoływać rekurencję.
Teraz każda z liczb od kroku 3 do kroku n jest obliczana raz, przy użyciu jednego dodawania. Dla n = 5 wywołania schodzą do kroku 2 tylko raz, a następnie wyniki wracają jako 3, 5 i 8; drugie żądanie dotyczące kroku 3 korzysta już z odczytu. Złożoność czasowa wynosi O(n) zamiast miliardów wywołań.
Pamięć podręczna przechowuje n + 1 liczb, a rekurencja nadal sięga n poziomów w głąb, więc złożoność pamięciowa wynosi O(n). 0 w danym miejscu oznacza, że wartość nie jest jeszcze znana, co jest bezpieczne, ponieważ każda rzeczywista liczba wynosi co najmniej 1.
Algorytm
- Utwórz tablicę memo z
n + 1miejscami, wszystkie ustawione na 0. - W funkcji pomocniczej rekurencyjnej zwróć
k, gdyk ≤ 2. - Jeśli miejsce w tablicy memo dla
kma wartość 0, wypełnij je sumą wyników funkcji pomocniczej dlak-1ik-2. - Zwróć wartość z miejsca w tablicy memo.
- Wywołaj funkcję pomocniczą dla
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)Od dołu w górę z dwiema zmiennymi
Intuicja
Odwróć rekurencję. Zamiast zaczynać od góry i pytać o kolejne wartości w dół, zacznij od dołu i buduj w górę. Gdy obliczasz liczbę sposobów dla kroku k, liczby dla kroków k-1 i k-2 są już znane, a do żadnych wcześniejszych już się nie odwołujesz. Całe memo można więc zastąpić dwiema zmiennymi.
Niech prev przechowuje liczbę sposobów dla kroku k-2, a curr — liczbę sposobów dla kroku k-1. Zacznij od prev = 1 i curr = 2 — to liczby sposobów dla kroków 1 i 2. W każdym kroku dodaj je i zapisz wynik w next, a następnie przesuń parę. Dla n = 5 para zmienia się z (1, 2) na (2, 3), (3, 5) i (5, 8), a curr = 8 jest odpowiedzią.
Pętla wykonuje się n-2 razy, za każdym razem wykonując jedno dodawanie. Złożoność czasowa wynosi O(n), a algorytm przechowuje trzy liczby całkowite, więc złożoność pamięciowa wynosi O(1). Oblicz next przed nadpisaniem prev, w przeciwnym razie suma będzie korzystać z niewłaściwej wartości.
Algorytm
- Jeśli
n ≤ 2, zwróćn. - Ustaw
prev = 1icurr = 2. - Dla
kod 3 donoblicznext = prev + curr, a następnie ustawprev = curricurr = next. - Zwróć
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
Pułapki i przypadki brzegowe
Rekurencja jest krótka, więc większość błędów kryje się w przypadkach bazowych, czasie działania i limicie 32-bitowym.
- Oddanie zwykłej rekurencji. Przechodzi małe testy, a potem wymaga około
2.3 × 10^9wywołań dlan = 45. Zapisuj każde obliczenie tylko raz. - Nieprawidłowe przypadki bazowe. Na dwa stopnie można wspiąć się na dwa sposoby:
1, 1i2. Zwrócenie 1 dlan = 2zmienia wszystkie kolejne wyniki: dlan = 3otrzymasz 2 zamiast 3. - Liczenie wyborów zamiast sekwencji.
1, 2i2, 1to dwa różne sposoby wspinania się. Zliczenie tylko tego, ile razy wykonujesz krok o 2 stopnie, dajen/2 + 1, czyli 3 dlan = 5zamiast 8. - Wypełnianie tablicy bez sprawdzenia warunku. Dla
n = 1tablica on + 1 = 2elementach nie ma miejsca na liczbę sposobów dla 2. stopnia. Zwróćnod razu, gdyn ≤ 2. - Wykonanie o jeden krok za dużo. Liczba sposobów dla 45 stopni, 1836311903, mieści się w 32 bitach, ale liczba sposobów dla 46 stopni wynosi 2971215073 i już się nie mieści. Pętla, która oblicza jedną wartość za dużo, powoduje przepełnienie i daje liczbę ujemną w Java, C lub C#.
Najczęstsze pytania4
Dlaczego wspinanie się po schodach jest problemem Fibonacciego?
Każde wejście na stopień n kończy się krokiem o 1 stopień z n-1 albo krokiem o 2 stopnie z n-2, więc ways(n) = ways(n-1) + ways(n-2). To reguła Fibonacciego. Dla ways(1) = 1 i ways(2) = 2 liczby wynoszą kolejno 1, 2, 3, 5, 8, 13, co stanowi ciąg Fibonacciego przesunięty o jedno miejsce: ways(n) = F(n+1).
Jaka jest złożoność czasowa problemu wspinania się po schodach?
Pętla od dołu wykonuje n-2 dodawania, więc działa w czasie O(n) i zużywa dodatkową pamięć O(1). Zwykła rekurencja ma złożoność wykładniczą: liczba wywołań rośnie około 1,618 raza z każdym krokiem i osiąga 2269806339, czyli w przybliżeniu 2.3 × 10^9, dla n = 45. Zapamiętywanie wyników zmniejsza złożoność rekurencji do O(n) pod względem czasu i O(n) pod względem pamięci.
Jaka jest różnica między memoizacją a rozwiązaniem od dołu?
Memoizacja zachowuje funkcję rekurencyjną i buforuje każdy wynik przy jego pierwszym obliczeniu, więc działa od góry do dołu i wymaga stosu wywołań oraz tablicy. Pętla działająca od dołu do góry oblicza liczby w rosnącej kolejności, więc każda potrzebna jej wartość jest już znana i nie ma tu rekurencji. Oba rozwiązania wymagają O(n) pracy. Pętla pozwala też zrezygnować z tablicy i przechowywać tylko dwie liczby.
Jak rozwiązać problem wchodzenia po schodach, gdy można pokonywać 1, 2 lub 3 stopnie naraz?
Ponownie podziel sposoby pokonania schodów według ostatniego kroku: ways(n) = ways(n-1) + ways(n-2) + ways(n-3). Zacznij od ways(0) = 1 (puste schody), ways(1) = 1 i ways(2) = 2, a następnie przechowuj trzy ostatnie liczby sposobów zamiast dwóch. Złożoność czasowa pozostaje równa O(n), a złożoność pamięciowa O(1).
Podobne zadania
Zadania oparte na tych samych pomysłach. Rozwiązanie dwóch lub trzech utrwala schemat.
Python
def climbStairs(n):
# Wpisz kod tutajPrzypadek 1
Przypadek 2
Wejście
n = 3
Oczekiwane
3