Day 26: Dynamic Programming Basics

Day 26: Dynamic Programming Basics

Welcome to Day 26 of my 100 Days of DSA challenge! Dynamic Programming (DP) stands out as a cornerstone technique for solving complex problems by breaking them into overlapping subproblems and reusing previously computed results. Today, I tackled foundational DP problems to solidify core concepts.

Check out my GitHub repository for all the solutions and progress updates at: 100 Days of DSA

Let’s continue this exciting journey! 🚀


1. Fibonacci Sequence Problem using Recursion, Memoization, and Tabulation

This program calculates Fibonacci numbers using three different methods: recursion, memoization, and tabulation.

  • The recursive approach computes Fibonacci numbers by breaking the problem into smaller subproblems, but it is inefficient due to repeated calculations.

  • The memoization approach improves efficiency by storing previously computed values in an array to avoid redundant calculations.

  • The tabulation approach uses an iterative method with a bottom-up approach, filling an array to compute the Fibonacci numbers sequentially.

Each method demonstrates a distinct way to solve the problem with varying levels of efficiency.

Code:

#include <iostream>
#include <cstring> 
using namespace std;

#define MAX 1000    // Maximum number for Fibonacci sequence

// Recursive approach
int fibonacci_recursive(int n) {
    if (n <= 1) {
        return n;   // Base cases: Fib(0) = 0, Fib(1) = 1
    }
    return fibonacci_recursive(n - 1) + fibonacci_recursive(n - 2);
}

// Memoization approach
int memo[MAX];      // Array for memoization

int fibonacci_memoization(int n) {
    if (n <= 1) {
        return n;   // Base cases: Fib(0) = 0, Fib(1) = 1
    }
    if (memo[n] != -1) {
        return memo[n]; // Return already computed value
    }
    memo[n] = fibonacci_memoization(n - 1) + fibonacci_memoization(n - 2);
    return memo[n];
}

// Tabulation approach
int fibonacci_tabulation(int n) {
    int dp[MAX] = {0}; // Array for storing Fibonacci numbers
    dp[0] = 0;         // Base case: Fib(0) = 0
    dp[1] = 1;         // Base case: Fib(1) = 1

    for (int i = 2; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];  // Fill the table iteratively
    }
    return dp[n];
}

int main() {
    int n;
    cout << "Enter the value of n: ";
    cin >> n;
    // Recursive solution
    cout << "Fibonacci using recursion: " << fibonacci_recursive(n) << endl;
    // Memoization solution
    memset(memo, -1, sizeof(memo)); // Initialize memoization array
    cout << "Fibonacci using memoization: " << fibonacci_memoization(n) << endl;
    // Tabulation solution
    cout << "Fibonacci using tabulation: " << fibonacci_tabulation(n) << endl;
    return 0;
}

Output:


2. Climbing Stairs Problem

This program solves the "Climbing Stairs" problem using dynamic programming with two approaches: tabulation and memoization.

  • In the tabulation approach, it uses a bottom-up method to calculate the number of ways to climb n steps iteratively, storing intermediate results in an array.

  • In the memoization approach, it uses recursion and stores previously computed results in an array to avoid redundant calculations.

Both methods rely on the recurrence relation dp[i] = dp[i−1] + dp[i−2], representing the number of ways to reach the i-th step from the two preceding steps.

Code:

#include <iostream>
#include <cstring> 
using namespace std;

#define MAX 1000    // Maximum number of steps

// Function to calculate the number of ways to climb stairs using tabulation
int climbing_stairs_tabulation(int n) {
    if (n <= 1) {
        return 1;   // Base cases: 1 way to climb 0 or 1 step
    }

    int dp[MAX] = {0};  // Array to store the number of ways to climb each step
    dp[0] = 1;          // Base case: 1 way to climb 0 steps
    dp[1] = 1;          // Base case: 1 way to climb 1 step

    for (int i = 2; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];  // Transition relation
    }

    return dp[n];
}

// Function to calculate the number of ways to climb stairs using memoization
int memo[MAX];  // Array for memoization

int climbing_stairs_memoization(int n) {
    if (n <= 1) {
        return 1;   // Base cases: 1 way to climb 0 or 1 step
    }

    if (memo[n] != -1) {
        return memo[n];     // Return already computed value
    }

    memo[n] = climbing_stairs_memoization(n - 1) + climbing_stairs_memoization(n - 2);
    return memo[n];
}

int main() {
    int n;
    cout << "Enter the number of steps: ";
    cin >> n;
    // Tabulation solution
    cout << "Number of ways using tabulation: " << climbing_stairs_tabulation(n) << endl;
    // Memoization solution
    memset(memo, -1, sizeof(memo));     // Initialize memoization array
    cout << "Number of ways using memoization: " << climbing_stairs_memoization(n) << endl;
    return 0;
}

Output:


3. House Robber Problem

This program solves the "House Robber" problem using dynamic programming with two approaches: tabulation and memoization.

The tabulation approach iteratively computes the maximum profit for robbing houses up to each index, using a bottom-up array-based method.

The memoization approach uses recursion and stores intermediate results in an array to avoid redundant calculations.

Both methods use the recurrence dp[i] = max⁡(dp[i−1], dp[i−2] + houses[i]) to decide whether to rob or skip the current house for maximum profit.

Code:

#include <iostream>
#include <cstring> 
using namespace std;

