DP on DP

Introduction

This article introduces the idea of DP on DP and demonstrates how it applies to concrete problems through two examples.

Idea

The term “DP on DP” refers to abstracting the process of solving a subproblem (usually another DP) as an automaton (DFA) during dynamic programming, and then designing a new layer of DP on top of that automaton.

This technique is mainly used for sequence counting, probability, and expectation problems. A typical problem has the following structure:

  • Given an alphabet Σ\Sigma and a set of “valid sequences” of length nn, denoted by A⊆ΣnA\subseteq\Sigma^n, over it. Depending on the alphabet, the sequences may be binary strings, digit strings, state sequences, and so on.
  • For each particular sequence s∈Σns\in\Sigma^n, dynamic programming can determine whether it is valid (that is, s∈As\in A), calculate its weight, or find a related value.
  • Finally, we want to count all sequences in AA, their total weight, their expected value, and so on.

At this point, enumerating all sequences is infeasible. We therefore abstract the process of “determining whether a sequence is valid” (the inner DP) as a deterministic finite automaton (DFA). In general, for a fixed sequence s∈Σns\in\Sigma^n, the state function of the inner DP can be written as g(i,x;s)g(i,x;s): the value of some quantity after processing the prefix of ss of length ii, with the other state components equal to xx. The corresponding state transition equation of the inner DP is

g(i,⋅;s)=G(g(i−1,⋅;s),si).g(i,\cdot;s) = G(g(i-1,\cdot;s),s_i).

In other words, the function g(i,⋅;s)g(i,\cdot;s) is uniquely determined by the previous function g(i−1,⋅;s)g(i-1,\cdot;s) and the current character sis_i. If we regard the function g(i,⋅;s)g(i,\cdot;s) as a state of an automaton, the state transition equation of the inner DP gives a transition of that automaton. Thus, the automaton (Q,Σ,δ,q0,F)(Q,\Sigma,\delta,q_0,F) corresponding to the inner DP has the following structure:

  • The state set QQ consists of every possible s∈Σns\in\Sigma^n and i=0,1,⋯ ,ni=0,1,\cdots,n, together with their corresponding functions g(i,⋅;s)g(i,\cdot;s).
  • The transition function δ:Q×Σ→Q\delta:Q\times\Sigma\to Q is GG in the state transition equation of the inner DP.
  • The initial state q0q_0 is usually obvious: it is the initial state of the inner DP.
  • The accepting state set FF corresponds to all valid sequences s∈As\in A.

The function g(i,⋅;s)g(i,\cdot;s) itself may be quite complex. Therefore, when solving a concrete problem, it is usually necessary to compress the state space using state compression or DFA minimization. This is also the main reason why DP on DP can significantly reduce the time and space complexity compared with brute-force DP.

After abstracting the inner DP as a DFA, we can design a new DP on this DFA to solve the original problem; this is the outer DP. For ease of exposition, consider a simple counting problem. Define the state function of the outer DP as f(i,q)f(i,q), the number of prefixes of length ii that reach state q∈Qq\in Q in the DFA. Its state transition equation is

f(i,q)=∑c∈Σ∑q′∈Q:δ(q′,c)=qf(i−1,q′).f(i,q) = \sum_{c\in\Sigma}\sum_{q'\in Q:\delta(q',c)=q} f(i-1,q').

The initial state is of course f(0,q0)f(0,q_0), and the final answer can usually be obtained by a simple calculation from {f(n,q):q∈F}\{f(n,q):q\in F\}. The outer DP is actually a special case of DP on a DAG.

Examples

The following two examples explain the general approach of DP on DP in detail.

Example 1

Hero meet devil

Given a string over the alphabet ACGT, denoted by SS, with ∣S∣≤15|S|\le 15. For every 0≤i≤∣S∣0\leq i \leq |S|, determine how many strings of length mm over the alphabet ACGT, denoted by TT, have a longest common subsequence with SS of length ii.

Solution

The first DP that comes to mind is to let fi,jf_{i,j} be the number of length-ii strings TT whose longest common subsequence with SS has length jj. However, this state cannot be transitioned: the main problem is that we do not know which characters constitute this longest common subsequence.

Consider the naive process for finding the longest common subsequence. Let gi,jg_{i,j} be the length of the LCS of TT's first ii characters and SS's first jj characters. Then

gi,j=max⁡{gi−1,j,gi,j−1,gi−1,j−1+[Ti=Sj]}.g_{i,j} = \max\{g_{i-1,j},g_{i,j-1},g_{i-1,j-1}+[T_i=S_j]\}.

For a fixed ii, recording each value of gig_i is enough to maintain the current state of SS and the first characters of TT—specifically, the first ii characters. Since SS has length only 1515, this idea is feasible.

We therefore redefine the state fi,xf_{i,x} as the number of length-ii strings TT whose DP array with SS (namely gig_i) is in state xx. This DP seems to have many states, but gi,j−gi,j−1∈{0,1}g_{i,j}-g_{i,j-1}\in\{0,1\}, so we can maintain the difference array of gig_i; the number of states is 2∣S∣2^{|S|}.

Now consider how to transition. If we know the array gig_i and also know Ti+1T_{i+1}, we can use the naive LCS transition (the DP equation above) to obtain gi+1g_{i+1}. Thus, the naive LCS becomes the inner DP that helps transition ff.

We enumerate Ti+1T_{i+1}, take a state xx and calculate its transitioned state x′x', then update fi+1,x′f_{i+1,x'} with fi,xf_{i,x} to complete the outer-DP transition. Finally, let ansi\textit{ans}_i be the answer for LCS length ii. For each state SS, increase anspopcount⁡(S)\textit{ans}_{\operatorname{popcount}(S)} by fm,Sf_{m,S}.

