Knuth’s Optimization in Dynamic Programming
Last Updated :
17 Feb, 2023
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) –
- It is a Monotone on the lattice of intervals (MLI) [cost(b, c) ≤ cost(a, d)]
- 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++
#include <bits/stdc++.h>
using namespace std;
int minCost( int arr[], int N)
{
int pref[N+1], dp[N][N];
pref[0] = 0;
memset (dp, 0, sizeof (dp));
for ( int i = 0; i < N; i++) {
pref[i + 1] = pref[i] + arr[i];
}
for ( int i = N - 2; i >= 0; i--) {
for ( int j = i + 1; j < N; j++) {
int cost = pref[j + 1] - pref[i];
dp[i][j] = INT_MAX;
for ( int k = i; k < j; k++) {
dp[i][j]
= min(dp[i][j], dp[i][k]
+ dp[k + 1][j] + cost);
}
}
}
return dp[0][N - 1];
}
int main()
{
int arr[] = { 3, 4, 2, 1, 7 };
int N = sizeof (arr) / sizeof (arr[0]);
cout << minCost(arr, N);
return 0;
}
|
Java
import java.io.*;
import java.lang.*;
import java.util.*;
class GFG {
static int minCost( int [] arr, int N)
{
int pref[] = new int [N+ 1 ];
int dp[][] = new int [N][N];
pref[ 0 ] = 0 ;
for ( int i = 0 ; i < N; i++) {
pref[i + 1 ] = pref[i] + arr[i];
}
for ( int i = N - 2 ; i >= 0 ; i--) {
for ( int j = i + 1 ; j < N; j++) {
int cost = pref[j + 1 ] - pref[i];
dp[i][j] = Integer.MAX_VALUE;
for ( int k = i; k < j; k++) {
dp[i][j]
= Math.min(dp[i][j], dp[i][k]
+ dp[k + 1 ][j] + cost);
}
}
}
return dp[ 0 ][N - 1 ];
}
public static void main (String[] args) {
int [] arr = { 3 , 4 , 2 , 1 , 7 };
int N = arr.length;
System.out.println(minCost(arr, N));
}
}
|
Python3
def minCost(arr, N):
pref = [ 0 ] * (N + 1 )
dp = [[ 0 ] * N for i in range (N)]
pref[ 0 ] = 0
for i in range (N):
pref[i + 1 ] = pref[i] + arr[i]
for i in range (N - 2 , - 1 , - 1 ):
for j in range (i + 1 , N):
cost = pref[j + 1 ] - pref[i]
dp[i][j] = float ( 'inf' )
for k in range (i, j):
dp[i][j] = min (dp[i][j], dp[i][k] + dp[k + 1 ][j] + cost)
return dp[ 0 ][N - 1 ]
if __name__ = = '__main__' :
arr = [ 3 , 4 , 2 , 1 , 7 ]
N = len (arr)
print (minCost(arr, N))
|
C#
using System;
using System.Collections.Generic;
class GFG
{
static int minCost( int [] arr, int N)
{
int [] pref = new int [N+1];
int [,] dp = new int [N, N];
pref[0] = 0;
for ( int i = 0; i < N; i++) {
pref[i + 1] = pref[i] + arr[i];
}
for ( int i = N - 2; i >= 0; i--) {
for ( int j = i + 1; j < N; j++) {
int cost = pref[j + 1] - pref[i];
dp[i, j] = Int32.MaxValue;
for ( int k = i; k < j; k++) {
dp[i, j]
= Math.Min(dp[i,j], dp[i,k]
+ dp[k + 1, j] + cost);
}
}
}
return dp[0, N - 1];
}
public static void Main(String[] args)
{
int [] arr = { 3, 4, 2, 1, 7 };
int N = arr.Length;
Console.WriteLine(minCost(arr, N));
}
}
|
Javascript
<script>
function minCost(arr, N)
{
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;
for (let i = 0; i < N; i++) {
pref[i + 1] = pref[i] + arr[i];
}
for (let i = N - 2; i >= 0; i--) {
for (let j = i + 1; j < N; j++) {
let cost = pref[j + 1] - pref[i];
dp[i][j] = Number.MAX_VALUE;
for (let k = i; k < j; k++) {
dp[i][j]
= Math.min(dp[i][j], dp[i][k]
+ dp[k + 1][j] + cost);
}
}
}
return dp[0][N - 1];
}
let arr = [3, 4, 2, 1, 7];
let N = arr.length;
document.write(minCost(arr, N));
</script>
|
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++
#include <bits/stdc++.h>
using namespace std;
int minCost( int arr[], int N)
{
int pref[N + 1], dp[N][N], opt[N][N];
pref[0] = 0;
memset (dp, 0, sizeof (dp));
memset (dp, 0, sizeof (dp));
for ( int i = 0; i < N; i++) {
pref[i + 1] = pref[i] + arr[i];
opt[i][i] = i;
}
for ( int i = N - 2; i >= 0; i--) {
for ( int j = i + 1; j < N; j++) {
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) {
opt[i][j] = k;
mn = dp[i][k]
+ dp[k + 1][j] + cost;
}
}
dp[i][j] = mn;
}
}
return dp[0][N - 1];
}
int main()
{
int arr[] = { 3, 4, 2, 1, 7 };
int N = sizeof (arr) / sizeof (arr[0]);
cout << minCost(arr, N);
return 0;
}
|
Java
import java.io.*;
class GFG
{
static int minCost( int arr[], int N)
{
int pref[] = new int [N + 1 ];
int dp[][] = new int [N][N];
int opt[][] = new int [N][N];
pref[ 0 ] = 0 ;
for ( int i = 0 ; i < N; i++) {
pref[i + 1 ] = pref[i] + arr[i];
opt[i][i] = i;
}
for ( int i = N - 2 ; i >= 0 ; i--) {
for ( int j = i + 1 ; j < N; j++) {
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) {
opt[i][j] = k;
mn = dp[i][k]
+ dp[k + 1 ][j] + cost;
}
}
dp[i][j] = mn;
}
}
return dp[ 0 ][N - 1 ];
}
public static void main(String[] args)
{
int arr[] = { 3 , 4 , 2 , 1 , 7 };
int N = arr.length;
System.out.println(minCost(arr, N));
}
}
|
C#
using System;
class GFG
{
static int minCost( int [] arr, int N)
{
int [] pref = new int [N + 1];
int [,] dp = new int [N, N];
int [,] opt = new int [N, N];
pref[0] = 0;
for ( int i = 0; i < N; i++) {
pref[i + 1] = pref[i] + arr[i];
opt[i, i] = i;
}
for ( int i = N - 2; i >= 0; i--) {
for ( int j = i + 1; j < N; j++) {
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) {
opt[i, j] = k;
mn = dp[i, k]
+ dp[k + 1, j] + cost;
}
}
dp[i, j] = mn;
}
}
return dp[0, N - 1];
}
public static void Main()
{
int [] arr = { 3, 4, 2, 1, 7 };
int N = arr.Length;
Console.Write(minCost(arr, N));
}
}
|
Python3
def minCost(arr, N):
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)]
for i in range (N):
pref[i + 1 ] = pref[i] + arr[i]
opt[i][i] = i
for i in range (N - 2 , - 1 , - 1 ):
for j in range (i + 1 , N):
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:
opt[i][j] = k
mn = dp[i][k] + dp[k + 1 ][j] + cost
dp[i][j] = mn
return dp[ 0 ][N - 1 ]
if __name__ = = '__main__' :
arr = [ 3 , 4 , 2 , 1 , 7 ]
N = len (arr)
print (minCost(arr, N))
|
Javascript
function minCost(arr, N) {
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;
}
for (let i = N - 2; i >= 0; i--) {
for (let j = i + 1; j < N; j++) {
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)
{
opt[i][j] = k;
mn = dp[i][k] + dp[k + 1][j] + cost;
}
}
dp[i][j] = mn;
}
}
return dp[0][N - 1];
}
let arr = [3, 4, 2, 1, 7];
let N = arr.length;
console.log(minCost(arr, N));
|
Time Complexity: O(N2)
Auxiliary Space: O(N2)
Similar Reads
Divide and Conquer Optimization in Dynamic Programming
Dynamic programming (DP) is arguably the most important tool in a competitive programmer's repertoire. There are several optimizations in DP that reduce the time complexity of standard DP procedures by a linear factor or more, such as Knuth's optimization, Divide and Conquer optimization, the Convex
15+ min read
Dynamic Programming meaning in DSA
Dynamic Programming is defined as an algorithmic technique that is used to solve problems by breaking them into smaller subproblems and avoiding repeated calculation of overlapping subproblems and using the property that the solution of the problem depends on the optimal solution of the subproblems
2 min read
Introduction to Dynamic Programming on Trees
Dynamic Programming is a technique to solve problems by breaking them down into overlapping sub-problems which follows the optimal substructure. There are various problems using DP like subset sum, knapsack, coin change etc. DP can also be applied to trees to solve some specific problems.Given a bin
13 min read
Dynamic Programming (DP) on Grids
Grid problems involve a 2D grid of cells, often representing a map or graph. We can apply Dynamic Programming on Grids when the solution for a cell is dependent on solutions of previously traversed cells like to find a path or count number of paths or solve an optimization problem across the grid, w
11 min read
Convert N to M with given operations using dynamic programming
Given two integers N and M and the task is to convert N to M with the following operations: Multiply N by 2 i.e. N = N * 2.Subtract 1 from N i.e. N = N - 1. Examples: Input: N = 4, M = 6 Output: 2 Perform operation 2: N = N - 1 = 4 - 1 = 3 Perform operation 1: N = N * 2 = 3 * 2 = 6 Input: N = 10, M
7 min read
Dynamic Programming or DP
Dynamic Programming is an algorithmic technique with the following properties. It is mainly an optimization over plain recursion. Wherever we see a recursive solution that has repeated calls for the same inputs, we can optimize it using Dynamic Programming. The idea is to simply store the results of
3 min read
Optimal Substructure Property in Dynamic Programming | DP-2
The following are the two main properties of a problem that suggest that the given problem can be solved using Dynamic programming: 1) Overlapping Subproblems 2) Optimal Substructure We have already discussed the Overlapping Subproblem property. Let us discuss the Optimal Substructure property here.
4 min read
Double Knapsack | Dynamic Programming
Given an array arr[] containing the weight of 'n' distinct items, and two knapsacks that can withstand capactiy1 and capacity2 weights, the task is to find the sum of the largest subset of the array 'arr', that can be fit in the two knapsacks. It's not allowed to break any items in two, i.e. an item
15+ min read
How Does Dynamic Programming Work?
Dynamic programming, popularly known as DP, is a method of solving problems by breaking them down into simple, overlapping subproblems and then solving each of the subproblems only once, storing the solutions to the subproblems that are solved to avoid redundant computations. This technique is usefu
15 min read
Ackermann's function using Dynamic programming
Given two non-zero integers M and N, the problem is to compute the result of the Ackermann function based on some particular equations. Ackermann function is defined as: Examples: Input: M = 2, N = 2Output: 7 Input: M = 2, N = 7Output: 6141004759 The approach for Ackermann function described in this
13 min read