#define MAX 1000    // Maximum number of houses

// Function to solve the House Robber problem using tabulation
int house_robber_tabulation(int houses[], int n) {
    if (n == 0) {
        return 0;   // No houses to rob
    }
    if (n == 1) {
        return houses[0];   // Only one house to rob
    }

    int dp[MAX] = {0};  // Array to store the maximum profit up to each house
    dp[0] = houses[0];  // Base case: Rob the first house
    dp[1] = max(houses[0], houses[1]);  // Base case: Max of robbing first or second house

    for (int i = 2; i < n; i++) {
        dp[i] = max(dp[i - 1], dp[i - 2] + houses[i]);  // Choose to rob or skip the current house
    }

    return dp[n - 1];
}

// Function to solve the House Robber problem using memoization
int memo[MAX];  // Array for memoization

int house_robber_memoization(int houses[], int n) {
    if (n == 0) {
        return 0;   // No houses to rob
    }
    if (n == 1) {
        return houses[0];   // Only one house to rob
    }
    if (memo[n] != -1) {
        return memo[n];     // Return the precomputed result
    }

    memo[n] = max(house_robber_memoization(houses, n - 1), house_robber_memoization(houses, n - 2) + houses[n - 1]);
    return memo[n];
}

int main() {
    int houses[] = {2, 7, 9, 3, 1}; // Amount of money in each house
    int n = sizeof(houses) / sizeof(houses[0]);
    // Tabulation solution
    cout << "Maximum profit using tabulation: " << house_robber_tabulation(houses, n) << endl;
    // Memoization solution
    memset(memo, -1, sizeof(memo)); // Initialize memoization array
    cout << "Maximum profit using memoization: " << house_robber_memoization(houses, n) << endl;
    return 0;
}

Output:


4. Minimum Cost Path in a Grid

This program calculates the minimum cost to traverse a grid from the top-left corner to the bottom-right corner using dynamic programming. It initializes a 2D array dp to store the minimum cost to reach each cell. The first row and first column are computed as cumulative sums since they can only be reached from one direction. For other cells, the cost is determined by adding the grid's value at that cell to the minimum cost of reaching it from either the top or the left. The result is stored in the bottom-right cell of the dp array.

Code:

#include <iostream>
#include <climits>
using namespace std;

#define MAX 100     // Maximum grid size

// Function to find the minimum cost path in a grid using dynamic programming
int min_cost_path(int grid[MAX][MAX], int rows, int cols) {
    int dp[MAX][MAX];   // Array to store the minimum cost to each cell

    // Initialize the base case for the top-left corner
    dp[0][0] = grid[0][0];

    // Fill the first row
    for (int j = 1; j < cols; j++) {
        dp[0][j] = dp[0][j - 1] + grid[0][j];
    }

    // Fill the first column
    for (int i = 1; i < rows; i++) {
        dp[i][0] = dp[i - 1][0] + grid[i][0];
    }

    // Fill the rest of the grid
    for (int i = 1; i < rows; i++) {
        for (int j = 1; j < cols; j++) {
            dp[i][j] = grid[i][j] + min(dp[i - 1][j], dp[i][j - 1]);
        }
    }

    // Return the minimum cost to reach the bottom-right corner
    return dp[rows - 1][cols - 1];
}

int main() {
    int grid[MAX][MAX] = {
        {1, 3, 1},
        {1, 5, 1},
        {4, 2, 1}
    };
    int rows = 3, cols = 3; 
    cout << "The minimum cost to reach the bottom-right corner is: " << min_cost_path(grid, rows, cols);
    return 0;
}

Output:


5. Maximum Sum of Non-Adjacent Elements in an Array

This program calculates the maximum sum of non-adjacent elements in an array using dynamic programming. It uses a dp array where each element represents the maximum sum that can be obtained from the start of the array to that index, while ensuring no two adjacent elements are included. The program iterates through the array, deciding at each step whether to include the current element or skip it, based on which choice provides a higher sum. The final result is the maximum sum obtainable by non-adjacent elements in the array.

Code:

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

#define MAX 1000    // Maximum array size

// Function to find the maximum sum of non-adjacent elements using dynamic programming
int max_sum_non_adjacent(int arr[], int n) {
    if (n == 0) return 0;       // No elements, no sum
    if (n == 1) return arr[0];  // Only one element, return it

    int dp[MAX]; // Array to store the maximum sum up to each index
    memset(dp, 0, sizeof(dp));  // Initialize with 0

    dp[0] = max(0, arr[0]);     // Base case: Maximum sum at the first element
    dp[1] = max(dp[0], arr[1]); // Base case: Maximum sum at the second element

    // Fill the dp array
    for (int i = 2; i < n; i++) {
        dp[i] = max(dp[i - 1], dp[i - 2] + arr[i]); // Include or exclude the current element
    }

    return dp[n - 1];   // Maximum sum up to the last element
}

int main() {
    int arr[] = {3, 2, 7, 10}; 
    int n = sizeof(arr) / sizeof(arr[0]);
    cout << "Maximum sum of non-adjacent elements: " << max_sum_non_adjacent(arr, n) << endl;
    return 0;
}

Output:


Day 26 reinforced the power of dynamic programming in solving optimization problems by breaking them down into manageable subproblems. Understanding key techniques like memoization and tabulation has laid a strong foundation for tackling more complex DP challenges ahead. 🚀