DP of DP

DP of DP

主要思想是将内层 DP 的结果作为外层 DP 的状态进行 DP 。

一般来说需要构建一个判定合法状态的 DFA,然后 DP 状态用一维表示在 DFA 上的状态,意义为若干步之后在 DFA 上的某点的方案数。

为了存储状态,通常需要满足内层 DP 的转移结果很小,才能记录在状态中,有时需要通过一些技巧优化内层结果值域。

[ABC391G] Many LCS

给出一个长度为 \(n\) 的小写字符串 \(S\) ,对于所有 \(0 \le k \le n\) ,求所有长度为 \(m\) 且与 \(S\) 的最长公共子序列长度为 \(k\) 的小写字符串 \(T\) 的数量。

\(n \le 10\)\(m \le 100\)

对于求 LCS​ ,一个经典做法是设 \(f_{i, j}\) 表示考虑到了 \(S_i, T_j\) 的答案,则:

\[f_{i, j} = \begin{cases} \max \{ f_{i - 1, j}, f_{i, j - 1} \} & S_i \ne T_j \\ f_{i - 1, j - 1} + 1 & S_i = S_j \end{cases} \]

\(F_{i, s}\) 表示考虑到 \(T\) 的第 \(i\) 位,\(f_{1 \sim n, i}\) 的状态为 \(s\) 的方案数。转移时枚举下一位填的数字,然后更新 \(s\) 作为外层 DP 下一位的状态。

但是发现 \(f_{i, j}\) 的状态数太多,不能直接存。注意到 \(f_{i, j} - f_{i, j - 1} \in [0, 1]\) ,那么可以考虑记录差分数组,状态就压缩到了 \(2^n\) 种。

预处理 \(tr_{i, j}\) 表示的当前状态为 \(i\) ,往后加一个字符 \(j\) 所转移到的状态,时间复杂度 \(O(|\sum| \times 2^n (m + n))\)

类似的题目:

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 10, M = 1e2 + 7, S = 26;