Reference Code
#include <cstring>
#include <iostream>
#include <string>
using namespace std;

const int MOD = 1e9 + 7;
const int MAXN = 100;

int T, a[MAXN + 1], h[MAXN + 1];
int g[MAXN + 1], m, n;
int nxt[1 << 16]
       [4];  // State transition table: nx[mask][ch] is the new state after appending ch to state mask
int f[1001]
     [1 << 16];  // DP array: f[i][mask] is the number of ways of length i in state mask
int ans[MAXN +
        1];  // Final answer: ans[i] is the number of length-m strings with i matching positions

void add(int &x, int y) { x = (x + y) % MOD; }

int calc(int mask, int x) {  // Given the current mask state and character
                             // x (0~3), return the new state
  int res = 0;
  for (int i = 0; i < n; ++i) g[i + 1] = g[i] + ((mask >> i) & 1);

  for (int i = 1; i <= n; ++i) {
    if (a[i] == x)
      h[i] = g[i - 1] + 1;
    else
      h[i] = max(h[i - 1], g[i]);
  }

  for (int i = 1; i <= n; ++i)
    if (h[i] > h[i - 1]) res |= (1 << (i - 1));

  return res;
}

int popcount(int x) {  // Count the number of 1s in mask (that is, the number of matched positions)
  int res = 0;
  while (x) {
    res += x & 1;
    x >>= 1;
  }
  return res;
}

int main() {
  ios::sync_with_stdio(false);
  cin.tie(nullptr);

  cin >> T;
  while (T--) {
    string S;
    cin >> S >> m;
    n = S.length();

    memset(f, 0, sizeof(f));
    memset(ans, 0, sizeof(ans));
    memset(a, 0, sizeof(a));

    for (int i = 0; i < n; ++i) {
      if (S[i] == 'A') a[i + 1] = 0;
      if (S[i] == 'C') a[i + 1] = 1;
      if (S[i] == 'G') a[i + 1] = 2;
      if (S[i] == 'T') a[i + 1] = 3;
    }

    int lim = (1 << n);
    for (int i = 0; i < lim; ++i)
      for (int j = 0; j < 4; ++j) nxt[i][j] = calc(i, j);

    f[0][0] = 1;
    for (int i = 0; i < m; ++i)
      for (int j = 0; j < lim; ++j)
        if (f[i][j])
          for (int k = 0; k < 4; ++k) add(f[i + 1][nxt[j][k]], f[i][j]);

    for (int i = 0; i < lim; ++i) add(ans[popcount(i)], f[m][i]);

    for (int i = 0; i <= n; ++i) cout << ans[i] << '\n';
  }

  return 0;
}

Example 2

[ZJOI2019] Mahjong

Suppose there are nn tile ranks in Mahjong, with 44 tiles of each rank. A meld is either three consecutive tiles of ranks i,i+1,i+2i,i+1,i+2 (a sequence) or three tiles of the same rank i,i,ii,i,i (a triplet), and a pair is two tiles of the same rank i,ii,i. A Mahjong hand is winning if and only if, viewed as a multiset, it can be divided into four melds and one pair, or into seven distinct pairs. Given 1313 Mahjong tiles, find the expected number of additional draws needed for the hand to contain a winning subsequence.

Solution

First, for a hand, we only need to consider the count of each tile rank, not their order. Thus, every prefix of any tile sequence can be converted into a length-nn sequence whose entries are in 0∼40\sim 4. Initially, if the count of rank ii is aia_i, the ii-th number xix_i in the converted sequence is restricted to an integer in [ai,4][a_i,4]. Note that the converted sequence does not consider the order of draws, whereas the Mahjong sequence in the problem does.

