跟 Nachia DP 博客学DP-12 子序列 DP

原文DP 的俗称 | Mathenachia --- DP の俗称 | Mathenachia

对于某个序列的(不一定连续的)所有子序列,在去除重复后进行处理的典型 DP 被称为子序列 DP

Note

「例题1」:

考虑序列 $A = (a_1,a_2,\ldots,a_N)$ 有多少中子序列

朴素的想就是一个位置上选或者不选,那么是 $2^N$ 种组合,复杂度是 $O(2^N)$ 的,那么考虑如何不去重复计算即可

法一

通过限制“前驱”的位置来避免重复计数

如果我们要判断字符串 $S$ 是不是 $T$ 的子序列,我们总会贪心的在 $T$ 种寻找 $S$ 的各个字符最靠前的位置匹配。

这样一来,当选择第 $i$ 个元素的时候,考虑其可能的前一个元素,也就是第 $i$ 个元素之前,且值与 $a_i$ 相同的元素:

所以这里就有我们的状态方程了:$dp[j][i]=$ 最后取出的元素在原序列的区间 $[j,i]$ 内的子序列问题的解

而为什么是上面那样的逻辑呢?

法二

容斥定理

$dp[i]=$ 由前缀 $(a_k)_{k=1}^i$ 能够构成的本质不同的子序列数量

转移如下:当考虑第 $i$ 个字符时:

那么转移方程就是: $dp[i] = dp[i - 1] \times 2 - dp[k - 1]$ ($k$ 是 $a_i$ 上一次出现的位置,没出现过就不减这部分)

法三

自动机 DP

$dp[i][c]=$ 在 $(a_k)_{k=1}^i$ 的子序列中,以 $c$ 结尾的种类数

$sum[i] =$ $(a_k)_{k=1}^i$ 的本质不同子序列数(含空序列)

转移就是:

$\begin{align}
\text{dp} \lbrack i + 1 \rbrack \lbrack c \rbrack &= \begin{cases} \text{sum} \lbrack i \rbrack & \quad ( S _ i = c ) \cr \text{dp} \lbrack i \rbrack \lbrack c \rbrack & \quad ( S _ i \neq c ) \end{cases} \cr
\text{sum} \lbrack i + 1 \rbrack &= 1 + \sum _ {c} \text{dp} \lbrack i + 1 \rbrack \lbrack c \rbrack = 2 ~ \text{sum} \lbrack i \rbrack - \text{dp} \lbrack i \rbrack \lbrack S _ i \rbrack
\end{align}$

int main() {
  int N;
  cin >> N;
  map<int, mint> dp;
  mint sum = 1;
  for (int i = 0; i < N; i++) {
    int A;
    cin >> A;
    mint temp = dp[A];
    dp[A] = sum;
    sum += sum - temp;
  }
  cout << (sum - 1).val();
}

类似字典树的想法,其实就是每加入一个新节点的时候,就去与之前没出现过的位置加入该节点,然后答案是节点数 $- 1$(去除根节点)

例题

模板题

AtCoder ABC214 F - Substrings

MID ++

AtCoder TDPC G - 字典序

子序列自动机(?)

auto Mainsol = [](){
  STR(S); LL(K);
  ll N = sz(S);
  v<array<int, 26>> nxt(N + 1, {0});
  v<ll> f(N + 1, 0); // 以 S[i] 为首字母开始的本质不同子序列数

  rep(c, 26) nxt[N][c] = N;
  rrep(i, N) {
  	rep(c, 26) nxt[i][c] = nxt[i + 1][c];
  	nxt[i][S[i] - 'a'] = i; // 从 i 之后,字符c 第一次出现的位置
  }

  rrep(i, N) {
  	f[i] = 1;
  	rep(c, 26) {
  		int next_idx = nxt[i + 1][c];
  		if(next_idx < N) 
  			f[i] = min(LINF, f[i] + f[next_idx]);
  	}
  }

  ll Sum = 0;
  rep(c, 26) {
  	if(nxt[0][c] < N) Sum = min(LINF, Sum + f[nxt[0][c]]);
  }

  if(K > Sum) return out("Eel");

  string ans = "";
  int now = 0;
  while(K > 0) {
  	rep(c, 26) {
  		int next_idx = nxt[now][c];
  		if(next_idx < N) {
  			if(K <= f[next_idx]) {
  				ans += (char)('a' + c);
  				K -= 1;
  				now = next_idx + 1;
  				break;
  			} else {
  				K -= f[next_idx];
  			}
  		}
  	}
  }
  out(ans);

};