Recursion, Memoization and Dynamic Programming : Min Steps to reduce a number to 1

Recursion, Memoization and Dynamic Programming : Min Steps to reduce a number to 1

·

7 min read

Given a number, you need to tell the minimum number of steps required to reduce the number down to 1.

The following 3 operations are allowed.

1) subtract 1
2) if divisible by 2 divide it by 2
3) if divisible by 3 divide it by 3

At first, you might feel greedy approach would fetch the optimal solution. By working through some examples, one can easily observe that for n = 10, following the greedy approach will unravel the following steps.

10 / 2 = 5 
5 - 1 = 4
4 / 2 = 2
2 / 2 = 1

This would account for 4 steps. However, the problem could be solved in 3 steps.

10 - 1 = 9
9 / 3 = 3
3 / 3 = 1

Hence, we would be looking for other approaches to find the optimal solution efficiently.

Let us observe the breakdown of this problem into sub-problems to come up with the base cases/conditions.

min_steps_one.jpg

The base case would be 1 as to reduce 1 to itself would involve no steps and hence will be assigned a value of 0.

1. Recursion

int minStepsOne(int n) {
    if(n == 1)
        return 0;

    int x = minStepsOne(n - 1);
    int y = INT_MAX;
    int z = INT_MAX;

    if(n % 2 == 0) 
        y = minStepsOne(n / 2);
    if(n % 3 == 0)
        z = minStepsOne(n / 3);

    return 1 + min(x, min(y, z));
}

Following the recursive approach could lead to a time complexity of O(3^n) and is often not an efficient solution. A simple reason to support the aforementioned fact is that there are certain values which are computed again and again. This actually increases the depth of the recursive calls leading to an increase in the time complexity.

2. Memoization

To encounter this problem, we will store the output of the previously encountered values preferably in arrays as these are the most efficient to traverse and extract the data. This way, we can improve the running time of our code.

This process of storing each recursive call's output and then using them for further calculations preventing the code from calculating these again is called Memoization.

Memoization is a top-down approach, where we save the previous solutions so that they could be used to calculate the future solutions and improve the time complexity to a greater extent.

To modify the recursive solution, we need to create an array and assign -1 initially. memset(arr, -1, sizeof(arr)); -1 means that the required value has not yet been computed.

Note: Memset works for initializing the arrays with values 0 and -1 only. memset(arr, 5, sizeof(arr)) This code would not assign a value of 5 and instead creates garbage value.

We will store the results in the array and before creating a recursive call we will first check whether the required solution is computed earlier. In such a case, we could skip the further calls and assign the value directly.

This drastically decreases the depth of the recursion tree.

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

int minStepsOne(int n, int *memo) {
    int res;
    if(n == 1) // base case
        return 0;

    if(memo[n] != -1) // the value has been computed earlier
        return memo[n];

    res = minStepsOne(n - 1, memo);
    if(n % 2 == 0)
        res = min(res, minStepsOne(n / 2, memo));
    if(n % 3 == 0)
        res = min(res, minStepsOne(n / 3, memo));

    // store the result in the array
    memo[n] = 1 + res;
    return memo[n];
}

int main() {

    int n;
    cin >> n;

    int memo[n + 1];
    memset(memo, -1, sizeof(memo));

    cout << minStepsOne(n, memo);
    return 0;
}

The total number of unique calls would be atmost n only.

3. Dynamic Programming

Dynamic Programming follows a bottom-up approach to reach the desired index. This approach of converting a recursion problem into iteration is called Dynamic Programming.

int minStepsOne(int n) {
    int dp[n + 1];
    dp[1] = 0; // base condition

    for(int i = 2; i <= n; i++) {
        int res = dp[i - 1]; 
        if(i % 2 == 0) 
            res = min(res, dp[i / 2]);
        if(i % 3 == 0)
            res = min(res, dp[i / 3]);
        dp[i] = 1 + res;
    }
    return dp[n];
}

General Approach

  1. Figure out the naive approach using Recursion.
  2. Try to store the intermediate results to avoid re-computation of values using Memoization.
  3. Finally replace recursion by simple iteration using Dynamic Programming.

The last step could be a bit tricky but by looking at various test cases, the relationship between a problem and its sub-problem could be established which plays a key role in formulating Dynamic Programming problems.

That's all for this article. Hope you were able to understand the flow to solve such problems. Connect with me on github and linkedin .