Let XX be the minimum number of draws needed to win. Directly calculating the expectation E[X]\mathbf E[X] is difficult, so consider the following transformation. Let hih_i be the number of sequences that are not winning after ii tiles have been drawn. In the corresponding Mahjong sequence, these ii tiles must appear before the remaining (4n−13−i)(4n-13-i) tiles, but the order among those ii tiles and the remaining (4n−13−i)(4n-13-i) tiles is arbitrary. Therefore, the number of Mahjong sequences in which drawing only the first ii tiles does not produce a win is

hi⋅i!(4n−13−i)!.h_i\cdot i!(4n-13-i)!.

The total number of Mahjong sequences is (4n−13)!(4n-13)!, so the probability that drawing the first ii tiles does not produce a win is

P[X>i]=hi⋅i!(4n−13−i)!(4n−13)!.\mathbf P[X>i] = \dfrac{h_i\cdot i!(4n-13-i)!}{(4n-13)!}.

Using the tail-sum formula, we obtain the desired expectation:

E[X]=∑i=0∞P[X>i]=1+∑i=14n−13hi⋅i!(4n−13−i)!(4n−13)!.\mathbf E[X] = \sum_{i=0}^\infty\mathbf P[X>i] = 1 + \sum_{i=1}^{4n-13}\dfrac{h_i\cdot i!(4n-13-i)!}{(4n-13)!}.

The problem has now been reduced to computing hih_i. We solve it using DP on DP.

First consider the inner DP, which uses dynamic programming to determine whether a converted sequence represents a winning hand. The seven-pairs case is relatively easy, so we focus on the first winning form. Let g0/1,i,j,kg_{0/1,i,j,k} denote the maximum number of melds after processing the first ii ranks, with jj groups of (i−1,i)(i−1,i) and kk tiles of rank ii remaining, and with a pair present/absent (that is, 0/10/1). If we run the DP on a sequence and the final g1,ng_{1,n} contains a number greater than or equal to 44, then the sequence is winning.

The state transition of this DP is fairly complicated. We discuss it in two steps. First, consider the transition of g0/1,ig_{0/1,i}. This means that after adding xix_i tiles of rank ii to the current hand, without forming a new pair, we determine how the number of melds changes. If, after adding xix_i tiles of rank ii, we want to obtain ℓ\ell sequences, jj groups of (i−1,i)(i-1,i), and kk standalone tiles of rank ii, then we should transition from (g0/1,i−1)ℓ,j(g_{0/1,i-1})_{\ell,j} (this choice minimizes waste as much as possible), and use the remaining (xi−ℓ−j−k)(x_i-\ell-j-k) tiles to form as many triplets as possible. Enumerating all possibilities gives the following transition equation:

G~(g0/1,i−1,xi)j,k=max⁡{(g0/1,i−1)ℓ,j+ℓ+⌊xi−ℓ−j−k3⌋:ℓ+j+k≤xi}.\tilde G(g_{0/1,i-1}, x_i)_{j,k} = \max\left\{(g_{0/1,i-1})_{\ell,j} + \ell + \left\lfloor\dfrac{x_i-\ell-j-k}{3}\right\rfloor:\ell+j+k\le x_i\right\}.

Second, consider the case where a pair must be formed. When adding xix_i tiles of rank ii, there are three transitions:

  • Transition g0,i−1g_{0,i-1} with xix_i added tiles to g0,ig_{0,i}.
  • Transition g1,i−1g_{1,i-1} with xix_i added tiles to g1,ig_{1,i}.
  • If xi≥2x_i\ge 2, transition g0,i−1g_{0,i-1} with xi−2x_i-2 added tiles to g1,ig_{1,i}.

These are all the transitions from gi−1g_{i-1} after adding xix_i tiles, yielding gig_i.

Once the state transitions of the inner DP are solved, we can construct the winning-hand automaton. The automaton transitions are exactly the transitions of the inner DP above; we must also decide how to represent each automaton state. Each state corresponds to one possible value of gig_i. Its three dimensions are (0/1,j,k)(0/1,j,k). The dimensions indexed by jj and kk retain (i−1,i)(i-1,i) groups and ii tiles, respectively, for forming sequences later. Since three identical sequences can always be rearranged into three triplets, we only need to consider the need to form at most 22 identical sequences, so each tile type needs to retain at most 22: j,k∈{0,1,2}j,k\in\{0,1,2\}. Thus, gig_i can be represented by a 2×3×32\times 3\times 3 array. In addition, to maintain the seven-pairs winning form, each state needs a counter representing the maximum number of pairs that can currently be formed.

