Wednesday, November 20, 2024
Google search engine
HomeData Modelling & AIKnuth’s Optimization in Dynamic Programming

Knuth’s Optimization in Dynamic Programming

Knuth’s optimization is a very powerful tool in dynamic programming, that can be used to reduce the time complexity of the solutions primarily from O(N3) to O(N2). Normally, it is used for problems that can be solved using range DP, assuming certain conditions are satisfied. In this article, we will first explore the optimization itself, and then solve a problem with it.

Optimization Criteria:

Knuth’s optimization is applied to DP problems with a transition of the following form –

dp[i][j] = cost[i][j] + mini≤k<j(dp[i][k] + dp[k+1][j])

Further, the cost function must satisfy the following conditions (for a ≤ b ≤ c ≤ d) –

  1. It is a Monotone on the lattice of intervals (MLI) [cost(b, c) ≤ cost(a, d)]
  2. It satisfies the quadrangle inequality (QI) [cost(a, c) + cost(b, d) ≤ cost(b, c) + cost(a, d)]

Optimization:

For a solution that runs in O(N3) time, we would iterate through all values of k from i to j-1. However, we can do better than this using the following optimization:

  • Let’s define another array in addition to the dp array – opt[N][N]. Define opt[i][j] as the maximum (or minimum) value of k for which dp[i][j] is minimized in the dp transition.
    opt[i][j] = argmini≤k<j(dp[i][k] + dp[k+1][j])
  • The key to Knuth’s optimization, and several other optimizations in DP such as the Divide and Conquer optimization (not to be confused with the divide and conquer algorithm explained in this article), is the following inequality –

opt[i][j-1] ≤ opt[i][j] ≤ opt[i+1][j]

  • This helps because now, for each dp[] transition to calculate dp[i][j], we only have to test values of k between opt[i][j-1] and opt[i+1][j] instead in between i to j-1. This reduces the time complexity of the algorithm by a linear factor, which is a significant improvement.

Proof of Correctness:

To prove the correctness of this algorithm, we only need to prove the inequality

opt[i][j-1] ≤ opt[i][j] ≤ opt[i+1][j].

Follow the below section for the proof of the correctness of the algorithm:

Assumptions: If cost(i, j) is an MLI and satisfies the Quadrangle Inequality, then dp[i][j] also satisfies the inequality.  
Now, consider the following setup – 

  • For i < j, we have some indices i ≤ p ≤ q < j-1. 
  • Let dpk[i][j] = cost[i][j] + dp[i][k] + dp[k+1][j]

If we can show that:

dpp[i][j-1] ≥ dpq[i][j-1] ⇒ dpp[i][j] ≥ dpq[i][j] 

then setting q = opt[i][j-1], it will be clear that opt[i][j] will be at least as much as opt[i][j-1], due to the implication of the above inequality for all indices p less than opt[i][j-1]
This will prove that opt[i][j-1] ≤ opt[i][j].

Proof: Using the quadrangle inequality on the dp array, we get –

dp[p+1][j-1] + dp[q+1][j] ≤ dp[q+1][j-1] + dp[p+1][j]
⇒ (dp[i, p] + dp[p+1][j-1] + cost(i, j-1)) + (dp[i, q] + dp[q+1][j] + cost(i, j))
     ≤ (dp[i, q] + dp[q+1][j-1] + cost(i, j-1)) + (dp[i, p] + dp[p+1][j] + cost(i, j))
⇒ dpp[i][j-1] + dpq[i][j] ≤ dpp[i][j] + dpq[i][j-1]
⇒ dpp[i][j-1] – dpq[i][j-1] ≤ dpp[i][j] – dpq[i][j]

dpp[i][j-1] ≥ dpq[i][j-1] 
⇒ 0 ≤ dpp[i][j-1] – dpq[i][j-1] ≤ dpp[i][j] – dpq[i][j] 
⇒ dpp[i][j] ≥ dpq[i][j]. 

Hence it is proved that: dpp[i][j-1] ≥ dpq[i][j-1] ⇒ dpp[i][j] ≥ dpq[i][j]

The second part of the inequality (i.e. opt[i][j] ≤ opt[i+1][j]) can be shown with the same idea, starting with the inequality

dp[i][p]+dp[i+1][q] ≤ dp[i][q]+dp[i+1][p].

The proof of these two inequalities combinedly gives: opt[i][j-1] ≤ opt[i][j] ≤ opt[i+1][j]

Example:

Given an array arr[] of N elements, the task is to find the minimum cost for reducing the array to a single element in N-1 operations where in each operation: 

  • Delete the elements at indices i and i+1 for some valid index i, replacing them with their sum. 
  • The cost of doing so is arr[i] + arr[i+1], where arr[] is the array state just before the operation.
  • This cost will be added to the final cost.

