jeffreyxiao
Hi! I'm a student at the University of Waterloo studying software engineering. I have an interest in large scale distributed algorithms and infrastructure for data analytics. I also enjoy working with low level systems.

Divide and Conquer Optimization

Monday, December 14, 2015

Dynamic ProgrammingProgramming

Introduction

This optimization for dynamic programming solutions uses the concept of divide and conquer. It is only applicable for the following recurrence:

dp[i][j]=mink<j{dp[i1][k]+C[k][j]}\text{dp}[i][j] = \min_{k < j}\{dp[i-1][k] + \text{C}[k][j]\} min[i][j]min[i][j+1]\text{min}[i][j] \leq \text{min}[i][j+1] min[i][j] is the smallest k that gives the optimal answer\text{min}[i][j] \text{ is the smallest k that gives the optimal answer}

This optimization reduces the time complexity from O(KN2)O(KN^2) to O(KNlog N)O(KN log \ N)

Example Problem: Codeforces Round 190: Div. 1E

There are NN 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 KK 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.

Sample Input

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

Sample Output

7

Sample Explanation

One optimal division is {1,2,3}{4,5,6}{7,8}\{1, 2, 3\} | \{4, 5, 6\} | \{7, 8\} which sum to 77

Analysis

First let us notice the O(KN2)O(KN^2) 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 jj, we are looping from 11 to jj, but if we use the observation that minK[j][i]minK[j+1][i]\text{minK}[j][i] \leq \text{minK}[j+1][i], we can reduce that left and right bounds for each iteration.

Let us define function compute(g,i,j,l,r)\text{compute}(g, i, j, l, r) that computes dp[i...j][g]\text{dp}[i...j][g] knowing that lkrl \leq k \leq r

We first call the function with the following parameters: compute(g,1,n,1,n)\text{compute}(g, 1, n, 1, n). This step will take O(N)O(N) time. For each call, if we compute the value of dp[g][i+j2]\text{dp}[g][{i+j\over 2}], we can essentially divide the function into two:

compute(g,i,i+j21,l,k)\text{compute}\left(g, i, {i+j\over 2} - 1, l, k\right) compute(g,i+j2+1,j,k,r)\text{compute}\left(g, {i+j\over 2} + 1, j, k, r\right)

At each depth of recursion, there are only 2N2N computations to be done. The total depth of recursion will be log Nlog\ N. Thus, for each value of gg, the running time is O(Nlog N)O(Nlog\ N). We then call the function for all values of gg, so the final running time is O(KNlog N)O(KNlog\ N)

Code

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:

  1. Convex Hull Trick
  2. Knuth's Optimization
  3. Divide and Conquer Optimization
Introduction
Example Problem: Codeforces Round 190: Div. 1E
Sample Input
Sample Output
Sample Explanation
Analysis
Code
Previous Post
Knuth's Optimization
Next Post
UofT Hacks