Climbing Stairs
n개의 계단이 있는 계단의 맨 아래에 서 있습니다. 한 번 움직일 때마다 1칸 또는 2칸을 오릅니다. 이동 순서가 다르면 서로 다른 오르기 방법으로 간주하므로, 1, 2와 2, 1은 서로 다른 두 가지 방법입니다. 함수는 n을 입력받아 꼭대기에 도달하는 서로 다른 방법의 수를 반환합니다.
함수
- ninteger
- 계단의 단계 수
- 반환값integer
- 단계 n에 도달하는 1단계와 2단계의 서로 다른 순서 배열 수
제약 조건
1 ≤ n ≤ 45- 답은 부호 있는 32비트 정수에 들어갑니다:
n = 45는1836311903을 반환합니다.
예제
- 입력
- n = 3
- 출력
- 3
- 설명
- 계단 세 칸은
1, 1, 1로 오르거나,1, 2또는2, 1로 오를 수 있으므로, 방법은 3가지입니다.
- 입력
- n = 5
- 출력
- 8
- 설명
- 5단계로 올라가는 모든 방법은 4단계에서 1단계 올라가는 경우(거기에 도달하는 방법 5가지) 또는 3단계에서 2단계 올라가는 경우(방법 3가지)로 끝나므로, 답은
5 + 3 = 8입니다.
제출 시 숨은 테스트 +13개
후속 질문
일부 계단이 망가져서 그 위에 절대 설 수 없다면 어떻게 될까요? 점화식은 어떻게 바뀌며, 망가진 계단의 경우의 수는 얼마일까요?
힌트
하나씩 열어 보세요. 열 때마다 조금 더 알려 줍니다.
n단계로 오르는 어떤 방법이든 마지막 이동을 살펴보세요. 그 직전에 어디에 서 있었을까요?n번째 계단까지 오르는 모든 방법은n-1번째 계단에서 1계단 오르거나n-2번째 계단에서 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 ≤ 2일 때 n을 반환하고, 그 외에는 합을 반환합니다.
답은 맞지만, 연산량이 폭발적으로 늘어납니다. 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이므로 안전합니다.
알고리즘
- 모든 값이 0인
n + 1개의 슬롯으로 메모 배열을 만듭니다. - 재귀 헬퍼에서
k ≤ 2이면k를 반환합니다. 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의 개수를 저장합니다. 단계 1과 2의 개수인 prev = 1과 curr = 2로 시작하세요. 각 단계에서 두 값을 더해 next에 저장한 다음, 쌍을 앞으로 이동합니다. n = 5일 때 쌍은 (1, 2)에서 (2, 3), (3, 5), (5, 8)로 이동하며, curr = 8이 답입니다.
반복문은 한 번씩 더하기를 수행하며 n-2번 실행되므로 시간 복잡도는 O(n)이고, 정수 세 개를 유지하므로 공간 복잡도는 O(1)입니다. prev를 덮어쓰기 전에 next를 계산하세요. 그렇지 않으면 잘못된 값으로 합을 계산하게 됩니다.
알고리즘
n ≤ 2이면n을 반환합니다.prev = 1과curr = 2를 설정합니다.- 3부터
n까지k에 대해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입니다.n = 2일 때 1을 반환하면 이후의 모든 답이 어긋납니다.n = 3일 때 3 대신 2를 얻게 됩니다. - 순서가 있는 경우의 수 대신 선택의 수를 세는 경우.
1, 2와2, 1은 서로 다른 두 가지 계단 오르기 방법입니다. 2칸씩 오르는 횟수만 세면n/2 + 1이 되어,n = 5일 때 8 대신 3이 나옵니다. - 범위 확인 없이 테이블을 채우는 경우.
n = 1이면n + 1 = 2개의 슬롯으로 된 테이블에는 2번째 계단의 개수를 저장할 공간이 없습니다.n ≤ 2일 때는 바로n을 반환하세요. - 한 단계 더 계산하는 경우. 45계단을 오르는 방법의 수인 1836311903은 32비트에 들어가지만, 46계단의 방법의 수인 2971215073은 들어가지 않습니다. 값을 하나 더 계산하는 루프는 Java, C 또는 C#에서 오버플로가 발생해 음수가 됩니다.
자주 묻는 질문4
왜 계단 오르기는 피보나치 문제일까요?
n단계까지 올라가는 모든 경우는 n-1에서 1단계를 올라가거나 n-2에서 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).
Climbing Stairs의 시간 복잡도는 얼마인가요?
상향식 루프는 n-2번 덧셈을 수행하므로, O(n) 시간과 O(1) 추가 공간으로 실행됩니다. 일반 재귀는 지수 시간입니다. 호출 횟수가 단계마다 약 1.618배씩 증가해 n = 45일 때 2269806339회, 즉 대략 2.3 × 10^9회에 이릅니다. 메모이제이션을 적용하면 재귀의 시간 복잡도는 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