Examples: 

Input: arr[] = {3, 4, 2, 1, 7}
Output: 37
Explanation: 
Remove the elements at 0th and 1st index. arr[] = {7, 2, 1, 7}, Cost = 3 + 4 = 7
Remove 1st and 2nd index elements. arr[] = {7, 3, 7}, Cost = 2 + 1 = 3
Remove 1st and 2nd index elements, arr[] = {7, 10}, Cost = 3 + 7 = 10
Remove the last two elements. arr[] = {17}, Cost =  = 7 + 10 = 17
Total cost = 7 + 3 + 10 + 17 = 37
This is the minimum possible total cost for this array.

Input: arr[] = {1, 2, 3, 4}
Output: 19
Explanation: 
Remove the 0th and 1st index elements. arr[] = {3, 3, 4}. Cost = 1 + 2 = 3
Remove the 0th and 1st index elements. arr[] = {6, 4}. Cost = 3 + 3 = 6
Remove the 0th and 1st index elements. arr[] = {10}. Cost = 6 + 4 = 10
Total cost = 3 + 6 + 10 = 19.
This is the minimi=um possible cost.

 

Sub-optimal solution (using Range DP): The problem can be solved using the following idea: 

  • Let arr[] be the original array before any modifications are made. 
  • For an element in the array that has been derived from indices i to j of a[], the cost of the final operation to form this single element will be the sum arr[i] + arr[i+1] + . . . + arr[j]. Let this value be denoted by the function cost(i, j).
  • To find the minimum cost for the section arr[i, i+1, … j], consider the cost of converting the pairs of sub-arrays arr[i, i+1 . . . k] & arr[k+1, k+2 . . . j] into single elements, and choose the minimum over all possible values of k from i to j-1 (both inclusive).

For implementing the above idea:

  • The cost function can be calculated in constant time with preprocessing, using a prefix sum array:
    • Calculate prefix sum (say stored in pref[] array).
    • So cost(i, j) can be calculated as (pref[j] – pref[i-1]).
  • Traverse from i = 0 to N-1:
    • Traverse j =  i+1 to N-1 to generate all the subarray of the main array:
      • Solve this problem for all these possible subarrays with the following dp transition – 
        dp[i][j] = cost(i, j) + mini≤k≤j-1(dp[i][k] + dp[k+1][j]) as explained in the above idea.
  • Here dp[i][j] is the minimum cost of applying (j – i) operations on the sub-array arr[i, i+1, . . . j] to convert it to a single element. 
    cost(i, j) denotes the cost of the final operation i.e. the cost of adding the last two values to convert arr[i, i+1, . . ., j] to a single element.
  • The final answer will be stored on dp[0][N-1].

Below is the implementation of the above approach.

C++




// C++ code to implement the approach
 
#include <bits/stdc++.h>
using namespace std;
 
// Function to find the minimum cst
int minCost(int arr[], int N)
{
    // Creating the prefix sum array
    int pref[N+1], dp[N][N];
    pref[0] = 0;
    memset(dp, 0, sizeof(dp));
   
    // Loop to calculate the prefix sum
    for (int i = 0; i < N; i++) {
        pref[i + 1] = pref[i] + arr[i];
    }
 
    // Iterating through all subarrays
    // of length 2 or greater
    for (int i = N - 2; i >= 0; i--) {
        for (int j = i + 1; j < N; j++) {
             
            // Cost function = sum of
            // all elements in the subarray
            int cost = pref[j + 1] - pref[i];
            dp[i][j] = INT_MAX;
            for (int k = i; k < j; k++) {
                 
                // dp transition
                dp[i][j]
                = min(dp[i][j], dp[i][k]
                      + dp[k + 1][j] + cost);
            }
        }
    }
 
    // Return answer
    return dp[0][N - 1];
}
 
// Driver code
int main()
{
    int arr[] = { 3, 4, 2, 1, 7 };
    int N = sizeof(arr) / sizeof(arr[0]);
     
    // Function call
    cout << minCost(arr, N);
    return 0;
}


Java




// Java code for the above approach
import java.io.*;
import java.lang.*;
import java.util.*;
 
