Dynamic Programming – From Recursive to Iteration – Solution with 1D Matrix – Part 2

Introduction

I continue to analyze how to construct 1D matrix and use iteration technique from a decision. In this blog, I attempt to focus on how each cell in 2D or 1D matrix should match with the nodes in a decision tree. Thus if we can intuitively come up with a good decision tree, we can implement iteration technique, which I think provides easier way to debug, and is also more efficient.

Not every decision tree are equal. For some problems, we could re-analyse the state variables and values to come up with cleaner relation.

In this blog, I solve problems with recursive solutions. But for iterative solutions, I have to "rethink" simpler decision tree.

Problem Number of Dice Rolls With Target Sum

I will attempt the problem Number of Dice Rolls With Target Sum with a recursive solution that solves only 40/60 test cases.
Then I will reconstruct a different decision tree and implement with recursive solution (Top down), and iteration solution (Bottom up) using 2D DP array.

Finally I represent a best optimized solution using 1D DP array and Prefix sum technique.

Recursive

First I com up with the following decision tree:

  • each level has the output rolling a dice. We have n dices, thus n level.
  • each parent node has k possible outcomes, because each roll can result in any face. We have k faces, thus each parent node has the same k branches.
  • base case happen at leaf nodes:
    • either all dices have been rolled, and the sum is still not the target, return zero or the sum is greater than the target before all dices have been rolled.
    • all dice have been rolled and the sum is same as the target, return 1, as we found away.
  • Since each node is represented by the diceIndex and the faceIndex, I use 2D array as memoization.

Using the above trees, I have the following code implementation:

class Solution {
    const int MOD = 10000000;
    int roll(int& n, int& k, int& target, int diceIndex, int currFaceIndex, int sum, vector<vector<int>>& memo ){
        // base case:
        if (diceIndex == n && sum == target)
            return 1;
        if (diceIndex >= n || sum > target)
            return 0;
        // memoization:
        if (memo[diceIndex][currFaceIndex] !=-1)
            return memo[diceIndex][currFaceIndex];
        // relation:
        int count =0;
        for( int faceIndex =1 ; faceIndex <= k; faceIndex++){
            count += roll(n,k, target, diceIndex+1, faceIndex, sum+faceIndex, memo) % MOD;
        }
        return memo[diceIndex][currFaceIndex] = count;
    }
public:
    int numRollsToTarget(int n, int k, int target) {
        vector<vector<int>> memo (n+1, vector<int>(k+1, -1));
        return roll(n,k,target, 1, 1, 0, memo );
    }
};

The above solution fixes only 40/60 test cases !!! The error is (to be added)

Rethink the decision tree

Now I decide the change the state variables:

  • each node is represented by the diceIndex and accumulated sum (currSum)
    • we still have n levels for we have n dices.
    • accumulated sum is calculated as previous node sum added with the face value.
    • the return value of each node is the count of possible ways.
  • base conditions are still same as above approach:
    • if diceIndex == n and currSum = target.

Note the following logic are the same:

// base case:
   if (sum == target && diceIndex == n)
       return 1;
       else return 0;
// same as: 
   if ( diceIndex == n)
       return sum == target;
// same as:
   if (diceIndex == n && sum == target)
       return 1;
   if (diceIndex >= n || sum > target)
       return 0;

The following illustrates the decision tree:

Iteration with 2DP array.

From above analysis we can see that

  • each node is represented by 2 state variables diceIndex and currSum
  • each node stores the value of count, or how many ways to reach target sum.

Therefore we could construct 2D arrays to represent the same nodes.

Instead of using recursive call, we could use simple formula to update the cells:

  1. parent cell = dp[diceIndex][currSum]
  2. child cell = dp[diceIndex-1][currSum - currFaceIndex] = parent cell + 0 if parent cell is 1 or dp[0][0]=1

Pay attention to the subtractions! With the above formular, the currSum will get progressively larger at the child nodes, the count values also get larger and larger!!! I will get the result at the rightmost bottom leaf nodes or dp[n][target]!!!

To map the value of DP array with decision tree node, I use test case n=2, k=3 and target=6. The following illustrate how each dp cell matches with the tree node.

class Solution {
public:
    const int MOD = 1e9 + 7;
    
    int numRollsToTarget(int n, int k, int target) {
        vector<vector<int>> dp(n + 1, vector<int>(target + 1, 0));
        // Initialize the base case
        dp[0][0] = 1;
        
        for (int diceIndex = 1; diceIndex <= n; diceIndex++) {
            for (int currSum = 0; currSum <= target; currSum++) {
                int ways = 0;
                
                // Iterate over the possible face value for current dice
                for (int i = 1; i <= k; i++) {
                    if (currSum - i >= 0) {
                        ways = (ways + dp[diceIndex - 1][currSum - i]) % MOD;
                    }
                }
                
                dp[diceIndex][currSum] = ways;
            }
        }
        
        return dp[n][target];
    }
};

