Home > Back-end >  Understanding subtleties of dynamic programming approaches
Understanding subtleties of dynamic programming approaches

Time:08-10

I understand that there are mainly two approaches to dynamic programming solutions:

  1. Fixed optimal order of evaluation (lets call it Foo approach): Foo approach usually goes from subproblems to bigger problems thus using results obtained earlier for subproblems to solve bigger problems, thus avoiding "revisiting" subproblem. CLRS also seem to call this as "Bottom Up" approach.

  2. Without fixed optimal order of evaluation (lets call it Non-Foo approach): In this approach evaluation proceeds from problems to their sub-problems . It ensures that sub problems are not "re-evaluated" (thus ensuring optimality) by maintaining results of their past evaluations in some data structure and then first checking if the result of the problem at hand exists in this data structure before starting its evaluation. CLRS seem to call this as "Top Down" approach

This is what is roughly conveyed as one of the main points by this answer.

I have following doubts:

Q1. Memoization or not?


CLRS uses terms "top down with memoization" approach and "bottom up" approach. I feel both approaches require memory to cache results of sub problems. But, then, why CLRS use term "memoization" only for top down approach and not for bottom up approach? After solving some problems by DP approach, I feel that solutions by top down approach for all problems require memory to caches results of "all" subproblems. However, that is not the case with bottom up approach. Solutions by bottom up approach for some problems does not need to cache results of "all" sub problems. Q1. Am I correct with this?

For example consider this problem:

Given cost[i] being the cost of ith step on a staircase, give the minimum cost of reaching the top of the floor if:

  • you can climb either one or two steps
  • you can start from the step with index 0, or the step with index 1

The top down approach solution is as follows:

class Solution: 
    def minCostAux(self, curStep, cost):

        if self.minCosts[curStep] > -1:
            return self.minCosts[curStep]
        
        if curStep == -1:
            return 0
        elif curStep == 0:
            self.minCosts[curStep] = cost[0] 
        else: 
            self.minCosts[curStep] = min(self.minCostAux(curStep-2, cost)   cost[curStep]
                                , self.minCostAux(curStep-1, cost)   cost[curStep]) 
        
        return self.minCosts[curStep]
        
    def minCostClimbingStairs(self, cost) -> int:
        cost.append(0)
        self.minCosts = [-1] * len(cost)
        return self.minCostAux(len(cost)-1, cost)

The bottom up approach solution is as follows:

class Solution:        
    def minCostClimbingStairs(self, cost) -> int:
        cost.append(0)
        
        secondLastMinCost = cost[0]
        lastMinCost = min(cost[0] cost[1], cost[1])
        
        minCost = lastMinCost
        
        for i in range(2,len(cost)):
            minCost = min(lastMinCost, secondLastMinCost)   cost[i]
            secondLastMinCost = lastMinCost
            lastMinCost = minCost
        
        return minCost

Note that the top down approach caches result of all steps in self.minCosts while bottom up approach caches result of only last two steps in variables lastMinCost and secondLastMinCost.

Q2. Does all problems have solutions by both approaches?


I feel no. I came to this opinion after solving this problem:

Find the probability that the knight will not go out of n x n chessboard after k moves, if the knight was initially kept in the cell at index (row, column).

I feel the only way to solve this problem is to find successive probabilities in increasing number of steps starting from cell (row, column), that is probability that the knight will not go out of chessboard after step 1, then after step 2, then after step 3 and so on. This is bottom up approach. We cannot do it top down, for example, we cannot start with kth step and go to k-1th step, then k-2th step and so on, because:

  1. We cannot know which cells will be reached in kth step to start with
  2. We cannot ensure that all paths from kth step will lead to initial knight cell position (row,column).

Even one of the top voted answer gives dp solution as follows:

class Solution {
    private int[][]dir = new int[][]{{-2,-1},{-1,-2},{1,-2},{2,-1},{2,1},{1,2},{-1,2},{-2,1}};
    private double[][][] dp;
    public double knightProbability(int N, int K, int r, int c) {
        dp = new double[N][N][K   1];
        return find(N,K,r,c);
    }
    public double find(int N,int K,int r,int c){
        if(r < 0 || r > N - 1 || c < 0 || c > N - 1) return 0;
        if(K == 0)  return 1;
        if(dp[r][c][K] != 0) return dp[r][c][K];
        double rate = 0;
        for(int i = 0;i < dir.length;i  )   rate  = 0.125 * find(N,K - 1,r   dir[i][0],c   dir[i][1]);
        dp[r][c][K] = rate;
        return rate;
    }
}

I feel this is still a bottom up approach since it starts with initial knight cell position (r,c) (and hence starts from 0th or no step to Kth step) despite the fact that it counts K downwads to 0. So, this is bottom up approach done recursively and not top down approach. To be precise, this solution does NOT first find:

  • probability of knight not going out of chessboard after K steps starting at cell (r,c)

and then find:

  • probability of knight not going out of chessboard after K-1 steps starting at cell (r,c)

but it finds in reverse / bottom up order: first for K-1 steps and then for K steps.

Also, I did not find any solutions in of top voted discussions in leetcode doing it in truly top down manner, starting from Kth step to 0th step ending in (row,column) cell, instead of starting with (row,column) cell.

Similarly we cannot solve the following problem with the bottom up approach but only with top down approach:

Find the probability that the Knight ends up in the cell at index (row,column) after K steps, starting at any initial cell.

Q2. So am I correct with my understanding that not all problems have solutions by both top down or bottom up approaches? Or am I just overthinking unnecessarily and both above problems can indeed be solved with both top down and bottom up approaches?

PS: I indeed seem to have done overthinking here: knightProbability() function above is indeed top down, and I ill-interpreted as explained in detailed above

  • Related