Given an integer n, there is a staircase with n steps, starting from the 0th step. Determine the number of unique ways to reach the nth step, given that each move can be either 1 or 2 steps at a time.
Input: n = 2
Output: 2
Explanation: There are 2 unique ways to climb to the 2nd step:
1) 1 step + 1 step
2) 2 steps
Input: n = 3
Output: 3
Explanation: There are 3 unique ways to climb to the 3rd step:
1) 1 step + 1 step + 1 step
2) 2 steps + 1 step
3) 1 step + 2 steps
Input: n = 1
When encountering a problem, it's crucial to recognize if it can be approached using dynamic programming (DP) techniques. This is especially relevant when the problem exhibits certain characteristics that align with the strengths of dynamic programming. Some common indicators that suggest a problem might be suitable for dynamic programming include:
If the question asks for either of the above two points mentioned earlier, recursion can be applied to attempt solving the problem. Once the recursive solution is obtained, it can be converted into a dynamic programming approach.
The problem statement asks to count the total ways one can jump from the 0th step to the nth step by jumping either 1 or 2 steps at a time. Since the question requires counting total ways, recursion can be applied, and eventually, this problem can also be solved using dynamic programming.
Once the problem has been identified, the following three steps comes handy in solving the problem:
/*It is pseudocode and not tied to
any specific programming language.*/
f(n){
oneStep = f(n-1)
twoStep = f(n-2)
return oneStep + twoStep
}
#include <bits/stdc++.h>
using namespace std;
class Solution {
public:
// Function to count total ways to reach nth stair
int climbStairs(int n) {
// Base case
if (n == 0) return 1;
if (n == 1) return 1;
// Taking 1 step at a time
int oneStep = climbStairs(n-1);
// Taking 2 steps at a time
int twoSteps = climbStairs(n-2);
// Return total ways
return oneStep + twoSteps;
}
};
int main() {
int n = 3;
// Create an instance of Solution class
Solution sol;
// Print the answer
cout << "The total number of ways: " << sol.climbStairs(n) << endl;
return 0;
}
import java.util.*;
class Solution{
//Function to count total ways to reach nth stair
public static int climbStairs(int n){
//Base case
if(n == 0) return 1;
if(n == 1) return 1;
//Taking 1 step at a time
int oneStep = climbStairs(n-1);
//Taking 2 steps at a time
int twoSteps = climbStairs(n-2);
//Return total ways
return oneStep+twoSteps;
}
public static void main (String[] args){
int n = 3;
//Create an instance of Solution class
Solution sol = new Solution();
//Print the answer
System.out.println("The total number of ways: "+sol.climbStairs(n));
}
}
class Solution:
# Function to count total ways to reach nth stair
def climbStairs(self, n):
# Base case
if n == 0:
return 1
if n == 1:
return 1
# Taking 1 step at a time
oneStep = self.climbStairs(n-1)
# Taking 2 steps at a time
twoSteps = self.climbStairs(n-2)
# Return total ways
return oneStep + twoSteps
if __name__ == "__main__":
n = 3
# Create an instance of Solution class
sol = Solution()
# Print the answer
print("The total number of ways:", sol.climbStairs(n))
class Solution {
// Function to count total ways to reach nth stair
climbStairs(n) {
// Base case
if (n === 0) return 1;
if (n === 1) return 1;
// Taking 1 step at a time
let oneStep = this.climbStairs(n-1);
// Taking 2 steps at a time
let twoSteps = this.climbStairs(n-2);
// Return total ways
return oneStep + twoSteps;
}
}
let n = 3;
// Create an instance of Solution class
let sol = new Solution();
// Print the answer
console.log("The total number of ways:", sol.climbStairs(n));
As, the recursive solutions recalculate the same subproblems again and again, leading to inefficiencies, especially with larger inputs. Memoization addresses this by storing the results of already computed subproblems.
When a subproblem is encountered again, which is known as overlapping subproblem, instead of recalculating it, the precomputed result is simply retrieved from memory. This speeds up the computation and also reduces the time complexity significantly, often transforming an exponential-time recursive solution into a polynomial-time solution.
#include <bits/stdc++.h>
using namespace std;
class Solution {
private:
//Helper function to apply memoization
int func(int n, vector<int> &dp) {
// Base condition
if (n <= 1) {
return 1;
}
// Check if the subproblem is already solved
if (dp[n] != -1) {
return dp[n];
}
// Return the calculated value
return dp[n] = func(n-1, dp) + func(n-2, dp);
}
public:
// Function to count total ways to reach nth stair
int climbStairs(int n) {
// Initialize dp vector with -1
vector<int> dp(n + 1, -1);
return func(n, dp);
}
};
int main() {
int n = 5;
// Create an instance of Solution class
Solution sol;
// Print the answer
cout << "The total number of ways: " << sol.climbStairs(n) << endl;
return 0;
}
import java.util.*;
class Solution {
//Helper function to apply memoization
private int func(int n, int dp[]){
//Base condition
if (n <= 1) {
return 1;
}
//Check if the subproblem is already solved
if (dp[n] != -1) {
return dp[n];
}
//Return the calculated value
return dp[n] = func(n-1, dp) + func(n-2, dp);
}
// Function to count total ways to reach nth stair
public int climbStairs(int n) {
int[] dp = new int[n+1];
// Initialize dp array with -1
Arrays.fill(dp, -1);
// Return the calculated value
return dp[n] = func(n, dp);
}
public static void main(String[] args) {
int n = 5;
// Create an instance of Solution class
Solution sol = new Solution();
// Print the answer
System.out.println("The total number of ways: " + sol.climbStairs(n));
}
}
class Solution:
#Helper function to apply memoization
def func(self, n, dp):
# Base condition
if n <= 1:
return 1
# Check if the subproblem is already solved
if dp[n] != -1:
return dp[n]
# Return the calculated value
dp[n] = self.func(n-1, dp) + self.func(n-2, dp)
return dp[n]
# Function to count total ways to reach nth stair
def climbStairs(self, n):
# Initialize dp array with -1
dp = [-1] * (n + 1)
return self.func(n, dp)
if __name__ == "__main__":
n = 5
# Create an instance of Solution class
sol = Solution()
# Print the answer
print("The total number of ways:", sol.climbStairs(n))
class Solution {
func(n, dp){
// Base condition
if (n <= 1) {
return 1;
}
// Check if the subproblem is already solved
if (dp[n] !== -1) {
return dp[n];
}
// Return the calculated value
dp[n] = this.func(n-1, dp) + this.func(n-2, dp);
return dp[n];
}
// Function to count total ways to reach nth stair
climbStairs(n) {
// Initialize dp array with -1
let dp = Array(n+1).fill(-1);
return this.func(n, dp);
}
}
let n = 5;
// Create an instance of Solution class
let sol = new Solution();
// Print the answer
console.log("The total number of ways: " + sol.climbStairs(n));
In order to convert a recursive code to tabulation code, we try to convert the recursive code to tabulation and here are the steps:
#include <bits/stdc++.h>
using namespace std;
class Solution {
public:
// Function to count total ways to reach nth stair
int climbStairs(int n) {
//Declare dp array of size n+1
vector<int> dp(n + 1, -1);
//Insert the values of base conditions
dp[0] = 1;
dp[1] = 1;
//Iterate and update every index of dp array
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
//Return the answer
return dp[n];
}
};
int main() {
int n = 3;
// Create an instance of Solution class
Solution sol;
// Print the answer
cout << "The total number of ways: " << sol.climbStairs(n) << endl;
return 0;
}
import java.util.*;
class Solution {
// Function to count total ways to reach nth stair
public int climbStairs(int n) {
// Declare dp array of size n+1
int[] dp = new int[n + 1];
// Insert the values of base conditions
dp[0] = 1;
dp[1] = 1;
// Iterate and update every index of dp array
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
// Return the answer
return dp[n];
}
public static void main(String[] args) {
int n = 3;
// Create an instance of Solution class
Solution sol = new Solution();
// Print the answer
System.out.println("The total number of ways: " + sol.climbStairs(n));
}
}
class Solution {
// Function to count total ways to reach nth stair
climbStairs(n) {
// Declare dp array of size n+1
let dp = new Array(n + 1).fill(-1);
// Insert the values of base conditions
dp[0] = 1;
dp[1] = 1;
// Iterate and update every index of dp array
for (let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
// Return the answer
return dp[n];
}
}
let n = 3;
// Create an instance of Solution class
let sol = new Solution();
// Print the answer
console.log("The total number of ways:", sol.climbStairs(n));
class Solution {
// Function to count total ways to reach nth stair
climbStairs(n) {
// Declare dp array of size n+1
let dp = new Array(n + 1).fill(-1);
// Insert the values of base conditions
dp[0] = 1;
dp[1] = 1;
// Iterate and update every index of dp array
for (let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
// Return the answer
return dp[n];
}
}
let n = 3;
// Create an instance of Solution class
let sol = new Solution();
// Print the answer
console.log("The total number of ways:", sol.climbStairs(n));
If we observe the relation dp[i] = dp[i-1] + dp[i-2], we notice that to calculate the current value at index i, we only need the previous two values. Using an entire array for just two values seems overhead. Instead, the work can be done using two variables to store the previous two values, achieving O(1) space complexity. In each iteration, the current value and the variables containing previous values can be updated.
#include <bits/stdc++.h>
using namespace std;
class Solution {
public:
// Function to count total ways to reach nth stair
int climbStairs(int n) {
/*Initialize two variables to
store previous results*/
int prev2 = 1;
int prev = 1;
//Iterate and update the variables
for (int i = 2; i <= n; i++) {
int cur_i = prev2 + prev;
prev2 = prev;
prev = cur_i;
}
//Return the answer as prev
return prev;
}
};
int main() {
int n = 3;
// Create an instance of Solution class
Solution sol;
// Print the answer
cout << "The total number of ways: " << sol.climbStairs(n) << endl;
return 0;
}
import java.util.*;
class Solution {
// Function to count total ways to reach nth stair
public int climbStairs(int n) {
/*Initialize two variables to
store previous results*/
int prev2 = 1;
int prev = 1;
//Iterate and update the variables
for (int i = 2; i <= n; i++) {
int cur_i = prev2 + prev;
prev2 = prev;
prev = cur_i;
}
//Return the answer as prev
return prev;
}
public static void main(String[] args) {
int n = 3;
// Create an instance of Solution class
Solution sol = new Solution();
// Print the answer
System.out.println("The total number of ways: " + sol.climbStairs(n));
}
}
class Solution:
# Function to count total ways to reach nth stair
def climbStairs(self, n):
"""Initialize two variables to
store previous results"""
prev2 = 1;
prev = 1;
#Iterate and update the variables
for i in range(2, n+1):
cur_i = prev2 + prev;
prev2 = prev;
prev = curr;
#Return the answer as prev
return prev;
if __name__ == "__main__":
n = 3
# Create an instance of Solution class
sol = Solution()
# Print the answer
print("The total number of ways:", sol.climbStairs(n))
class Solution {
// Function to count total ways to reach nth stair
climbStairs(n) {
/*Initialize two variables to
store previous results*/
let prev2 = 1;
let prev = 1;
//Iterate and update the variables
for (let i = 2; i <= n; i++) {
let cur_i = prev2 + prev;
prev2 = prev;
prev = curr;
}
//Return the answer as prev
return prev;
}
}
let n = 3;
// Create an instance of Solution class
let sol = new Solution();
// Print the answer
console.log("The total number of ways:", sol.climbStairs(n));
Q: Why is the Fibonacci sequence relevant here?
A: The recurrence relation f(n) = f(n-1) + f(n-2) follows the same pattern as the Fibonacci series because each step depends on the sum of the previous two step counts.
Q: What if we allow jumps of 3 steps instead of just 1 or 2?
A: The recurrence relation changes to f(n) = f(n-1) + f(n-2) + f(n-3), increasing the number of ways exponentially.
Q: How can this problem be solved iteratively instead of recursively?
A: Using a bottom-up approach with a loop to compute f(n) from f(0) to f(n), storing only the last two computed values (O(1) space).
Q: How would you modify this problem for a 2D grid, where you can move right or down instead of just forward?
A: The problem then transforms into counting paths in a matrix, where dp[i][j] = dp[i-1][j] + dp[i][j-1] to move right or down.