Each element of the array gig_i may take a value in {−∞}∪N\{-\infty\}\cup\mathbf N. However, since a meld count of at least 44 is already winning, every element can be capped at 44. Since adding any tile to a winning sequence still yields a winning sequence, DFA minimization lets us compress all winning states into a single state. Therefore, for non-winning states, each position only needs to consider {−∞}∪{0,1,2,3}\{-\infty\}\cup\{0,1,2,3\}. In the implementation, −∞-\infty is represented by −1-1.

Even so, there are still many possible states: 1+7×5181+7\times 5^{18}. It is not realistic to enumerate them all. In fact, most of these possibilities never occur in a winning-hand automaton. To avoid considering states that do not actually exist, use the idea of BFS: start from the initial state and expand states one step at a time, stopping when a winning state is reached. The resulting automaton has N=2092N = 2092 states.

Finally, consider how to run a DP on the winning-hand automaton (the outer DP). Let fi,j,kf_{i,j,k} be the number of sequences that have processed the first ii ranks, have drawn jj tiles in total, and have reached state kk of the winning-hand automaton. During the transition, enumerate the number of drawn tiles 0≤t≤4−ai0\leq t\leq 4-a_i, where aia_i is the number of the initial 1313 tiles that are of rank ii. Multiply the previous sequence count by the number of ways to choose from the 4−ai4−a_i remaining tiles a total of tt tiles, namely (4−ait)\dbinom{4-a_i}{t}, and accumulate. Formally,