Notes: It is especially tricky but vital construct correct values for:

  • the root node or dp[0][0]
  • how each node or dp[diceIndex][currSum] cell should be updated, and if the child nodes should be calculated from the parent nodes
  • which cell gives the final answer. Is it dp[n][target] or dp[0][0]

Iteration - 1D DP array

Instead of using 2D arrays, we could use two 1D arrays.

  • observe that each row in 2D arrays corresponds to a level in the decision tree
  • each child row have its cells updated and calculated by cells in higher/previous rows.

Therefore, why not just use two 1D arrays:

  • a newDp array to calculate the cells of each rows.
  • a Dp array to store the cells value of the previous row. Because cell is calculated by sum.
#include <vector>
#include <algorithm>

class Solution {
public:
    int numRollsToTarget(int n, int k, int target) {
        const int MOD = 1000000007;
        
        // Initialize an array to store the number of ways to achieve each sum
        std::vector<int> dp(target + 1, 0);
        dp[0] = 1;

        // Iterate over the number of dice
        for (int i = 1; i <= n; i++) {
            // Create a new array to store the updated number of ways for the current dice count
            std::vector<int> newDp(target + 1, 0);
            int sum = 0; // Variable to calculate the prefix sum

            // Iterate over possible sums
            for (int j = 1; j <= target; j++) {
                // Calculate the number of ways to achieve the current sum using 'i' dice
                // Update sum with the current element from the previous row
                sum = (sum + dp[j - 1]) % MOD;

                // If the current sum is greater than k, subtract the contribution from k+1 steps back
                if (j >= k + 1) {
                    sum = (sum - dp[j - k - 1] + MOD) % MOD;
                }

                // Update the newDp array with the calculated sum
                newDp[j] = sum;
            }

            // Update dp array with the newDp array for the next iteration
            std::copy(newDp.begin(), newDp.end(), dp.begin());
        }

        return dp[target];
    }
};

Problem Paint Fence