class GFG {
 
// Function to find the minimum cst
static int minCost(int[] arr, int N)
{
    // Creating the prefix sum array
    int pref[] = new int[N+1];
    int dp[][] = new int[N][N];
    pref[0] = 0;
   
    // Loop to calculate the prefix sum
    for (int i = 0; i < N; i++) {
        pref[i + 1] = pref[i] + arr[i];
    }
 
    // Iterating through all subarrays
    // of length 2 or greater
    for (int i = N - 2; i >= 0; i--) {
        for (int j = i + 1; j < N; j++) {
             
            // Cost function = sum of
            // all elements in the subarray
            int cost = pref[j + 1] - pref[i];
            dp[i][j] = Integer.MAX_VALUE;
            for (int k = i; k < j; k++) {
                 
                // dp transition
                dp[i][j]
                = Math.min(dp[i][j], dp[i][k]
                      + dp[k + 1][j] + cost);
            }
        }
    }
 
    // Return answer
    return dp[0][N - 1];
}
 
// Driver Code
public static void main (String[] args) {
    int[] arr = { 3, 4, 2, 1, 7 };
    int N = arr.length;
     
    // Function call
    System.out.println(minCost(arr, N));
}
}
 
// This code is contributed by sanjoy_62.


Python3




def minCost(arr, N):
    # Creating the prefix sum array
    pref = [0] * (N + 1)
    dp = [[0] * N for i in range(N)]
    pref[0] = 0
   
    # Loop to calculate the prefix sum
    for i in range(N):
        pref[i + 1] = pref[i] + arr[i]
 
    # Iterating through all subarrays
    # of length 2 or greater
    for i in range(N - 2, -1, -1):
        for j in range(i + 1, N):
             
            # Cost function = sum of
            # all elements in the subarray
            cost = pref[j + 1] - pref[i]
            dp[i][j] = float('inf')
            for k in range(i, j):
                 
                # dp transition
                dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + cost)
 
    # Return answer
    return dp[0][N - 1]
 
# Driver Code
if __name__ == '__main__':
    arr = [3, 4, 2, 1, 7]
    N = len(arr)
 
    # Function call
    print(minCost(arr, N))
 
    # This code is contributed by unstoppablepandu.


C#




// C# program for the above approach
using System;
using System.Collections.Generic;
 
class GFG
{
 
// Function to find the minimum cst
static int minCost(int[] arr, int N)
{
    // Creating the prefix sum array
    int[] pref = new int[N+1];
    int[,] dp = new int[N, N];
    pref[0] = 0;
   
    // Loop to calculate the prefix sum
    for (int i = 0; i < N; i++) {
        pref[i + 1] = pref[i] + arr[i];
    }
 
    // Iterating through all subarrays
    // of length 2 or greater
    for (int i = N - 2; i >= 0; i--) {
        for (int j = i + 1; j < N; j++) {
             
            // Cost function = sum of
            // all elements in the subarray
            int cost = pref[j + 1] - pref[i];
            dp[i, j] = Int32.MaxValue;
            for (int k = i; k < j; k++) {
                 
                // dp transition
                dp[i, j]
                = Math.Min(dp[i,j], dp[i,k]
                      + dp[k + 1, j] + cost);
            }
        }
    }
 
    // Return answer
    return dp[0, N - 1];
}
 
 
// Driver Code
public static void Main(String[] args)
{
        int[] arr = { 3, 4, 2, 1, 7 };
    int N = arr.Length;
     
    // Function call
    Console.WriteLine(minCost(arr, N));
}
}
 
// This code is contributed by avijitmondal1998.


Javascript




<script>
       // JavaScript program for the above approach
 
       // Function to find the minimum cst
       function minCost(arr, N)
       {
        
           // Creating the prefix sum array
           let pref = new Array(N + 1), dp = new Array(N);
 
           for (let i = 0; i < dp.length; i++) {
               dp[i] = new Array(N);
           }
           for (let i = 0; i < dp.length; i++) {
               for (let j = 0; j < dp[i].length; j++) {
                   dp[i][j] = 0;
               }
           }
           pref[0] = 0;
 
 
           // Loop to calculate the prefix sum
           for (let i = 0; i < N; i++) {
               pref[i + 1] = pref[i] + arr[i];
           }
 
           // Iterating through all subarrays
           // of length 2 or greater
           for (let i = N - 2; i >= 0; i--) {
               for (let j = i + 1; j < N; j++) {
 
                   // Cost function = sum of
                   // all elements in the subarray
                   let cost = pref[j + 1] - pref[i];
                   dp[i][j] = Number.MAX_VALUE;
                   for (let k = i; k < j; k++) {
 
                       // dp transition
                       dp[i][j]
                           = Math.min(dp[i][j], dp[i][k]
                               + dp[k + 1][j] + cost);
                   }
               }
           }
 
           // Return answer
           return dp[0][N - 1];
       }
 
       // Driver code
       let arr = [3, 4, 2, 1, 7];
       let N = arr.length;
 
       // Function call
       document.write(minCost(arr, N));
 
   // This code is contributed by Potta Lokesh
 
   </script>