int f[M][1 << N], tr[1 << N][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 dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline int encode(vector<int> vec) {
    int res = 0;
    
    for (int i = 1; i <= n; ++i)
        res = res << 1 | (vec[i] > vec[i - 1]);
    
    return res;
}

inline vector<int> decode(int state) {
    vector<int> vec(n + 1);

    for (int i = n; i; --i)
        vec[i] = state & 1, state >>= 1;
    
    for (int i = 1; i <= n; ++i)
        vec[i] += vec[i - 1];

    return vec;
}

inline void prework() {
    for (int i = 0; i < (1 << n); ++i) {
        vector<int> g = decode(i);
        
        for (int j = 0; j < S; ++j) {
            vector<int> f(n + 1);

            for (int k = 1; k <= n; ++k)
                f[k] = (str[k] == 'a' + j ? g[k - 1] + 1 : max(f[k - 1], g[k]));
        
            tr[i][j] = encode(f);
        }
    }
}

signed main() {
    scanf("%d%d%s", &n, &m, str + 1);
    prework(), f[0][0] = 1;
    
    for (int i = 1; i <= m; ++i)
        for (int j = 0; j < (1 << n); ++j)
            for (int k = 0; k < S; ++k)
                f[i][tr[j][k]] = add(f[i][tr[j][k]], f[i - 1][j]);
    
    for (int i = 0; i < (1 << n); ++i) {
        int lcs = __builtin_popcount(i);
        ans[lcs] = add(ans[lcs], f[m][i]);
    }
    
    for (int i = 0; i <= n; ++i)
        printf("%d ", ans[i]);
    
    return 0;
}

CF1142D Foreigner

定义一个整数时好的当且仅当其满足以下两个条件之一:

  • \(x \in [1, 9]\)
  • \(\lfloor \frac{x}{10} \rfloor\) 是好的,且记 $\lfloor \frac{x}{10} \rfloor $ 在好的数字中的排名为 \(k\) ,则 \(x\) 的最后一位必须严格小于 \(k \bmod 11\)

给出一个数字串,求有多少子串是好的。

\(n \le 10^5\)

题目给出的判定方式是一个递归的方式,这并不好看,考虑逆过来,变成不断在末尾加数构造出一个好的数字,这样就可以从 \(r = i - 1\) 的答案推到 \(r = i\) 的答案。

\(g(i, j)\) 表示排名为 \(i\) 的好数在后面加上 \(j\) 后的排名,由于排名小于 \(i\) 的数在后面加上数字后均小于它,因此:

\[g(i, j) = 9 + (\sum_{k = 1}^{i - 1} k \bmod 11) + j + 1 \]

由于后面加的数的合法性仅与 \(g(i, j) \bmod 11\) 有关,所以考虑设 \(f_{i, j}\) 表示排名以 \(i\) 结尾、排名 \(\bmod 11\)\(j\) 的数的数量。若 \(j > S[i]\) ,则有转移:

\[f_{i - 1, j} \to f_{i, g(j, S[i]) \bmod 11} \]

时间复杂度 \(O(n \times|\sum|)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e5 + 7, S = 11;

int f[N][S];
char str[N];

int n;

inline int g(int i, int j) {
    return (10 + i * (i - 1) / 2 + j) % 11;
}

signed main() {
    scanf("%s", str + 1), n = strlen(str + 1);
    ll ans = 0;

    for (int i = 1; i <= n; ++i) {
        f[i][str[i] & 15] = (str[i] > '0');

        for (int j = (str[i] & 15) + 1; j < S; ++j)
            f[i][g(j, str[i] & 15)] += f[i - 1][j];

        for (int j = 0; j < S; ++j)
            ans += f[i][j];
    }

    printf("%lld", ans);
    return 0;
}

P8386 [PA 2021] Od deski do deski

给定 \(n, m\) ,求满足以下条件的长度为 \(n\) 的序列数量:

  • 每个数 \(\in [1, m]\)
  • 一次操作定义为删除一个长度 \(\ge 2\) 且区间两端相等的区间,该序列需要在若干次操作内被删空。

\(n \le 3000\)\(m \le 10^9\)

考虑如何判定一个序列合法,设 \(g_i\) 表示 \(1 \sim i\) 是否合法,则 \(g_i = \or_{a_j = a_i} g_{j - 1}\)

考虑 DP of DP,由于判定过程中只需要知道添加最后一个数字后是否合法,考虑在状态中记录合法的数的数量。设 \(f_{i, j, 0/1}\) 表示长度为 \(i\) ,在末尾有 \(j\) 种添加方案使其合法,当前是否合法的方案数,答案即为 \(\sum f_{n, i, 1}\) 。考虑转移:

\[\begin{aligned} f_{i, j, 1} \times j &\to f_{i + 1, j, 1} \\ f_{i, j, 1} \times (m - j) &\to f_{i + 1, j + 1, 0} \\ f_{i, j, 0} \times j &\to f_{i + 1, j , 1} \\ f_{i, j, 0} \times (m - j) &\to f_{i + 1, j, 0} \end{aligned} \]

时间复杂度 \(O(n^2)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 3e3 + 7;

int f[N][N][2];

int n, m;

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;
}

signed main() {
    scanf("%d%d", &n, &m);
    f[0][0][1] = 1;

    for (int i = 0; i < n; ++i)
        for (int j = 0; j <= i; ++j) {
            f[i + 1][j][1] = add(f[i + 1][j][1], 1ll * f[i][j][1] * j % Mod);
            f[i + 1][j + 1][0] = add(f[i + 1][j + 1][0], 1ll * f[i][j][1] * (m - j) % Mod);
            f[i + 1][j][1] = add(f[i + 1][j][1], 1ll * f[i][j][0] * j % Mod);
            f[i + 1][j][0] = add(f[i + 1][j][0], 1ll * f[i][j][0] * (m - j) % Mod);
        }

    int ans = 0;

    for (int i = 0; i <= n; ++i)
        ans = add(ans, f[n][i][1]);

    printf("%d", ans);
    return 0;
}

P8352 [SDOI/SXOI2022] 小 N 的独立集

给定 \(n\) 个点的树,每个点的权值范围为 \(k\) ,对于所有 \(i \in [1,kn]\) ,求有多少种权值分配方案,使得树的最大权独立集大小为 \(i\)

\(n \le 10^3\)\(k \le 5\)

\(f_{u, 0/1}\) 表示 \(u\) 子树内 \(u\) 是否选择的最大独立集,则:

\[f_{u, 0} = \sum_{v \in son(u)} \max(f_{v, 0}, f_{v, 1}) \\ f_{u, 1} = a_u + \sum_{v \in son(u)} f_{v, 0} \]

\(g_{u, p, q}\) 表示 \(f_{u, 0} = p, f_{u, 1} = q\) 的方案数,则:

\[g_{u, i, j} \times g_{v, p, q} \to g'_{u, i + \max(p, q), j + p} \]

时间复杂度 \(O(n^4 k^4)\) ,无法通过。

注意到上述做法的瓶颈在于状态数过大,达到了 \(O(n^3 k^2)\) 的级别,并且看起来没有什么优化空间。

考虑换一个内层 DP,设 \(f_{u, 0}\) 表示不选 \(u\) 的最大独立集,\(f_{u, 1}\) 表示无限制的最大独立集,则:

\[f_{u, 0} = \sum_{v \in son(u)} f_{v, 1} \\ f_{u, 1} = \max \{ f_{u, 0} , a_u + \small \sum_{v \in son(u)} f_{v, 0} \} \]

则可以得出一个关键性质:\(0 \le f_{u, 0} - f_{u, 1} \le a_u \le k\)

\(g_{u, i, j}\) 表示 \(f_{u, 0} = i, f_{u, 1} = i + j\) 的方案数,则:

\[g_{u, i, j} \times g_{v, p, q} \to g'_{u, i + p + q, \max(i + j + p, i + p + q) - (i + 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;

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() {
    scanf("%d%d", &n, &k);
    
    for (int i = 1; i < n; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        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;
}

P5279 [ZJOI2019] 麻将

牌堆里有 \(4n\) 张牌,牌面为 \(1 \sim n\) 的每张牌各 \(4\) 张。

定义:

  • 面子:形如 \(\{ i, i, i \}\)\(\{ i - 1, i, i + 1 \}\) 的牌面。
  • 对子:形如 \(\{ i, i \}\) 的牌面。
  • 一个牌集 \(S\) 是胡的当且仅当 \(|S| = 14\) 且满足其可以被划分为一个对子和四个面子或其可以被划分为七个互异对子。

给出 \(13\) 张麻将牌,期望再摸多少张牌可以满足存在一个胡的子集。

\(5 \le n \le 100\)

可以发现一副牌是否能胡仅与每张牌的数量有关,因此对于一副牌可以将其转化为一个长度为 \(n\) ,每个位置上为 \(0\sim4\) 的序列。

考虑如何判断一副牌是否能胡,可以建一个自动机处理。如果能得出一个判断一副牌是否能胡的 DP ,然后把每个状态看作自动机的点,DP转移看作自动机的边,即可建立自动机。

考虑如何 DP 判断一副牌是否能胡。先特判掉七个对子的情况,设 \(f_{0/1,i,j,k}\) 表示处理完前 \(i\) 种牌,还剩 \(j\)\((i-1,i)\) 以及 \(k\)\(i\) ,是否存在对子的最多面子数,\(f_{1, i, j, k} > 3\) 时就胡了。

由于:

  • \(j \ge 3\) 时可以用 \(3\)\(i-1\)\(3\)\(i\) 各自组成面子。
  • \(k \ge 3\) 时可以直接用 \(3\)\(i\) 组成面子。

因此 \(j, k \in [0, 2]\) ,所以可以用一个 \(3 \times 3\) 的矩阵存下 \(f_{0/1,i}\)

转移时枚举若干张牌和之前的 \((i-2,i-1)\) 拼面子,保留若干组 \((i-1,i)\) 和若干张 \(i\),然后拿剩下的牌尽可能地拼面子,这样即可进行转移。

接下来考虑将这个 DP 转化为自动机。首先确定空牌集为初始状态。然后以类似于 BFS 的方式,找到未处理过的节点,枚举新加入的牌数,然后通过 DP 转移的方式得出子节点的状态。

考虑对自动机上每个节点开两个矩阵 \(P_{0/1}\) 来进行转移。此外,由于七对子也可以胡,考虑再开一个变量 \(t\) 记录出现的对子个数。于是一个节点是胡的,当且仅当其 \(P_1\) 中存在一个元素大于 \(3\)\(t\ge7\)

常数优化可以考虑把所有胡的节点全部压成一个节点,以其 \(t=-1\) 作为特殊标记即可。

接下来考虑在自动机上DP,设 \(g_i\) 表示摸了 \(i\) 张牌后不胡的方案数,则答案为:

\[\frac{\sum_{i=1}^{4n-13}g_ii!(4n-13-i)!}{(4n-13)!}+1 \]

\(f_{i,j,k}\) 表示处理到第 \(i\) 张牌,共摸了 \(j\) 张牌,走到了自动机上的 \(k\) 号节点的方案数。

转移考虑可以枚举一个摸的牌数 \(t\)\(t \in [0, 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;
}

[ARC193B] Broken Wheel

给定一个长度为 \(n\) 的 01 串 \(S\) ,其对应一张 \(n + 1\) 个点的无向图,图由两种边构成:

  • 对于 \(i = 0, 1, \cdots, n - 1\)\(i\)\((i + 1) \bmod n\) 之间存在一条边。
  • 对于 \(i = 0, 1, \cdots, n - 1\)\(S_i = 1\)\(i\)\(n\) 之间存在一条边。

求将无向图定向后入度序列的种数。

\(n \le 10^6\)

首先由于度数和一定,因此只要对 \(d_{0 \sim n - 1}\) 计数即可。

考虑设计一个 DP 判定 \(d_{0 \sim n - 1}\) 的合法性,设 \(f_{i, 0/1, 0/1}\) 表示考虑前 \(i\) 个点,固定 \((0, n - 1)\)\((i, i + 1)\) 的边的方向时是否合法,转移枚举当前点的度数即可。

剩下的就是对这个 DP 的取值进行 DP 套 DP 即可,时间复杂度 \(O(n)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 1e6 + 7;

int f[N][1 << 4];
char str[N];

int n;

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;
}

signed main() {
    scanf("%d%s", &n, str);

    ++f[0][1 << 2]; // d[0] = 0 : 0 -> n - 1, 0 -> 1

    if (str[0] == '1') // d[0] = 1
        ++f[0][(1 << 3) | (1 << 0) | (1 << 2)]; // 1 -> 0 -> n - 1 | n - 1 -> 0 -> 1 | 0 -> n - 1, 0 -> 1, n -> 0
    else
        ++f[0][(1 << 3) | (1 << 0)]; // 1 -> 0 -> n - 1 | n - 1 -> 0 -> 1


    if (str[0] == '1') // d[0] = 2
        ++f[0][(1 << 1) | (1 << 3) | (1 << 0)]; // n - 1 -> 0, 1 -> 0 | 1 -> 0 -> n - 1, n -> 0 | n - 1 -> 0 -> 1, n -> 0
    else
        ++f[0][1 << 1]; // n - 1 -> 0, 1 -> 0

    if (str[0] == '1') // d[0] = 3
        ++f[0][1 << 1]; // n - 1 -> 0, 1 -> 0, n -> 0

    for (int i = 0; i < n - 1; ++i)
        for (int s = 0; s < (1 << 4); ++s) {
            if (!f[i][s])
                continue;

            int g[2][2] = {{s >> 0 & 1, s >> 1 & 1}, {s >> 2 & 1, s >> 3 & 1}};

            for (int j = 0; j <= 3; ++j) { // d[i + 1]
                int h[2][2] = {{0, 0}, {0, 0}};

                for (int k = 0; k < 2; ++k) // (i, i + 1)
                    for (int l = 0; l < 2; ++l) // (i + 1, i + 2)
                        if ((k ^ 1) + l <= j && j <= (k ^ 1) + l + (str[i + 1] & 15))
                            h[0][l] |= g[0][k], h[1][l] |= g[1][k];

                int t = (h[0][0] << 0) | (h[0][1] << 1) | (h[1][0] << 2) | (h[1][1] << 3);
                f[i + 1][t] = add(f[i + 1][t], f[i][s]);
            }
        }

    int ans = 0;

    for (int s = 0; s < (1 << 4); ++s)
        if ((s >> 0 & 1) || (s >> 3 & 1))
            ans = add(ans, f[n - 1][s]);
    
    printf("%d", ans);
    return 0;
}

[AGC022E] Median Replace

给出一个长度为 \(n\) 的 01 串,其中有若干位置是 ? ,并保证 \(n\) 是奇数。

一次操作可以将三个连续的字符替换成这三个数的中位数。

求有多少将 ? 替换为 01 的方案使得 \(\frac{n - 1}{2}\) 次操作后最后可以剩下 \(1\)

\(n \le 3 \times 10^5\)

考虑如何判定一个 01 串合法。维护一个栈,栈底到栈顶由一段 \(1\) 和一段 \(0\) 组成。

按顺序将每一个数加入栈中,并进行如下操作:

  • 当前加入的数为 \(0\) :若栈顶有两个 \(0\) ,则将它们三个绑一组消成一个 \(0\)(即弹栈),否则将其入栈。
  • 当前加入的数为 \(1\)
    • 栈顶为 \(0\) :则栈顶的 \(0\) 、当前的 \(1\) 和任意数绑一组,前两者都会互相抵消,因此这两个数实际没用,直接弹栈。
    • 栈顶为 \(1\) :若已经有两个 \(1\) ,则说明该串一定合法,此时忽略新的 \(1\) 即可,否则将其入栈。

最后若栈内 \(1\) 的数量大于 \(0\) 的数量则合法。

不难发现栈内的 \(0\)\(1\) 只会有 \(0 \sim 2\) 个,因此可以设 \(f_{i, a, b}\) 表示考虑到 \(i\) ,栈内有 \(a\)\(0\)\(b\)\(1\) 的方案数,转移不难做到 \(O(n)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 3e5 + 7;

int f[N][3][3];
char str[N];

int n;

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;
}

signed main() {
    scanf("%s", str + 1), n = strlen(str + 1);
    f[0][0][0] = 1;
    
    for (int i = 1; i <= n; ++i)
        for (int j = 0; j <= 2; ++j)
            for (int k = 0; k <= 2; ++k) {
                if (str[i] != '1') {
                    if (k == 2)
                        f[i][j][1] = add(f[i][j][1], f[i - 1][j][k]);
                    else
                        f[i][j][k + 1] = add(f[i][j][k + 1], f[i - 1][j][k]);
                }

                if (str[i] != '0') {
                    if (k)
                        f[i][j][k - 1] = add(f[i][j][k - 1], f[i - 1][j][k]);
                    else
                        f[i][min(j + 1, 2)][k] = add(f[i][min(j + 1, 2)][k], f[i - 1][j][k]);
                }
            }
    
    int ans = 0;
    
    for (int i = 0; i <= 2; ++i)
        for (int j = 0; j <= i; ++j)
            ans = add(ans, f[n][i][j]);
    
    printf("%d", ans);
    return 0;
}

CF924F Minimal Subset Difference

\(f(n)\) 表示在 \(n\) 的所有相邻数位之间插入加号或减号,最终得到的算式的值的绝对值的最小值。

\(T\) 组询问,每次给定 \(l, r, k\) ,求 \(n \in [l, r]\)\(f(n) \le k\)\(n\) 的数量。

\(T \le 5 \times 10^4\)\(1 \le l \le r \le 10^{18}\)\(0 \le k \le 9\)

先考虑如何求 \(f\) ,只要做背包即可。

考虑背包的值域,有结论:对于值域为 \([0, w]\) 的序列,最小化 \(f(n)\) 时最大前缀和的绝对值 \(\le w(w - 1)\)

由于正负是对称的,因此只要记录 \([0, 72]\) 即可,转移就是 \(f_j \to f_{j + n_i}\) 以及 \(f_j \to f_{|j - n_i|}\)

暴搜可以发现对于所有 \(n\) ,有效的背包状态只有 \(10^4\) 级别,因此可以压到一个 __int128 里用 map 存储。

预处理 \(g_{i, S, k}\) 表示无最高位限制,还要填 \(i\) 位,此时背包状态为 \(S\)\(f(n) \le k\) 方案数,直接数位 DP 即可。

注意背包时 \(j + n_i\)\(j - n_i\) 的部分用位运算,\(n_i - j\) 的部分暴力枚举 \(j \in [0, n_i]\) 可以优化复杂度。注意 __int128 的常数。

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e4 + 7;

map<__int128, int> mp;

__int128 num[N], trans[N][10];
ll f[20][N][10];
int digit[20];

__int128 all = ((__int128)1 << 73) - 1;
int tot;

void dfs1(int d, int u) {
    if (~f[d][u][0])
        return;
    else if (!d) {
        for (int i = 0; i <= 9; ++i)
            f[d][u][i] = num[u] & ((1 << (i + 1)) - 1) ? 1 : 0;

        return;
    }

    memset(f[d][u], 0, sizeof(f[d][u]));

    for (int i = 0; i <= 9; ++i) {
        if (trans[u][i] == -1) {
            __int128 v = ((num[u] << i) | (num[u] >> i)) & all;

            for (int j = 0; j <= i; ++j)
                v |= (num[u] >> j & 1) << (i - j);

            if (mp.find(v) == mp.end())
                num[mp[v] = ++tot] = v;

            trans[u][i] = mp[v];
        }

        dfs1(d - 1, trans[u][i]);

        for (int j = 0; j <= 9; ++j)
            f[d][u][j] += f[d - 1][trans[u][i]][j];
    }
}

ll dfs2(int x, bool lead, int s, int k) {
    if (!lead)
        return f[x][s][k];
    else if (!x)
        return num[s] & ((1 << (k + 1)) - 1) ? 1 : 0;

    ll res = 0;

    for (int i = 0, high = lead ? digit[x] : 9; i <= high; ++i)
        res += dfs2(x - 1, lead && i == high, trans[s][i], k);

    return res;
}

inline ll solve(ll n, int k) {
    int len = 0;

    do
        digit[++len] = n % 10, n /= 10;
    while (n);

    return dfs2(len, true, 1, k);
}

signed main() {
    memset(f, -1, sizeof(f)), memset(trans, -1, sizeof(trans));
    num[mp[1] = ++tot] = 1;

    for (int i = 0; i < 20; ++i)
        dfs1(i, 1);

    int T;
    scanf("%d", &T);

    while (T--) {
        ll l, r;
        int k;
        scanf("%lld%lld%d", &l, &r, &k);
        printf("%lld\n", solve(r, k) - solve(l - 1, k));
    }

    return 0;
}

P8194 [USACO22FEB] Phone Numbers P

有一个九键手机。需要打出一个目标序列,可以用以下三种手法:

  • 按下某个键。

  • 同时按下相邻的两个键;

  • 同时按下组成一个正方形的四个键。

同时按下若干个键时,这些键的输入顺序是随机的。当然,这些输入方式必须可能打出目标序列。

给定实际输入的序列 \(s\) ,求有多少种可能的目标序列 \(t\)

数字串总长度 \(\le 10^5\)

先考虑如何判定一个序列合法,设 \(f_i\) 表示 \(1 \sim i\) 是否合法,转移就枚举 \(i\) 结尾的长度为 \(1, 2, 4\) 的后缀,判定其是否能一次按下并且与 \(s\) 的这一段对应等价。

接下来套路地考虑 DP of DP,记录状态为 \((i, t_{i - 2}, t_{i - 1}, t_i, f_{i - 3}, f_{i - 2}, f_{i - 1}, f_i)\) ,状态数为 \(n \times 9^3 \times 2^4\) ,一次转移需要枚举 \(9\) 种下一位,无法通过。

事实上有效的状态很少,考虑如下几个优化:

  • 若新添加的数字不在范围内(前后三个和中间)出现则也没有用。
  • \(t_{i - 2}, t_{i - 1}, t_i\) 不可能在一个正方形内,则 \(t_{i - 2}, f_{i - 3}\) 没有用,可以将这一部分压成一个等价类。
  • \(f_{i - 3} = 0\) ,则 \(t_{i - 2}\) 没有用,可以将这一部分压成一个等价类。
  • \(f_{i - 3} = f_{i - 2} = 0\) ,则 \(t_{i - 2}, t_{i - 1}\) 没有用,可以将这一部分压成一个等价类。
  • \(f_{i - 3} = f_{i - 2} = f_{i - 1} = 0\) ,则 \(t_{i - 2}, t_{i - 1}, t_i\) 没有用,可以将这一部分压成一个等价类。
  • \(f\)\(0\) ,则该状态显然没用,可以直接丢掉。

状态数只剩 \(100\) 多种,即可通过,可能需要一些卡常。

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 1e5 + 7;

int chk[1 << 10], num[N];
char str[N];

int n;

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 int encode(vector<int> t, vector<int> f) {
    t.resize(3), f.resize(4);
    int s = 0;

    for (int it : t)
        s = s * 10 + it;

    for (int it : f)
        s = s << 1 | it;

    return s;
}

inline pair<vector<int>, vector<int> > decode(int s) {
    vector<int> t(3), f(4);

    for (int i = 3; ~i; --i)
        f[i] = s & 1, s >>= 1;

    for (int i = 2; ~i; --i)
        t[i] = s % 10, s /= 10;

    return make_pair(t, f);
}

signed main() {
    chk[1 << 1] = chk[1 << 2] = chk[1 << 3] = 1;
    chk[1 << 4] = chk[1 << 5] = chk[1 << 6] = 1;
    chk[1 << 7] = chk[1 << 8] = chk[1 << 9] = 1;

    chk[(1 << 1) | (1 << 2)] = chk[(1 << 2) | (1 << 3)] = 1;
    chk[(1 << 4) | (1 << 5)] = chk[(1 << 5) | (1 << 6)] = 1;
    chk[(1 << 7) | (1 << 8)] = chk[(1 << 8) | (1 << 9)] = 1;
    chk[(1 << 1) | (1 << 4)] = chk[(1 << 4) | (1 << 7)] = 1;
    chk[(1 << 2) | (1 << 5)] = chk[(1 << 5) | (1 << 8)] = 1;
    chk[(1 << 3) | (1 << 6)] = chk[(1 << 6) | (1 << 9)] = 1;

    chk[(1 << 1) | (1 << 2) | (1 << 4) | (1 << 5)] = 1;
    chk[(1 << 2) | (1 << 3) | (1 << 5) | (1 << 6)] = 1;
    chk[(1 << 4) | (1 << 5) | (1 << 7) | (1 << 8)] = 1;
    chk[(1 << 5) | (1 << 6) | (1 << 8) | (1 << 9)] = 1;

    chk[(1 << 2) | (1 << 4) | (1 << 5)] = chk[(1 << 1) | (1 << 4) | (1 << 5)] = 2;
    chk[(1 << 1) | (1 << 2) | (1 << 5)] = chk[(1 << 1) | (1 << 2) | (1 << 4)] = 2;

    chk[(1 << 3) | (1 << 5) | (1 << 6)] = chk[(1 << 2) | (1 << 5) | (1 << 6)] = 2;
    chk[(1 << 2) | (1 << 3) | (1 << 6)] = chk[(1 << 2) | (1 << 3) | (1 << 5)] = 2;

    chk[(1 << 5) | (1 << 7) | (1 << 8)] = chk[(1 << 4) | (1 << 7) | (1 << 8)] = 2;
    chk[(1 << 4) | (1 << 5) | (1 << 8)] = chk[(1 << 4) | (1 << 5) | (1 << 7)] = 2;

    chk[(1 << 6) | (1 << 8) | (1 << 9)] = chk[(1 << 5) | (1 << 8) | (1 << 9)] = 2;
    chk[(1 << 5) | (1 << 6) | (1 << 9)] = chk[(1 << 5) | (1 << 6) | (1 << 8)] = 2;

    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%s", str + 1);
        n = strlen(str + 1);

        for (int i = 1; i <= n; ++i)
            num[i] = str[i] & 15;

        vector<pair<int, int> > f;
        f.emplace_back(encode((vector<int>){}, (vector<int>){1}), 1);

        for (int i = 1; i <= n; ++i) {
            vector<pair<int, int> > g;

            auto update = [&](vector<int> t, vector<int> f, int k) {
                if (t.size() == 3 && chk[(1 << t[0]) | (1 << t[1]) | (1 << t[2])] != 2)
                    t.pop_back(), f.pop_back();

                while (!t.empty() && !f.back())
                    t.pop_back(), f.pop_back();

                if (!t.empty() || f.back())
                	g.emplace_back(encode(t, f), k);
            };

            vector<int> vec(num + max(1, i - 3), num + min(n, i + 3) + 1);
            sort(vec.begin(), vec.end()), vec.erase(unique(vec.begin(), vec.end()), vec.end());
            int s[5] = {0, -1, -1, -1, -1};

            for (int j = 1; j <= min(i, 4) && (~s[j - 1] >> num[i - j + 1] & 1); ++j)
            	s[j] = s[j - 1] | (1 << num[i - j + 1]);

            auto check = [&](int siz, int state) {
            	return chk[state] && s[siz] == state;
            };

            for (auto it : f) {
                auto state = decode(it.first);
                auto t = state.first, f = state.second, nt = t, nf = f;
                nt.insert(nt.begin(), 0), nt.pop_back();
                nf.insert(nf.begin(), 0), nf.pop_back();

                for (int x : vec) {
                    bool flag = (f[0] && check(1, 1 << x)) ||
                        (f[1] && x != t[0] && check(2, (1 << x) | (1 << t[0]))) ||
                        (f[3] && check(4, (1 << x) | (1 << t[0]) | (1 << t[1]) | (1 << t[2])));
                    nt[0] = x, nf[0] = flag, update(nt, nf, it.second);
                }
            }

            sort(g.begin(), g.end()), f.clear();

            for (auto it : g) {
                if (!f.empty() && f.back().first == it.first)
                    f.back().second = add(f.back().second, it.second);
                else
                    f.emplace_back(it);
            }
        }

        int ans = 0;

        for (auto it : f)
            if (decode(it.first).second[0])
                ans = add(ans, it.second);

        printf("%d\n", ans);
    }

    return 0;
}

CF1383E Strange Operation

给定一个长度为 \(n\) 的 01 串 \(a_{1 \sim n}\) ,定义依次操作为选择任意一个 \(i \in [1, |a|)\) ,令 \(a_i = \max(a_i, a_{i + 1})\) ,然后删除 \(a_{i + 1}\) 。求若干次操作后得到的本质不同 01 串的数量 \(\bmod (10^9 + 7)\)

\(n \le 10^6\)

考虑一次操作的影响,若为 \(00\)\(10\)\(01\) 则等价于删去 \(0\) ,若为 \(11\) 则等价于删去 \(1\)

不难发现对于连续的一段 \(0\) ,删除任意一个都是等价的;对于连续的一段 \(1\) (长度 \(\ge 2\) ),删除任意一个都是等价的。

考虑用一个新序列表示相邻两个 \(1\) 之间的 \(0\) 的数量,注意开头的也要算上(没有 \(0\) 记为 \(0\) ),下面用 \(a_{1 \sim m}\) 表示,需要提前特判掉整个 01 串没有 \(1\) 的情况。此时一次操作有两种:

  • 将一个非 \(0\) 的位置减去 \(1\)
  • 删去一个不在首尾的 \(0\)

发现首尾的元素始终不会被删去,由乘法原理,最后将答案乘上 \((a_1 + 1) (a_m + 1)\) 即可,接下来去掉首尾元素统计。

考虑一个最终序列的合法性,对于两个新序列 \(a, b\) ,若 \(a\) 能被 \(b\) 生成,则一定存在一个 \(b\) 的长度为 \(|a|\) 的子序列 \(c_{1 \sim |a|}\) ,满足与 \(\forall 1 \le i \le |a|, a_i \le c_i\)

直接对一个最终状态判定是简单的,直接类似子序列自动机的方法贪心即可。

计数可以考虑 DP of DP,设 \(f_i\) 表示匹配到 \(a_i\) 的最终态的数量,考虑一个转移 \(f_j \to f_i\) 的条件,则最终态的当前值应 \(> a_{j + 1 \sim i - 1}\) ,则转移系数为 \(\max(0, b_i - \max_{k = j + 1}^{i - 1} b_k)\)

单调栈配合前缀和优化即可做到线性。

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 1e6 + 7;

int a[N], f[N], s[N], sta[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 dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

signed main() {
    scanf("%s", str + 1), n = strlen(str + 1);

    if (count(str + 1, str + n + 1, '0') == n)
        return printf("%d", n), 0;

    for (int i = 1; i <= n; ++i) {
        if (str[i] == '1')
            ++m;
        else
            ++a[m];
    }

    s[0] = f[0] = 1;

    for (int i = 1, top = 0; i < m; ++i) {
        f[i] = add(f[i], 1ll * (a[i] + 1) * f[i - 1] % Mod);

        while (top && a[i] >= a[sta[top]]) {
            if (top > 1)
                f[i] = add(f[i], 1ll * (a[i] - a[sta[top]]) * dec(s[sta[top] - 1], s[sta[top - 1] - 1]) % Mod);
            else
                f[i] = add(f[i], 1ll * (a[i] - a[sta[top]]) * s[sta[top] - 1] % Mod);

            --top;
        }

        sta[++top] = i, s[i] = add(s[i - 1], f[i]);
    }

    printf("%d", 1ll * (a[0] + 1) * (a[m] + 1) % Mod * s[m - 1] % Mod);
    return 0;
}

P8329 [ZJOI2022] 树

给定 \(N\) 和模数 \(M\) ,对于所有 \(n = 2, 3, \cdots, N\) ,求满足以下条件的树的二元组 \((T_1, T_2)\) 的方案数:

  • \(T_1\) 中每个点的父亲编号均小于自己。
  • \(T_2\) 中每个点的父亲编号均大于自己。
  • \(T_1\) 的叶子集合与 \(T_2\) 的非叶子集合相同,\(T_1\) 的非叶子集合与 \(T_2\) 的叶子集合相同。

\(n \le 500\)

考虑只有 \(T_1\) 的时 \(T_1\) 的方案数,设 \(f_{i, j}\) 表示考虑前 \(i\) 个点、还剩 \(j\) 个点可以接儿子的方案数,转移就是:

  • \(i\) 是叶子:\(f_{i, j} = f_{i - 1, j} \times j + f_{i - 1, j + 1} \times (j + 1)\)
  • \(i\) 不是叶子:\(f_{i, j} = f_{i - 1, j} \times j + f_{i - 1, j - 1} \times (j - 1)\)

将第二维的值记为 \(b_{0 \sim n}\) ,规定 \(b_0 = b_n = 0\) ,DP 就是在统计 \(\prod_{i = 1}^{n - 1} b_i\) ,转移就是:

  • \(i\) 是叶子:\(b_i \in \{ b_{i - 1}, b_{i - 1} - 1 \}\)
  • \(i\) 不是叶子:\(b_i \in \{ b_{i - 1}, b_{i - 1} + 1 \}\)

因此考虑 DP of DP,设 \(f_{i, j, k}\) 表示考虑到第 \(i\) 个点、\(T_1\)\(b_i = j\)\(T_2\)\(b_{i + 1} = k\) 的方案数,转移就直接枚举 \(j, k\) 各自的增减方式即可,时间复杂度 \(O(n^3)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int dx[] = {-1, -1, 0, 0, 0, 0, 1, 1};
const int dy[] = {-1, 0, -1, 0, 0, 1, 0, 1};
const int N = 5e2 + 7;

ll f[2][N][N];

int n, Mod;

signed main() {
    scanf("%d%d", &n, &Mod);
    f[0][0][0] = 1;

    for (int i = 1; i <= n; ++i) {
        memset(f[i & 1], 0, sizeof(f[i & 1]));

        for (int x = 0; x <= i; ++x)
            for (int y = 0; y <= n - i; ++y) {
                for (int j = 0; j < 8; ++j) {
                    int nx = x + dx[j], ny = y + dy[j];

                    if (nx >= 0 && ny >= 0)
                        f[i & 1][x][y] += f[~i & 1][nx][ny] * (i == 1 ? 1 : nx) * y;
                }

                f[i & 1][x][y] %= Mod;
            }

        if (i > 1)
            printf("%d\n", (f[~i & 1][1][0] + f[~i & 1][1][1]) % Mod);
    }

    return 0;
}
posted @ 2025-07-25 21:27  wshcl  阅读(22)  评论(0)    收藏  举报