fi+1,j+t,k′=∑t=04−ai(4−ait)fi,j,k.f_{i+1,j+t,k'} = \sum_{t=0}^{4-a_i}\dbinom{4-a_i}{t}f_{i,j,k}.

Here, k′=δ(k,ai+t)k'=\delta(k,a_i+t) is the state obtained by adding tiles to automaton state kk, specifically ai+ta_i+t tiles. After the outer DP ends, we can calculate the number of sequences that have drawn ii tiles and are still not winning:

hi=∑j=1Nfn,i,j.h_i=\sum_{j=1}^{N} f_{n,i,j}.

Substituting this into the expression above gives the desired expectation.

Reference Code
#include <algorithm>
#include <cstdio>
#include <iostream>
#include <map>
#include <unordered_map>
using namespace std;

const int N = 100, M = 400, mod = 998244353;

int n, m, a[N + 5], fact[M + 5], inv[M + 5];

inline int C(int x, int y) {
  return 1LL * fact[x] * inv[y] % mod * inv[x - y] % mod;
}

class HuAutomation {  // Winning-hand automaton
 private:
  class Mat {
   private:
    int f[3][3];

   public:
    Mat() {
      for (int i = 0; i <= 2; i++)
        for (int j = 0; j <= 2; j++) f[i][j] = -1;
    }

    int* operator[](const int& x) { return f[x]; }

    inline bool operator==(Mat x) const {
      for (int i = 0; i <= 2; i++)
        for (int j = 0; j <= 2; j++)
          if (f[i][j] != x[i][j]) return 0;
      return 1;
    }

    inline bool operator<(Mat x) const {
      for (int i = 0; i <= 2; i++)
        for (int j = 0; j <= 2; j++)
          if (f[i][j] != x[i][j]) return f[i][j] < x[i][j];
      return 0;
    }

    inline bool Check() {
      for (int i = 0; i <= 2; i++)
        for (int j = 0; j <= 2; j++)
          if (f[i][j] > 3) return 1;
      return 0;
    }

    inline void Upd(Mat x, int t) {
      // Update the current matrix with the values of existing matrix x, simulating the state after adding t current tiles
      for (int i = 0; i <= 2; i++)
        for (int j = 0; j <= 2; j++)
          if (x[i][j] != -1)
            for (int k = 0; k < 3 && i + j + k <= t; k++)
              f[j][k] = max(f[j][k], min(i + x[i][j] + (t - i - j - k) / 3, 4));
      // i,j,k,(t-i-j-k)
      // Enumerate the numbers of tiles used to form melds, retained as (i-1,i), retained as i, and used to form melds directly, respectively
      // Limit the update to at most 4 pairs
    }
  };

  struct node {
    int t, state[5];
    Mat F[2];

    node() {
      for (int i = 0; i <= 4; i++) state[i] = 0;
      t = 0;
      for (int i = 0; i <= 1; i++) F[i] = Mat();
    }

    inline bool Check() { return t == -1 || t >= 7 || F[1].Check(); }

    node Hu() {
      node x;
      x.t = -1;
      return x;
    }

    bool operator<(const node& x) const {
      if (t == x.t) {
        if (F[0] == x.F[0]) return F[1] < x.F[1];
        return F[0] < x.F[0];
      }
      return t < x.t;
    }

    node operator+(int x) {      // Add x new tiles
      if (Check()) return Hu();  // Return immediately if already winning
      node res;
      res.F[0].Upd(F[0], x), res.F[1].Upd(F[1], x);  // Update
      res.t = t;
      if (x > 1) res.F[1].Upd(F[0], x - 2), ++res.t;
      if (res.Check()) res = Hu();  // Check whether the hand is winning
      return res;
    }
  } A[2100];

  map<node, int> mp;

  inline int Get(node x) {
    if (mp.count(x)) return mp[x];
    mp[x] = ++tot;
    A[tot] = x;
    return tot;
  }

  inline void Expand(int x) {
    // Build the state transition graph:
    // For the current state A[x], try adding 0~4 tiles of the same rank to obtain 5 new states
    for (int i = 0; i <= 4; i++) A[x].state[i] = Get(A[x] + i);
  }

  inline node Hu() {
    node x;
    x.t = -1;
    return x;
  }

  inline node Emp() {
    node x;
    x.F[0][0][0] = 0;
    return x;
  }

 public:
  int tot, f[N + 5][M + 5][2100];

  inline void Build() {
    A[1] = Emp(), A[2] = Hu();  // State 1 is the initial valid state, and state 2 is the winning state
    mp[A[1]] = 1, mp[A[2]] = 2;
    tot = 2;
    Expand(1);
    for (int i = 3; i <= tot; i++)
      Expand(i);  // Enumerate all reachable states and build the state graph (since 2
                  // is the winning state, it need not be expanded)
  }

  void DP() {
    f[0][0][1] = 1;
    for (int i = 1; i <= n; i++)
      for (int j = m; j >= 0; j--)
        for (int k = 1; k <= tot; k++)
          if (f[i - 1][j][k])
            for (int t = 0; t <= 4 - a[i]; t++)
              // Enumerate how many tiles can be added (at most 4 in total, with a[i] already present) and perform the combinatorial transition
              (f[i][j + t][A[k].state[a[i] + t]] +=
               1LL * f[i - 1][j][k] * C(4 - a[i], t) % mod) %= mod;
  }
} Hfu;

inline long long qpow(long long x, int y) {
  long long res = 1;
  while (y) {
    if (y & 1) res = res * x % mod;
    x = x * x % mod;
    y >>= 1;
  }
  return res;
}

void init(int n) {
  fact[0] = 1;
  for (int i = 1; i <= n; i++) fact[i] = 1LL * fact[i - 1] * i % mod;
  inv[n] = qpow(fact[n], mod - 2);
  for (int i = n - 1; i >= 0; i--) inv[i] = 1LL * inv[i + 1] * (i + 1) % mod;
}

int main() {
  ios::sync_with_stdio(false);
  cin.tie(nullptr);

  int ans = 0;
  Hfu.Build();  // Build the state graph
  cin >> n;
  for (int i = 1; i <= 13; i++) {
    int x, y;
    cin >> x;
    cin >> y;
    ++a[x];
  }
  m = (n << 2) - 13;
  init(m);
  Hfu.DP();

  for (int i = 1; i <= m; i++) {
    (ans += 1LL * Hfu.f[n][i][1] * fact[i] % mod * fact[m - i] % mod) %= mod;
    for (int j = 3; j <= Hfu.tot; j++)  // Do not count node 2 because it is the winning node
      (ans += 1LL * Hfu.f[n][i][j] * fact[i] % mod * fact[m - i] % mod) %= mod;
    // f[i][j][k]: the number of ways after processing the first i ranks, drawing j tiles in total, and reaching k
    // -th node; multiply each way by i! (permutations of these i tiles) × (m - i)!
    // (permutations of the remaining tiles), then sum modulo mod
  }

  cout << (1LL * ans * inv[m] + 1) % mod;
  // Multiplying the final answer by inv[m] divides by m!, removing the total number of permutations; the final +1 comes from the DP definition that after drawing i
  // tiles the hand is still not winning
  return 0;
}

Exercises

This content comes from OI Wiki: DP on DP. See the original for authors and revision history. AIOJ has adapted the formatting, in-site links, and resource paths.

Content is shared under CC BY-SA 4.0 and SATA license; additional terms are governed by the original notice. Thanks to the OI Wiki authors and contributors; per the SATA terms, please visit the OI Wiki repository and star it to show your support.