Output

37

Time complexity: O(N3)
Auxiliary Space: O(N2

Optimal solution (using Knuth’s optimization):

The conditions for applying Knuth’s optimization hold for this question:

cost(i, j) is merely the sum of all elements in the subarray bounded by the indices i and j. Further, the transition function of dynamic programming matches the general case for applying this technique.

Follow the steps mentioned here to implement the idea of Knuth optimization:

  • Define the array opt[N][N] and the dp[][] array. 
  • Now, process subarrays in increasing order of length, with the initial state dp[i][i] = 0, and opt[i][i] = i (because the value should be i to minimize the value dp[i][i]).
  • Thus, when we reach the subarray arr[i, . . ., j], the value of opt[i][j-1] and opt[i+1][j] are already known. So, only check for the condition opt[i][j-1] ≤ k ≤ opt[i+1][j], to find opt[i][j] and dp[i][j]
  • Here also the cost function is calculated in the same way as in the previous approach using the prefix sum array.

Note: In the code given below different way of looping through all sub-arrays is used (instead of looping in increasing order of length), However, ensuring that opt[i+1][j] and opt[i][j-1] are calculated before dp[i][j].

Below is the implementation of the above approach. 

C++




// C++ code to implement the approach
 
#include <bits/stdc++.h>
using namespace std;
 
// Function to find the minimum possible cost
int minCost(int arr[], int N)
{
    // Creating the prefix sum array
    int pref[N + 1], dp[N][N], opt[N][N];
    pref[0] = 0;
    memset(dp, 0, sizeof(dp));
    memset(dp, 0, sizeof(dp));
   
    // Loop to calculate the prefix sum
    for (int i = 0; i < N; i++) {
        pref[i + 1] = pref[i] + arr[i];
        opt[i][i] = i;
    }
 
    // Iterating through all sub-arrays
    // of length 2 or greater
    for (int i = N - 2; i >= 0; i--) {
        for (int j = i + 1; j < N; j++) {
 
            // Cost function = sum of
            // all elements in the sub-array
            int cost = pref[j + 1] - pref[i];
            int mn = INT_MAX;
            for (int k = opt[i][j - 1];
                 k <= min(j - 1, opt[i + 1][j]);
                 k++) {
                if (mn >= dp[i][k]
                    + dp[k + 1][j] + cost) {
 
                    // Updating opt table
                    opt[i][j] = k;
 
                    // Updating minimum cost
                    mn = dp[i][k]
                         + dp[k + 1][j] + cost;
                }
            }
 
            // dp transition
            dp[i][j] = mn;
        }
    }
    return dp[0][N - 1];
}
 
// Driver code
int main()
{
    int arr[] = { 3, 4, 2, 1, 7 };
    int N = sizeof(arr) / sizeof(arr[0]);
 
    // Function call
    cout << minCost(arr, N);
    return 0;
}


Java




// Java code to implement the approach
import java.io.*;
 
class GFG
{
 
// Function to find the minimum possible cost
static int minCost(int arr[], int N)
{
    // Creating the prefix sum array
    int pref[] = new int[N + 1];
    int dp[][] = new int[N][N];
    int opt[][] = new int[N][N];
    pref[0] = 0;
     
    // Loop to calculate the prefix sum
    for (int i = 0; i < N; i++) {
        pref[i + 1] = pref[i] + arr[i];
        opt[i][i] = i;
    }
 
    // Iterating through all sub-arrays
    // of length 2 or greater
    for (int i = N - 2; i >= 0; i--) {
        for (int j = i + 1; j < N; j++) {
 
            // Cost function = sum of
            // all elements in the sub-array
            int cost = pref[j + 1] - pref[i];
            int mn = Integer.MAX_VALUE;
            for (int k = opt[i][j - 1];
                 k <= Math.min(j - 1, opt[i + 1][j]);
                 k++) {
                if (mn >= dp[i][k]
                    + dp[k + 1][j] + cost) {
 
                    // Updating opt table
                    opt[i][j] = k;
 
                    // Updating minimum cost
                    mn = dp[i][k]
                         + dp[k + 1][j] + cost;
                }
            }
 
            // dp transition
            dp[i][j] = mn;
        }
    }
    return dp[0][N - 1];
}
 
  // Driver code
  public static void main(String[] args)
  {
    int arr[] = { 3, 4, 2, 1, 7 };
    int N = arr.length;
 
    // Function call
    System.out.println(minCost(arr, N));
  }
}
 
// This code is contributed by sanjoy_62.


C#




// C# program to implement
// the above approach
using System;
 
class GFG
{
 
// Function to find the minimum possible cost
static int minCost(int[] arr, int N)
{
    // Creating the prefix sum array
    int[] pref = new int[N + 1];
    int[,] dp = new int[N, N];
    int[,] opt = new int[N, N];
    pref[0] = 0;
      
    // Loop to calculate the prefix sum
    for (int i = 0; i < N; i++) {
        pref[i + 1] = pref[i] + arr[i];
        opt[i, i] = i;
    }
  
    // Iterating through all sub-arrays
    // of length 2 or greater
    for (int i = N - 2; i >= 0; i--) {
        for (int j = i + 1; j < N; j++) {
  
            // Cost function = sum of
            // all elements in the sub-array
            int cost = pref[j + 1] - pref[i];
            int mn = Int32.MaxValue;
            for (int k = opt[i, j - 1];
                 k <= Math.Min(j - 1, opt[i + 1, j]);
                 k++) {
                if (mn >= dp[i, k]
                    + dp[k + 1, j] + cost) {
  
                    // Updating opt table
                    opt[i, j] = k;
  
                    // Updating minimum cost
                    mn = dp[i, k]
                         + dp[k + 1, j] + cost;
                }
            }
  
            // dp transition
            dp[i, j] = mn;
        }
    }
    return dp[0, N - 1];
}
 
// Driver Code
public static void Main()
{
    int[] arr = { 3, 4, 2, 1, 7 };
    int N = arr.Length;
  
    // Function call
    Console.Write(minCost(arr, N));
}
}
 
// This code is contributed by spevel62.


Python3




def minCost(arr, N):
    # Creating the prefix sum array
    pref = [0] * (N + 1)
    dp = [[0 for i in range(N)] for j in range(N)]
    opt = [[0 for i in range(N)] for j in range(N)]
 
    # Loop to calculate the prefix sum
    for i in range(N):
        pref[i + 1] = pref[i] + arr[i]
        opt[i][i] = i
 
    # Iterating through all sub-arrays
    # of length 2 or greater
    for i in range(N - 2, -1, -1):
        for j in range(i + 1, N):
            # Cost function = sum of
            # all elements in the sub-array
            cost = pref[j + 1] - pref[i]
            mn = float("inf")
            for k in range(opt[i][j - 1], min(j - 1, opt[i + 1][j]) + 1):
                if mn >= dp[i][k] + dp[k + 1][j] + cost:
                    # Updating opt table
                    opt[i][j] = k
                    # Updating minimum cost
                    mn = dp[i][k] + dp[k + 1][j] + cost
            # dp transition
            dp[i][j] = mn
    return dp[0][N - 1]
 
# Driver code
if __name__ == '__main__':
    arr = [3, 4, 2, 1, 7]
    N = len(arr)
 
    # Function call
    print(minCost(arr, N))


Javascript




function minCost(arr, N) {
 
// Creating the prefix sum array
let pref = new Array(N + 1).fill(0);
let dp = new Array(N);
let opt = new Array(N);
 
for (let i = 0; i < N; i++) {
    dp[i] = new Array(N).fill(0);
    opt[i] = new Array(N).fill(0);
    pref[i + 1] = pref[i] + arr[i];
    opt[i][i] = i;
}
 
// Iterating through all sub-arrays
// of length 2 or greater
for (let i = N - 2; i >= 0; i--) {
    for (let j = i + 1; j < N; j++) {
     
        // Cost function = sum of
        // all elements in the sub-array
        let cost = pref[j + 1] - pref[i];
        let mn = Number.MAX_VALUE;
        for (let k = opt[i][j - 1]; k <= Math.min(j - 1, opt[i + 1][j]); k++) {
            if (mn >= dp[i][k] + dp[k + 1][j] + cost)
            {
             
                // Updating opt table
                opt[i][j] = k;
                 
                // Updating minimum cost
                mn = dp[i][k] + dp[k + 1][j] + cost;
            }
        }
         
        // dp transition
        dp[i][j] = mn;
    }
}
return dp[0][N - 1];
}
 
// Driver code
let arr = [3, 4, 2, 1, 7];
let N = arr.length;
 
// Function call
console.log(minCost(arr, N));


Output

37

Time Complexity: O(N2)
Auxiliary Space: O(N2

Feeling lost in the world of random DSA topics, wasting time without progress? It’s time for a change! Join our DSA course, where we’ll guide you on an exciting journey to master DSA efficiently and on schedule.
Ready to dive in? Explore our Free Demo Content and join our DSA course, trusted by over 100,000 neveropen!

RELATED ARTICLES

Most Popular

Recent Comments