This optimization for dynamic programming solutions uses the concept of divide and conquer. It is only applicable for the following recurrence:
This optimization reduces the time complexity from to
There are people at an amusement park who are in a queue for a ride. Each pair of people has a measured level of unfamiliarity. The people will be divided into non-empty contiguous groups. Each division has a total unfamiliarity value which is the sum of the levels of unfamiliarity between any pair of people for each group.
Determine the minimal possible total unfamiliarity value.
8 3
0 1 1 1 1 1 1 1
1 0 1 1 1 1 1 1
1 1 0 1 1 1 1 1
1 1 1 0 1 1 1 1
1 1 1 1 0 1 1 1
1 1 1 1 1 0 1 1
1 1 1 1 1 1 0 1
1 1 1 1 1 1 1 0
7
One optimal division is which sum to
First let us notice the solution:
for (int i = 0; i <= n; i++)
for (int j = 0; j <= k; j++)
dp[i][j] = 1 << 30;
dp[0][0] = 0;
for (int i = 1; i <= k; i++)
for (int j = 1; j <= n; j++)
for (int k = 1; k <= j; k++)
dp[j][i] = Math.min(dp[j][i], dp[k-1][i-1] + cost(k, j));
For each iteration of , we are looping from to , but if we use the observation that , we can reduce that left and right bounds for each iteration.
Let us define function that computes knowing that
We first call the function with the following parameters: . This step will take time. For each call, if we compute the value of , we can essentially divide the function into two:
At each depth of recursion, there are only computations to be done. The total depth of recursion will be . Thus, for each value of , the running time is . We then call the function for all values of , so the final running time is
import java.util.*;
import java.io.*;
public class main {
static BufferedReader br;
static PrintWriter out;
static StringTokenizer st;
static int n, k;
static int[][] a;
static int[][] dp;
public static void main(String[] args) throws IOException {
br = new BufferedReader(new InputStreamReader(System.in));
out = new PrintWriter(new OutputStreamWriter(System.out));
n = readInt();
k = readInt();
a = new int[n + 1][n + 1];
dp = new int[n + 1][k + 1];
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
a[i][j] = readInt() + a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1];
for (int i = 0; i <= n; i++)
for (int j = 0; j <= k; j++)
dp[i][j] = 1 << 30;
dp[0][0] = 0;
for (int i = 1; i <= k; i++)
compute(i, 1, n, 1, n);
out.println(dp[n][k] / 2);
out.close();
}
static void compute(int g, int i, int j, int l, int r) {
if (i > j)
return;
int mid = (i + j) / 2;
int bestIndex = l;
for (int k = l; k <= Math.min(r, mid); k++) {
int val = dp[k - 1][g - 1] +
(a[mid][mid] - a[mid][k - 1] - a[k - 1][mid] + a[k - 1][k - 1]);
if (val < dp[mid][g]) {
dp[mid][g] = val;
bestIndex = k;
}
}
compute(g, i, mid - 1, l, bestIndex);
compute(g, mid + 1, j, bestIndex, r);
}
static String next() throws IOException {
while (st == null || !st.hasMoreTokens())
st = new StringTokenizer(br.readLine().trim());
return st.nextToken();
}
static int readInt() throws IOException {
return Integer.parseInt(next());
}
}
This post is a part of a series of three posts on dynamic programming optimizations: