DP套DP
DP套DP
DP套DP,就是将内层DP的结果作为外层DP的状态进行DP。
主要思想是一位一位确定子DP的输入,不妨考虑已经枚举了前 \(i\) 位,由于我们只对DP方程的最终结果感兴趣,故并不需要记录这前 \(i\) 位都是什么,只需要记录对这前 \(i\) 位进行转移以后,DP方程关于每个状态的当前DP值即可,也就是说外层DP的状态是所有子DP的状态的值。
可以看作一个DP自动机,每个点有出边,有起始状态和终止状态集合,DP若干步之后在 DFA 上的某节点的方案数。
这类题目的一个显著特点是经典的DP和极小的值域。
实现时体现在内层DP的转移结果为外层DP的状态来转移,需要内层DP的转移结果很小,一般可以通过差分等操作减小结果。
给一个只由
A, G, C, T
组成的长度为 \(n\) 的字符串 \(S\) 。对于每个 \(i \in [0, n]\) ,问有多少个只由A, G, C, T
组成的长度为 \(m\) 的字符串 \(T\) ,使得 \(LCS(S, T) = i\) 。\(n \leq 15, m \leq 10^3\)
对于求 \(LCS\) ,有一个经典 DP ,设 \(f_{i, j}\) 表示考虑到了 \(S_i, T_j\) 的答案,则:
若 \(S_i = T_j\) ,则又有:
注意到 \(f_{i, j} - f_{i, j - 1} \in [0, 1]\) ,那么可以考虑记录差分数组,状态就压缩到了 \(2^{|S|}\) 种。
依次枚举 \(T\) 的每一位,根据外层DP中的值,计算填入新位以后,新的内层DP值也就是LCS的结果,再将其作为外层DP下一位的状态。预处理 \(tr_{i, j}\) 表示 \(f\) 的当前状态为 \(i\) ,往后加一个字符 \(j\) 所转移到的状态即可。外层转移时只要枚举之前的状态与当前要添加的字符即可。
时间复杂度 \(O(|\sum| \times 2^n (m + n))\) 。
#include <bits/stdc++.h>
using namespace std;
const int ch[] = {'A', 'G', 'C', 'T'};
const int Mod = 1e9 + 7;
const int N = 17, S = 4;
int f[2][1 << N | 1], tr[1 << N | 1][S], ans[N];
char str[N];
int n, m;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int encode(int *seq) {
int res = 0;
for (int i = 1; i <= n; ++i)
res = res << 1 | (seq[i] != seq[i - 1]);
return res;
}
inline void decode(int *seq, int state) {
for (int i = n; i; --i)
seq[i] = state & 1, state >>= 1;
for (int i = 1; i <= n; ++i)
seq[i] += seq[i - 1];
}
inline void prework() {
static int f[N], g[N];
for (int i = 0; i < (1 << n); ++i) {
decode(g, i);
for (int j = 0; j < 4; ++j) {
for (int k = 1; k <= n; ++k) {
f[k] = max(f[k - 1], g[k]);
if (ch[j] == str[k])
f[k] = max(f[k], g[k - 1] + 1);
}
tr[i][j] = encode(f);
}
}
}
signed main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%s%d", str + 1, &m);
n = strlen(str + 1);
prework();
memset(f[0], 0, sizeof(f[0]));
f[0][0] = 1;
for (int i = 1; i <= m; ++i) {
memset(f[i & 1], 0, sizeof(f[i & 1]));
for (int j = 0; j < (1 << n); ++j)
for (int k = 0; k < 4; ++k)
f[i & 1][tr[j][k]] = add(f[i & 1][tr[j][k]], f[~i & 1][j]);
}
memset(ans, 0, sizeof(int) * (n + 1));
for (int i = 0; i < (1 << n); ++i) {
int lcs = __builtin_popcount(i);
ans[lcs] = add(ans[lcs], f[m & 1][i]);
}
for (int i = 0; i <= n; ++i)
printf("%d\n", ans[i]);
}
return 0;
}
给定长度为 \(k\) 的模式串 \(s\) ,令 \(f(i)\) 为满足下面几条限制的字符串个数:
- 长度为 \(n\) ,字符集为 \(\{\mathtt{N,O,I}\}\) 。
- 不包含子串 \(\mathtt{NOI}\) 。
- 与模式串的最长公共子序列长度为 \(i\) 。
对于 \(i\in [0,k]\),求 \(f_i \bmod (10^9 + 7)\) 。
\(n \leq 10^3, k \leq 15\)
大体与上一题是类似的,外层DP记录一下与 \(\mathtt{NOI}\) 的匹配长度即可。
#include <bits/stdc++.h>
using namespace std;
const int ch[] = {'N', 'O', 'I'};
const int Mod = 1e9 + 7;
const int M = 17, S = 3;
int tr[1<< M | 1][S], f[2][1 << M | 1][S], ans[M];
char str[M];
int n, m;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int encode(int *seq) {
int res = 0;
for (int i = 1; i <= m; ++i)
res = res << 1 | (seq[i] - seq[i - 1]);
return res;
}
inline void decode(int *seq, int state) {
for (int i = m; i; --i)
seq[i] = state & 1, state >>= 1;
for (int i = 1; i <= m; ++i)
seq[i] += seq[i - 1];
}
inline void prework() {
static int f[M], g[M];
for (int i = 0; i < (1 << m); ++i) {
decode(g, i);
for (int j = 0; j < S; ++j) {
for (int k = 1; k <= m; ++k) {
f[k] = max(f[k - 1], g[k]);
if (str[k] == ch[j])
f[k] = max(f[k], g[k - 1] + 1);
}
tr[i][j] = encode(f);
}
}
}
signed main() {
scanf("%d%d%s", &n, &m, str + 1);
prework();
f[0][0][0] = 1;
for (int i = 1; i <= n; ++i) {
memset(f[i & 1], 0, sizeof(f[i & 1]));
for (int j = 0; j < (1 << m); ++j)
for (int k = 0; k < S; ++k)
for (int p = 0; p < S; ++p)
if (p == k) {
if (k + 1 < S)
f[i & 1][tr[j][p]][k + 1] = add(f[i & 1][tr[j][p]][k + 1], f[~i & 1][j][k]);
} else
f[i & 1][tr[j][p]][!p] = add(f[i & 1][tr[j][p]][!p], f[~i & 1][j][k]);
}
for (int i = 0; i < (1 << m); ++i) {
int lcs = __builtin_popcount(i);
for (int j = 0; j < S; ++j)
ans[lcs] = add(ans[lcs], f[n & 1][i][j]);
}
for (int i = 0; i <= m; ++i)
printf("%d\n", ans[i]);
return 0;
}
P8352 [SDOI/SXOI2022] 小 N 的独立集
给定 \(n\) 个点的树,每个点的权值范围为 \(k\) ,对于所有 \(i \in [1,kn]\) ,求有多少种权值分配方案,使得树的最大权独立集大小为 \(i\) 。
\(n \leq 10^3, k \leq 5\)
设 \(f_{u,0/1}\) 表示 \(u\) 子树内是否强制不选节点 \(u\) 时的最大方案大小,转移显然。
可以得到一条重要的性质:\(0 \le f_{u,0}-f_{u,1} \le val_{u} \le k\)(强制不选的答案肯定小于等于无限制的答案,且两者一定不会差出 \(u\) 点的权值),此时自然可以设 \(g_{u,v1,d}\) 表示 \(u\) 子树中 \(f_{u,0},f_{u,1}\) 分别为 \(v1+d,v1\) 时的方案数,枚举 \(g_{u, i, j}\) 和 \(g_{v, p, q}\) ,易得转移:
时间复杂度\(O(n^2k^4)\),加判 \(0\) 跑得很快。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 1e3 + 7, K = 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int f[N][N * K][K], g[N * K][K], siz[N];
int n, k;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
void dfs(int u, int fa) {
siz[u] = 1;
fill(f[u][0] + 1, f[u][0] + k + 1, 1);
for (int v : G.e[u]) {
if (v == fa)
continue;
dfs(v, u);
memset(g, 0, sizeof(g));
for (int i = 0; i <= k * siz[u]; ++i)
for (int j = 0; j <= k; ++j)
if (f[u][i][j])
for (int p = 0; p <= k * siz[v]; ++p)
for (int q = 0; q <= k; ++q)
if (f[v][p][q])
g[i + p + q][max(j - q, 0)] = add(g[i + p + q][max(j - q, 0)], 1ll * f[u][i][j] * f[v][p][q] % Mod);
memcpy(f[u], g, sizeof(g));
siz[u] += siz[v];
}
}
signed main() {
n = read(), k = read();
for (int i = 1; i < n; ++i) {
int u = read(), v = read();
G.insert(u, v), G.insert(v, u);
}
dfs(1, 0);
for (int i = 1; i <= n * k; ++i) {
int ans = 0;
for (int j = 0; j <= min(i, k); ++j)
ans = add(ans, f[1][i - j][j]);
printf("%d\n", ans);
}
return 0;
}
牌堆里有 \(4n\) 张牌,牌面为 \(1 \sim n\) 的每张牌各 \(4\) 张。
定义:
- 面子:形如 \(\{ i, i, i \}\) 或 \(\{ i - 1, i, i + 1 \}\) 的牌面。
- 对子:形如 \(\{ i, i \}\) 的牌面。
- 一个牌集 \(S\) 是胡的当且仅当 \(|S| = 14\) 且满足其可以被划分为一个对子和四个面子或其可以被划分为七个互异对子。
给出 \(13\) 张麻将牌,期望再摸多少张牌可以满足存在一个胡的子集。
\(5 \leq n \leq 100\)
一个显然的性质:一副牌仅需要考虑其每张牌的张数,顺序是无用的。因此对于一副牌可以将其转化为一个长度为 \(n\) ,每个位置上为 \(0\sim4\) 的序列。
在前面性质的基础上,我们来考虑如何判断一副牌,即判断一个长度为 \(n\) 的序列是否能胡,考虑建一个自动机去处理。如果能得出一个判断一副牌是否能胡的DP,然后把每个状态看作自动机的点,DP转移看作自动机的边,则一个自动机就建成了。下面考虑如何用DP判断一副牌是否能胡。
先特判掉七个对子的情况,设 \(f_{0/1,i,j,k}\) 表示处理完前 \(i\) 种牌,还剩 \(j\) 组 \((i-1,i)\) 以及 \(k\) 张 \(i\) ,且存在/不存在对子时最多的面子数。
由于 \(j\ge3\) 时,我们可以用 \(3\) 个 \(i-1\) 和 \(3\) 个 \(i\) 各自组成面子;\(k\ge3\) 时,我们可以直接用 \(3\) 个 \(i\) 组成面子。因此 \(0\le j,k\le2\) ,所以可以考虑建一个 \(3 \times 3\) 的矩阵存下 \(f_{0/1,i}\) 的全部答案。
转移时枚举若干张牌和之前的 \((i-2,i-1)\) 拼面子,保留若干组 \((i-1,i)\) 和若干张 \(i\),然后拿剩下的牌尽可能地拼面子,这样即可进行转移。
根据定义,若 \(f_{1,i}>3\) ,这副牌就能胡了。接下来我们考虑如何将这个DP转化为自动机。
首先,我们确定一个初始状态(一张牌都没有)。然后以类似于BFS的方式,找到未处理过的节点,枚举新加入的牌数,然后通过DP转移的方式得出子节点的状态。
不难发现,前面的 \(i\) 在这里没有任何作用,可以不用记录。因为我们是从每个节点一步步转移的。而 \(0/1\) 这个状态还是十分必要的,因此可以考虑对自动机上每个节点开两个矩阵 \(P_{0/1}\) 来进行转移。
此外,由于七对子也可以胡,我们再开一个变量 \(t\) 记录出现的对子个数。于是一个节点是胡的,当且仅当其 \(P_1\) 中存在一个元素大于 \(3\) 或 \(t\ge7\) 。
为了提高效率,我们可以把所有胡的节点全部压成一个节点,以其 \(t=-1\) 作为特殊标记即可。
接下来考虑在胡牌自动机上DP。设 \(g_i\) 表示摸了 \(i\) 张牌后不胡的方案数,则答案为:
然后设 \(f_{i,j,k}\) 表示处理到第 \(i\) 张牌,共摸了 \(j\) 张牌,走到了胡牌自动机上的 \(k\) 号节点的方案数。
那么显然我们可以枚举一个摸的牌数 \(t\)(\(0≤t≤4-a_i\) ,其中 \(a_i\) 为初始 \(13\) 张牌中 \(i\) 的张数),然后从 \(f_{i,j,k}\) 向 \(f_{i+1,j+t,O_k.Son_{a_i+t}}\) 转移,其中 \(O_k.Son_{a_i+t}\) 表示胡牌自动机上 \(k\) 号节点的第 \(a_i+t\) 个儿子。记得乘上还有 \(4-a_i\) 张牌中选 \(t\) 张牌的方案数 \(C_{4-a_i}^t\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 1e2 + 7, M = N << 2 | 1;
int fac[M], inv[M], invfac[M];
int a[N];
int n, m;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline void prework(int n) {
fac[0] = fac[1] = 1;
inv[0] = inv[1] = 1;
invfac[0] = invfac[1] = 1;
for (int i = 2; i <= n; ++i) {
fac[i] = 1ll * fac[i - 1] * i % Mod;
inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
}
}
inline int C(int n, int m) {
return m > n ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}
namespace HAM {
const int SIZE = 3e3 + 7;
struct Matrix {
int f[3][3];
inline Matrix() {
memset(f, -1, sizeof(f));
}
inline int *operator [] (const int x) {
return f[x];
}
inline bool operator != (Matrix o) const {
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 3; ++j)
if (f[i][j] != o[i][j])
return true;
return false;
}
inline bool operator < (Matrix o) const {
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 3; ++j)
if (f[i][j] != o[i][j])
return f[i][j] < o[i][j];
}
inline bool Check() {
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 3; ++j)
if (f[i][j] > 3)
return true;
return false;
}
inline void calc(Matrix o, const int t) {
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 3; ++j)
if (~o[i][j])
for (int k = 0; k < 3 && i + j + k <= t; ++k)
f[j][k] = max(f[j][k], min(i + o[i][j] + (t - i - j - k) / 3, 4));
}
};
struct Node {
Matrix P[2];
int S[5];
int t;
inline Node() {
t = S[0] = S[1] = S[2] = S[3] = S[4] = 0, P[0] = P[1] = Matrix();
}
inline bool operator < (const Node &o) const {
return t ^ o.t ? t < o.t : (P[0] != o.P[0] ? P[0] < o.P[0] : (P[1] != o.P[1] ? P[1] < o.P[1] : 0));
}
inline bool IsHu() {
return !~t || t >= 7 || P[1].Check();
}
inline Node Hu() {
Node x;
return x.t = -1, x;
}
inline Node insert(int x) {
if (IsHu())
return Hu();
Node res;
res.P[0].calc(P[0], x), res.P[1].calc(P[1], x), res.t = t;
if (x > 1)
res.P[1].calc(P[0], x - 2), ++res.t;
if (res.IsHu())
res = Hu();
return res;
}
} idx[SIZE];
map<Node, int> mp;
int f[N][M][SIZE];
int tot;
inline int getid(Node x) {
return mp.count(x) ? mp[x] : (idx[mp[x] = ++tot] = x, tot);
}
inline Node Begin() {
Node x;
return x.P[0][0][0] = 0, x;
}
inline Node Hu() {
Node x;
return x.t = -1, x;
}
inline void prework() {
mp[idx[1] = Begin()] = 1, mp[idx[2] = Hu()] = tot = 2;
for (int i = 1; i <= tot; ++i)
if (i != 2)
for (int j = 0; j < 5; ++j)
idx[i].S[j] = getid(idx[i].insert(j));
}
inline void solve() {
f[0][0][1] = 1;
for (int i = 1; i <= n; ++i)
for (int j = m; ~j; --j)
for (int k = 1; k <= tot; ++k)
if (f[i - 1][j][k])
for (int t = 0; t <= 4 - a[i]; ++t)
f[i][j + t][idx[k].S[a[i] + t]] = add(f[i][j + t][idx[k].S[a[i] + t]], 1LL * f[i - 1][j][k] * C(4 - a[i], t) % Mod);
}
} // namespace HAM
signed main() {
HAM::prework();
n = read();
for (int i = 1; i <= 13; ++i)
++a[read()], read();
prework(m = n * 4 - 13);
HAM::solve();
int ans = 0;
for (int i = 1; i <= m; ++i)
for (int j = 1; j <= HAM::tot; ++j)
if (j != 2)
ans = add(ans, 1ll * HAM::f[n][i][j] * fac[i] % Mod * fac[m - i] % Mod);
printf("%d", add(1ll * ans * invfac[m] % Mod, 1));
return 0;
}