A Systematic Approach to Algorithms

Vijay K. Garg · The University of Texas at Austin

Chapter 10. Dynamic Programming

Classical bottom-up table fills for weighted interval scheduling, longest increasing subsequence, optimal BSTs, and 0/1 knapsack.

This page: Classical forms. View LLP forms »

This page collects the classical / sequential implementations of the algorithms developed in this chapter. The lattice-linear (LLP) reformulations and chapter setup live on the LLP companion page.

WeightedIntervalScheduling

Classical sequential DP: pre-compute $p[\cdot]$, then sweep left to right, taking the better of "skip $j$" ($Opt(j-1)$) and "take $j$" ($w_j + Opt(p(j))$). $O(n)$ given $p$.

Time complexity: $O(n)$ once $p$ is in hand, $O(n \log n)$ to compute $p$.

int[] schedule(int[] s, int[] f, int[] w, int[] p) {
  int n = s.length;
  int[] opt = new int[n];
  int[] G   = new int[n];
  opt[0] = 0;
  int cur = 1;
  while (cur < n) {
    opt[cur] = opt[cur - 1];
    if (w[cur] + opt[p[cur]] >= opt[cur - 1]) {
      opt[cur] = w[cur] + opt[p[cur]];
      G[cur]   = 1;
    } else {
      G[cur] = 0;
    };
    cur = cur + 1;
  };
  return G;
}

LIS

The classical $O(n^2)$ DP: $dp[i] = 1$ initially, then for each $i$, scan all $j < i$ and update $dp[i] := \max(dp[i],\ dp[j] + 1)$ whenever $A[j] < A[i]$.

Time complexity: $O(n^2)$, where $n$ is the size of the array.

int[] solve(int[] A) {
  int n = A.length;
  int[] dp = new int[n];
  int i = 0;
  while (i < n) { dp[i] = 1; i = i + 1; };
  i = 1;
  while (i < n) {
    int j = 0;
    while (j < i) {
      if (A[j] < A[i]) {
        if (dp[j] + 1 > dp[i]) { dp[i] = dp[j] + 1; }
      };
      j = j + 1;
    };
    i = i + 1;
  };
  return dp;
}

OptimalBinarySearchTree

The classical $O(n^3)$ DP: fill $dp[i][j]$ by increasing range length, trying every key $r \in [i, j]$ as the root and combining left/right subtree costs.

Time complexity: $O(n^3)$, where $n$ is the number of keys.

double[][] solve(double[] prob) {
  int n = prob.length;
  double[][] dp = new double[n][n];
  double[][] s  = new double[n][n];
  int i = 0;
  while (i < n) {
    dp[i][i] = prob[i];
    s[i][i]  = prob[i];
    i = i + 1;
  };
  int len = 1;
  while (len < n) {
    int lo = 0;
    while (lo < n - len) {
      int hi = lo + len;
      s[lo][hi] = s[lo][hi - 1] + prob[hi];
      double best = infinity;
      int r = lo;
      while (r <= hi) {
        double left  = 0.0;
        double right = 0.0;
        if (r > lo) { left  = dp[lo][r - 1]; };
        if (r < hi) { right = dp[r + 1][hi]; };
        double cost = s[lo][hi] + left + right;
        if (cost < best) { best = cost; };
        r = r + 1;
      };
      dp[lo][hi] = best;
      lo = lo + 1;
    };
    len = len + 1;
  };
  return dp;
}

Knapsack01

The classical $O(nW)$ table fill. $G[i][c]$ is the optimum value using items $1..i$ within capacity $c$; row $i$ depends only on row $i-1$.

Time complexity: $O(nW)$, where $n$ is the number of items and $W$ is the capacity.

int[][] solve(int[] w, int[] v, int W) {
  int n = w.length - 1;
  int[][] G = new int[n + 1][W + 1];
  int c = 0;
  while (c <= W) {
    G[0][c] = 0;
    c = c + 1;
  };
  int i = 0;
  while (i <= n) {
    G[i][0] = 0;
    i = i + 1;
  };
  i = 1;
  while (i <= n) {
    c = 1;
    while (c <= W) {
      if (w[i] > c) {
        G[i][c] = G[i - 1][c];
      } else {
        int skip = G[i - 1][c];
        int take = G[i - 1][c - w[i]] + v[i];
        if (take > skip) {
          G[i][c] = take;
        } else {
          G[i][c] = skip;
        }
      };
      c = c + 1;
    };
    i = i + 1;
  };
  return G;
}