This problem is very similar to the problem Paint House. With that problem it is very intuitive to construct a 2D array to calculate the cost of painting houses. After we reiterate down the row (based on the number of houses), and over the columns (based on the number of color), we can update each cells (cost of painting based on the values in previous row. That problem is also similar to problem Calculating Min Path in a Grid or Min Falling Path in a grid.

But for this problem, it is not possible to construct 2D grids using the same approach. I start here with a recursive solution and derive a bottom up, iteration solution with 1D array.

Recursive solution (Top down)

Recursive solution attempts to construct a decision tree. The function colorFence is defined to calculate the number of ways to paint the fence starting from a given fenceIndex with a specific colorIndex and a count of consecutive same colors (sameColor).

Image to be updated

  • Base case: If fenceIndex equals n, it means all fences are painted, so it returns 1
  • Recursive case:
    • For each fence, there are all k color choices, except when 2 previous fence are of the same color. Thus we reiterate from color index = 0 to color index = k.
    • To go to child node/next fence, we could use the same color, but have to increment sameColor count. Thus we make recursive call
ways += colorFence(fenceIndex + 1, color, n, k, sameColor + 1, cache)
  • However, for case that 2 previous fences are of the same color, we still have to paint the next fence (move to next child node) but reset sameColor to 1. The next next is painted with what ever color on the current loop.
ways += colorFence(fenceIndex + 1, color, n, k, 1, cache); // Reset sameColor to 1
  • Memoization needs 3D array! After much trial and error, I figured that it is required.

Code Implementation:

class Solution {
    int colorFence(int fenceIndex, int colorIndex, const int &n, const int &k, int sameColor, vector<vector<vector<int>>>& cache) {
        // base case
        if (fenceIndex == n) { // Changed from n-1 to n
            return 1; // Ensuring valid configurations return 1
        }
        // check memoization
        if (cache[fenceIndex][colorIndex][sameColor] != -1) { // Changed from 0 to -1
            return cache[fenceIndex][colorIndex][sameColor]; // Updated to use 3D cache
        }
        
        int ways = 0;
        for (int color = 0; color < k; color++) {
            if (color == colorIndex) {
                if (sameColor + 1 < 3) { // Ensuring no more than two consecutive same colors
                    ways += colorFence(fenceIndex + 1, color, n, k, sameColor + 1, cache); // Updated recursive call
                }
                    
            } else {
                ways += colorFence(fenceIndex + 1, color, n, k, 1, cache); // Reset sameColor to 1
            }
        }
        return cache[fenceIndex][colorIndex][sameColor] = ways; // Updated to use 3D cache
    }
public:
    int numWays(int n, int k) {
        vector<vector<vector<int>>> cache(n, vector<vector<int>>(k, vector<int>(3, -1))); // Changed cache initialization
        int ways = 0;
        for (int color = 0; color < k; color++) {
            ways += colorFence(1, color, n, k, 1, cache); // Start from fenceIndex 1
        }
        return ways;
    }
};

The above solution is very cumbersome.

Rethink the math:

Earlier, our approach has been based on the choices that we have at each fence. Thus each node/state, we have the fenceIndex, and colorIndex. The value of each node is derived from the child node plus 1.

If we have n fence and k colors, without constrain, the total way is n*k.

  1. Use a different color than the previous post. If we use a different color, then there are k - 1 colors for us to use. This means there are (k - 1) * totalWays(i - 1) ways to paint the ith post a different color than the (i−1)th post.
  2. Use the same color as the previous post. There is only one color for us to use, so there are 1 * totalWays(i - 1) ways to paint the ith post the same color as the (i−1)th post.
  3. Because of the constrain that no more than 2 adjacent fence can be painted the same color, there are (k - 1) * totalWays(i - 2) ways to paint the the ith post a different color than the (i−2)th post.
  4. Overally, we have totalWays(i) = (k - 1) * (totalWays(i - 1) + totalWays(i - 2))

Iteration

From the above analysis, we could make a dp array, and update each cell of the array during iteration, using the formular dp[i] = (dp[i-1] + dp[i-2]) * (k-1)

class Solution {
    public: 
    int numWays(int n, int k) {
        // edge cases
        if (n == 0) return 0;
        if (n == 1) return k;
        if (n == 2) return k*k;
        // base cases
        int dp[n+1];
        dp[0] = 0;
        dp[1] = k;
        dp[2] = k*k;
        for (int i = 3; i <= n; i++) {
            // the recursive formula that we derived
            dp[i] = (dp[i-1] + dp[i-2]) * (k-1);
        }
        return dp[n];
    }
};
// source: https://leetcode.com/problems/paint-fence/submissions/1373350360

Problem Minimum Cost for Tickets

In order to construct 1D DP array for iteration technique, a very good decision tree should be made, because the cells in 1D DP array closely matches the nodes/states in decision tree.

As shown in early section, once we come up with a math formular to calculate state values, which also makes our decision tree cleaner, the relation between nodes simpler, we could easily come up with almost the same equation to calculate each cell in the 1D DP array.

In this problem, I come up with this decision tree at first.

  • Each node represents the cost of tickets at each nums[i]. The state variable is the cost of tickets.
  • Each node returns value which the cost of days[i].

image to be updated

But the decision tree is hard to implement because in other to go to each child node and return values, I have to check nums for the nums[i] = day using ticket. So why not use the days as the state variables? With that I have the following decision tree:

  • each node state variable is the actual day. 1, ..... days[days.size() -1].
  • each node return values, depending on the previous purchases.
    • If the day is 5 for example, then the 7 days ticket has been bought, then the cost is just 7+0 on the dp array: dp[7] = dp[5] + 0;
    • because we have 3 choices, thus on each day, the cost is the minim of the previous 3 scenarios:
dp[day] = Min(dp[day - 1] + costs[0], dp[day - 7] + costs[1], dp[day - 30] + costs[2];

The relation equation is much simplier to implement in both recursive solution and iterative solution.

  • The final cost is the dp[lastDay]

Code implementation:

  • initialize dp array whose size is the last day of days array.
    • all other values of days array doesn't matter in this problem. We only care about the last day.
  • reiterate until total day = lastDay.
  • If the current day is not a travel day (day < days[i]), the cost remains the same as the previous day (dp[day] = dp[day - 1]).
  • If the current day is a travel day (day == days[i]), the program calculates the minimum cost by considering three options:
    • Buying a 1-day ticket: dp[day - 1] + costs[0]
    • Buying a 7-day ticket: dp[max(0, day - 7)] + costs[1]
    • Buying a 30-day ticket: dp[max(0, day - 30)] + costs[2]
    • Note that the max(0, day - 7) and max(0, day - 30) ensure that we do not access negative indices in the dp array.
class Solution {
public:
    int mincostTickets(vector<int>& days, vector<int>& costs) {
        int lastDay = days[days.size() - 1];
        vector<int> dp(lastDay + 1, 0);
        
        int i = 0;
        for (int day = 1; day <= lastDay; day++) {
            if (day < days[i]) {
                dp[day] = dp[day - 1];
            } else {
                i++;
                dp[day] = min({dp[day - 1] + costs[0],
                               dp[max(0, day - 7)] + costs[1],
                               dp[max(0, day - 30)] + costs[2]});
            }
        }
     
        return dp[lastDay];
    }
};

Suppose we have inputs

  • days = [1, 4, 6, 7, 8, 20]
  • costs = [2, 7, 15]

The dp array will be updated as follows:

  • dp[1] = min(2) = 2 (1-day ticket)
  • dp[2] = dp[1] (no travel day)
  • dp[3] = dp[2] = 2 (no travel day)
  • dp[4] = min(4 + 2, 0 + 7, 0 + 15) = 4 (1-day ticket for each day)
  • dp[6] = min(6 + 2, 0 + 7, 0 + 15) = 6 (1-day ticket for each day)
  • dp[7] = min(7 + 2, 0 + 7, 0 + 15) = 7 (1-day ticket for each day)
  • dp[9] = 7 (no travel day)

Leave a Reply

Your email address will not be published. Required fields are marked *