1.背包问题
经典背包
分组背包
P4158 [SCOI2009] 粉刷匠
有N条木板,每条木板有 M 个格子。每个格子要被刷成红色或蓝色。 每次粉刷只能选择一条木板上一段连续的格子,然后涂上一种颜色。 每个格子最多只能被粉刷一次。 一共只能粉刷 T 次,问最多能正确粉刷多少格子?(1<=N,M<=50, T<=2500)
注意这个题是一个格子只能涂一次,很简单,分别处理每个木板,然后对每个木板跑一个分组背包即可.
多重背包
优化:二进制拆分,单调队列
完全背包
混合背包
有依赖背包
AcWing 10. 有依赖的背包问题
有 N 个物品和一个容量是 V 的背包。
物品之间具有依赖关系,且依赖关系组成一棵树的形状。如果选择一个物品,则必须选择它的父节点。
每件物品的编号是 i,体积是 vi,价值是 wi,依赖的父节点编号是 pi。物品的下标范围是 1…N。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。输出最大价值。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define P pair<int,int>
const double eps = 1e-8;
const ll INF = 1e18;
const ll N = 1e2 + 10;
vector<vector<int> >e(N);
int n, m, v[N], w[N], dp[N][N];
void dfs(int x) {
for (int i : e[x]) {
dfs(i);
for (int j = m; j >= v[x]; --j) {
for (int k = 0; k <= j - v[x]; ++k) {
dp[x][j] = max(dp[x][j], dp[x][j - k] + dp[i][k]);
}
}
}
for (int i = v[x]; i <= m; ++i) dp[x][i] += w[x];
}
void solve() {
int fa, root;
cin >> n >> m;
for (int i = 1; i <= n; ++i) {
cin >> v[i] >> w[i] >> fa;
if (fa == -1) root = i;
else e[fa].push_back(i);
}
dfs(root);
cout << dp[root][m];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int _t = 1;
//cin>>_t;
while (_t--) {
solve();
}
return 0;
}
二维费用背包
AcWing 8. 二维费用的背包问题
有 N 件物品和一个容量是 V 的背包,背包能承受的最大重量是 M。
每件物品只能用一次。体积是 vi,重量是 mi,价值是 wi。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。输出最大价值。
求方案数
求具体方案
回退背包
2022icpc济南C DFS Order 2
给定一棵 n 个节点的树。考虑dfs序,求出第 i 个点,在第 k 次被访问的方案数是多少。
定义dp[u][k]表示u点,第k次被访问到的方案数,也就是我们最后的输出。
确定一共有多少种不同的dfs序。我们定义 tot[u] 为节点u有多少个儿子。这些儿子具有 tot[u]! 个排列可能,并且每个儿子自身又有一定的排列方案,所以我们乘起来就可以了。
考虑,在点u被访问后,v距离u的位置。定义距离为 i 的方案数为 g[i]。我们枚举v的位置前面有多少个。我们需要除v剩下的儿子可以凑出 k。我们定义 f[i][k] 为除了v这个儿子里面选了i个点,size 的总和为 k 的方案数。贡献为
f
[
i
]
[
k
]
∗
i
!
∗
(
m
−
i
−
1
)
!
f[i][k]*i!*(m-i-1)!
f[i][k]∗i!∗(m−i−1)!。我们需要对u的全部儿子跑一个背包,也就是记录下全部儿子的,选了i个点,size 的总和为 k 的方案数。现在,我们删除一个v的贡献,怎么加的贡献,就怎么减回来。
int dfs1(int u,int fa) {
int res = 1;
tot[u] = 0;
siz[u] = 1;
for (int v : g[u]) {
if (v == fa)continue;
res *= dfs1(v, u); res %= mod;
tot[u]++;
siz[u] += siz[v];
}
res *= fac[tot[u]]; res %= mod;
return res;
}
void dfs(int u, int fa) {
vector<vector<int>>f(n + 10, vector<int>(n + 10, 0));
f[0][0] = 1;//f[i][siz] 选取i个节点,大小为siz的方案数
//计算全部儿子的背包
for (int v : g[u]) {
if (v == fa)continue;
for (int i = tot[u]; i>=1; i--) {
for (int sz = siz[u]; sz>=siz[v]; sz--) {
f[i][sz] += f[i - 1][sz - siz[v]];
f[i][sz] %= mod;
}
}
}
// 计算除了v之外的儿子的背包
for (int v : g[u]) {
if (v == fa)continue;
//回退背包
for (int i = 1; i <= tot[u]; i++) {
for (int sz = siz[v]; sz <= siz[u]; sz++) {
f[i][sz] -= f[i - 1][sz - siz[v]];
f[i][sz] = (f[i][sz] + mod) % mod;
}
}
vector<int>g(n + 10, 0);
for (int i = 0; i <= tot[u] - 1; i++) {
for (int k = 0; k <= siz[u]; k++) {
g[k + 1] += (fac[i] * fac[tot[u] - 1 - i])%mod * f[i][k];
g[k + 1] %= mod;
}
}
//转移dp[v]
for (int i = 1; i <= n; i++) {
for (int k = 1; k <= n; k++) {
if (i + k <= n) {
dp[v][i + k] += dp[u][i] * g[k];
dp[v][i + k] %= mod;
}
}
}
//再加回来
for (int i = tot[u]; i >= 1; i--) {
for (int sz = siz[u]; sz >= siz[v]; sz--) {
f[i][sz] += f[i - 1][sz - siz[v]];
f[i][sz] %= mod;
}
}
}
//不要忘记dfs
for (int v : g[u]) {
if (v == fa)continue;
dfs(v, u);
}
}
void slove() {
fac[0] = 1;
for (int i = 1; i < N; i++)fac[i] = (i * fac[i - 1]) % mod;
cin >> n;
for (int i = 1; i <= n - 1; i++) {
int u, v; cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
dp[1][1] = dfs1(1, 0);
dfs(1, 0);
for (int i = 1; i <= n; i++) {
int sum = 0;
for (int k = 1; k <= n; k++)sum += dp[i][k];
int invsum = inv(sum);
for (int k = 1; k <= n; k++) {
cout << (dp[i][k] * dp[1][1]) % mod * invsum % mod << " ";
}
cout << endl;
}
}
2.线性dp
最长上升子序列
P1020 [NOIP1999 普及组] 导弹拦截
最长不升子序列
贪心+二分
记 f[i] 表示对于所有长度为 i 的单调不升子序列,它的最后一项的大小的最大值,若不存在则 f[i]=0。f[i] 单调不增,找到尽可能大的 x 满足 h[i] ≤ f[x],考虑二分。
Dilworth 定理:把序列分成不上升子序列的最少个数,等于序列的最长上升子序列长度
最长公共子序列
3.区间dp
P1775 石子合并(弱化版)
设有
N
(
N
≤
300
)
N(N \le 300)
N(N≤300) 堆石子排成一排,其编号为
1
,
2
,
3
,
⋯
,
N
1,2,3,\cdots,N
1,2,3,⋯,N。每堆石子有一定的质量
m
i
(
m
i
≤
1000
)
m_i\ (m_i \le 1000)
mi (mi≤1000)。现在要将这
N
N
N 堆石子合并成为一堆。每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻。合并时由于选择的顺序不同,合并的总代价也不相同。试找出一种合理的方法,使总的代价最小,并输出最小代价。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
ll dp[310][310],a[310],w[310][310];
void solve() {
int n;
cin >> n;
memset(dp, 0x3f, sizeof(dp));
for (int i = 1; i <= n; ++i) {
cin >> a[i];
dp[i][i] = 0;
}
for (int i = 1; i <= n; ++i) {
for (int j = i; j <= n; ++j) {
if (i == j) w[i][j] = a[i];
else w[i][j] = w[i][j-1] + a[j];
}
}
for (int i = 2; i <= n; ++i) {
for (int j = i-1; j > 0; --j) {
for (int k = j; k < i; ++k) {
dp[j][i] = min(dp[j][i], dp[j][k] + dp[k+1][i] + w[j][i]);
}
}
}
cout << dp[1][n];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int _t = 1;
//cin>>_t;
while (_t--) {
solve();
}
return 0;
}
CF1572C Paint
有一个长为 n 的字符串为涂色目标,不同字母表示不同颜色,一开始均没有颜色,每次可以选择连续的一段染成同一种颜色,颜色会覆盖。问最少染色次数
f(l,r)表示把[l,r]染好颜色的最少次数。如果s[l]=s[r],那么我们在染好 l 的同时不如顺便把 r 染上颜色,所以 f ( l , r ) = min { f ( l + 1 , r ) , f ( l , r − 1 ) } f(l,r) = \min\{f(l+1,r),f(l, r - 1)\} f(l,r)=min{f(l+1,r),f(l,r−1)};如果 s [ l ] ≠ s [ r ] s[l] \ne s[r] s[l]=s[r],可以枚举中转节点, f ( l , r ) = min l ≤ k < r { f ( l , k ) + f ( k + 1 , r ) } f(l,r) = \min\limits_{l \le k < r}\{f(l,k) + f(k + 1,r)\} f(l,r)=l≤k<rmin{f(l,k)+f(k+1,r)}。
P3736 [HAOI2016] 字符合并
有一个长度为 n 的01串,每次可以将相邻的 k 个数字合并,得到一个新的数字,并获得一定分数 。输入将给出
2
k
2^k
2k 条信息,表示所有可能的合并情况下的合并得到的数字
c
i
c_i
ci ,以及得到的分数
w
i
w_i
wi 。求出你能获得的最大分数。
c
i
c_i
ci为 0 或 1。
设 f(l,r,sta) 表示把[l,r][变为状态sta获得的最大分数。考虑状态转移起点 f ( i , i , a i ) = 0 f(i,i,a_i) = 0 f(i,i,ai)=0,其他的初始化为 − ∞ -\infty −∞.转移的时候,按照区间长度是否模 k−1余 1 来讨论。如果不余1, 从最后一位开始往前每隔k-1位枚举 f [ l , r , s t a ] = f [ l , d , s t a > > 1 ] + f [ d + 1 , r , s t a & 1 ] f[l,r,sta] = f[l,d,sta >> 1] + f[d+1,r,sta\ \&\ 1] f[l,r,sta]=f[l,d,sta>>1]+f[d+1,r,sta & 1]。即枚举最后一位是从那一部分转移过来的. 到达不了的状态肯定还是 − ∞ -\infty −∞,到达得了但是位数小于 k 位无法合并的话,其状态会被更新为 0.
二维
4.状压dp
P1433 吃奶酪
房间里放着
n
n
n 块奶酪。一只小老鼠要把它们都吃掉,问至少要跑多少距离?老鼠一开始在
(
0
,
0
)
(0,0)
(0,0) 点处。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
struct node {
double x, y;
}p[16];
int n;
double dis[16][16],dp[1<<16][16];
double dist(double x1, double y1, double x2, double y2) {
return sqrt(pow(x1 - x2, 2) + pow(y1 - y2, 2));
}
void solve() {
cin >> n;
memset(dp, 0x43, sizeof(dp));
p[0].x = 0;
p[0].y = 0;
for (int i = 1; i <= n; ++i) {
cin >> p[i].x >> p[i].y;
for (int j = 0; j < i; ++j) dis[i][j] = dis[j][i] = dist(p[i].x, p[i].y, p[j].x, p[j].y);
}
dp[1][0] = 0;
for (int i = 1; i < (1 << (n + 1)); ++i) {
for (int j = 0; j <= n; ++j) {
if ((i & (1 << j))==0 ) continue;
for (int k = 0; k <= n; ++k) {
if (((i^(1<<j)) & (1 << k)) == 0) continue;
dp[i][j] = min(dp[i][j], dp[i ^ (1 << j)][k] + dis[k][j]);
}
}
}
double ans = 0x3f3f3f3f;
for (int i = 0; i <= n; ++i) {
ans = min(ans, dp[(1 << (n + 1))-1][i]);
}
printf("%.2f", ans);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int _t = 1;
//cin>>_t;
while (_t--) {
solve();
}
return 0;
}
P1896 [SCOI2005] 互不侵犯
在
N
×
N
N \times N
N×N 的棋盘里面放
K
K
K 个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共
8
8
8 个格子。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define P pair<int,int>
const ll N = 2e2 + 10;
ll n,k, dp[10][100][100],mp[100],cnt[100],num=0;
int count(int x) {
int res = 0;
for (int i = 1; i <= n; ++i) {
if (x & (1 << (i - 1))) res++;
}
return res;
}
bool ok(int x) {
if (x & (x << 1) || x & (x >> 1)) return 0;
else return 1;
}
void init() {
for (int i = 0; i < (1 << n); ++i) {
if (ok(i)) {
mp[++num] = i;
cnt[num] = count(i);
}
}
}
bool judge(int x, int y) {
if (x & y || x & (y << 1) || x & (y >> 1)) return 0;
else return 1;
}
void solve() {
cin >> n>>k;
init();
for (int i = 1; i <=num; ++i) {
dp[1][i][cnt[i]] = 1;
}
for (int i = 2; i <= n; ++i) {
for (int j = 1; j <=num; ++j) {
for (int l = 1; l <=num; ++l) {
if (!judge(mp[j], mp[l])) continue;
for (int r = cnt[j]; r <= k; ++r) {
dp[i][j][r] += dp[i - 1][l][r - cnt[j]];
}
}
}
}
ll ans = 0;
for (int i = 1; i <= num; ++i) {
ans += dp[n][i][k];
}
cout << ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int _t = 1;
//cin>>_t;
while (_t--) {
solve();
}
return 0;
}
P3226 [HNOI2012] 集合选数
求集合 { 1 , 2 , … , n }的子集个数,且需要满足:若 x 在子集中,则 2x 和 3x 不在子集中。
写出如下矩阵
1 3 9 27…
2 6 18 54…
4 12 36 108…
发现最多有11列,最多17 行。我们在其中选取一些数,相邻的不能选择。然后就可以状压求方案数了,但是5没有出现,同样5的倍数也没有出现,7也如此。应该记录哪些数字出现过,没出现过就作为矩阵的第一个元素,最后把若干个矩阵的方案相乘。
限制条件是否已满足
2023icpc网络赛(一)I Pa?sWorD
求可能的密码的总数,密码长度为n,是数字、大写字母、小写字母的组合。数字、大写字母、小写字母至少出现一次。相邻的位置的值不能相同。
给一个长度为n的字符串s
1、s[i]是?,位置 i 可能是数字、大写字母、小写字母
2、s[i]是小写字母,位置 i 可能是s[i]或者s[i]的大写
3、其他情况位置 i 就是s[i]
dp一维位置,二维当前位置的值,三维状态(是否有数字/大写字母/小写字母),状态用3位二进制表示,第2位大写字母,第1位小写字母,第0位数字
优化枚举t的时间:优化s[i]==?,发现转移时可以提前把前一个位置的相同状态的情况 预先加起来,再减去f[i-1][j][k]就是答案
枚举子集
P5911 [POI2004] PRZ
n 个人要过桥,桥有承重量 W。所以这只队伍过桥时只能分批过,当一组全部过去时,下一组才能接着过。一个队伍的过桥时时间应该算走得最慢的那一个,我们想知道如何分批过桥能使总时间最少
首先,dp[i]为状态为 i 下,这些队员过桥最少要用的时间,再维护一下每个状态 i 的总重量 W 以及总时间 T (指一次过桥的重量和时间,不管这个状态能否过桥)。接着,顺序枚举状态 i ,并枚举 j (j ∈ i),将 i 分为状态 j 和状态 i ⊕ j,意思就是状态 j 的一次过桥时间 + 状态 i 的最优过桥时间,注意状态 j 的总重量要小于题目给定的 w , d p [ i ] = min ( d p [ i ] , T [ j ] + d p [ i ⊕ j ] ) ( W [ j ] ≤ w ) dp[i] = \min (dp[i],T[j]+dp[i \oplus j] )(W[j] \le w) dp[i]=min(dp[i],T[j]+dp[i⊕j])(W[j]≤w)
for (int j = i; j; j = i & (j - 1))//3^n枚举子集
轮廓线
5.数位dp
P2602 [ZJOI2010] 数字计数
给定两个正整数
a
a
a 和
b
b
b,求在
[
a
,
b
]
[a,b]
[a,b] 中的所有整数中,每个数码(digit)各出现了多少次。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define P pair<int,int>
const ll N = 2e2 + 10;
ll a, b,cnta[15],cntb[15],dp[15],ten[15],num[15];
void init() {
ten[0] = 1;
for (int i = 1; i < 15; ++i) {
dp[i] = i * ten[i - 1];
ten[i] = 10 * ten[i - 1];
}
}
void sol(ll x, ll* cnt) {
int len = 0;
while (x) {
num[++len] = x % 10;
x /= 10;
}
for (int i = len; i >0; --i) {
for (int j = 0; j < 10; ++j) cnt[j] += dp[i - 1] * num[i];
for (int j = 0; j < num[i]; ++j) cnt[j] += ten[i - 1];
ll num2 = 0;
for (int j = i-1; j>0 ; --j) num2 = num2 * 10 + num[j];
cnt[num[i]] += num2 + 1;
cnt[0] -= ten[i - 1];
}
}
void solve() {
cin >> a >> b;
init();
sol(a-1, cnta);
sol(b, cntb);
for (int i = 0; i < 10; ++i) {
cout << cntb[i] - cnta[i] << " ";
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int _t = 1;
//cin>>_t;
while (_t--) {
solve();
}
return 0;
}
记忆化搜索
AcWing1085 不要62
[L,R]间,不出现“62”和“4”的数的个数。
#include<bits/stdc++.h>
using namespace std;
const int N = 25;
int f[N][N][2], a[N];
int A, B;
int dp(int pos, int k, int lim){//位置,上一位数,是否是当前位最大数
if(pos == -1) return 1;
int& v = f[pos][k][lim];
if(v != -1) return v;//记忆化
int ans = 0, up = lim ? a[pos] : 9;
for(int i = 0; i <= up; i++){
if(i == 4) continue;
if(k == 6 && i == 2) continue;
ans += dp(pos - 1, i, i == up && lim);
}
return f[pos][k][lim] = ans;
}
int solve(int x){
int sz = 0;
while(x){
a[sz++] = x % 10;
x /= 10;
}
memset(f, -1, sizeof f);
return dp(sz - 1, 0, 1);
}
int main(){
ios::sync_with_stdio(0);
while(cin >> A >> B, A){
cout << solve(B) - solve(A - 1) << endl;
}
}
hdu3652 B-number
B数的定义:能被13整除且本身包含字符串"13"的数。例如:130和2613是B数,但是143和2639不是B数。你的任务是计算1到n之间有多少个数是B数。
#include<bits/stdc++.h>
using namespace std;
const int N = 35, mod = 13;
int f[N][15][15][2], a[N];
int dp(int pos, int sum, int k, int flag, int lim){//sum除13余数,flag是否包含13
if(pos == -1) return sum == 0 && flag;
int& v = f[pos][sum][k][flag];
if(!lim && v != -1) return v;
int ans = 0, up = lim ? a[pos] : 9;
for(int i = 0; i <= up; i++){
if(k == 1 && i == 3) ans += dp(pos - 1, (sum * 10 + i) % mod, i, 1, i == up && lim);
else ans += dp(pos - 1, (sum * 10 + i) % mod, i, flag, i == up && lim);
}
if(!lim) v = ans;
return ans;
}
int main(){
int x;
while(cin >> x){
int sz = 0;
while(x){
a[sz++] = x % 10;
x /= 10;
}
memset(f, -1, sizeof f);
printf("%d\n", dp(sz - 1, 0, 0, 0, 1));
}
return 0;
}
P4124 [CQOI2016] 手机号码
[L,R]间,11位无前导零不同时出现 8, 4 且必须包含三个相邻且相同的数。
#include<bits/stdc++.h>
using namespace std;
const int N = 15;
typedef long long ll;
ll f[N][N][N][2][2][2][2][2];
int a[N];
ll dp(int pos, int flag_4, int flag_8, int consecutive, int k, int cnt, int lead_zero, int lim){//flag_4/8是否包含4/8,consecutive是否包含三个相邻且相同的数,cnt相邻且相同的数数量,lead_zero前一位是否是前导零
if(flag_4 && flag_8) return 0;
if(pos == -1) return consecutive;
ll& v = f[pos][k][cnt][flag_4][flag_8][consecutive][lead_zero][lim];
if(v != -1) return v;
ll ans = 0;
int up = lim ? a[pos] : 9;
for(int i = 0; i <= up; i++){
int t = !lead_zero || i != 0;
if(i == k && !lead_zero) t = cnt + 1;
ans += dp(pos - 1, flag_4 || i == 4, flag_8 || i == 8, consecutive || (t >= 3), i, t, i == 0 && lead_zero, i == up && lim);
}
return v = ans;
}
ll solve(ll x){
int sz = 0;
while(x){
a[sz++] = x % 10;
x /= 10;
}
memset(f, -1, sizeof f);
return dp(sz - 1, 0, 0, 0, 0, 0, 1, 1);
}
int main(){
ll L, R;
scanf("%lld%lld", &L, &R);
printf("%lld\n", solve(R) - solve(L - 1));
return 0;
}
6.树形dp
P1352 没有上司的舞会
某大学有
n
n
n 个职员,编号为
1
…
n
1\ldots n
1…n。
他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。
现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数
r
i
r_i
ri,但是呢,如果某个职员的直接上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了。
所以,请你编程计算,邀请哪些职员可以使快乐指数最大,求最大的快乐指数。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 6e3 + 10;
int n, r[N], dp[N][2],fa[N];
vector<vector<int> > p(N);
void dfs(int root) {
dp[root][0] = dp[root][1] = 0;
for (int i : p[root]) {
dfs(i);
dp[root][1] += dp[i][0];
dp[root][0] += max(dp[i][1],dp[i][0]);
}
dp[root][1] += r[root];
}
void solve() {
cin >> n;
int root;
for (int i = 1; i <= n; ++i) cin >> r[i];
for (int i = 1; i <= n-1; ++i) {
int l, k;
cin >> l >> k;
p[k].push_back(l);
fa[l] = k;
if (fa[k] == 0) root = k;
}
dfs(root);
cout << max(dp[root][0], dp[root][1]);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int _t = 1;
//cin>>_t;
while (_t--) {
solve();
}
return 0;
}
树上背包
P1273 有线电视网
某收费有线电视网计划转播一场重要的足球比赛。他们的转播网和用户终端构成一棵树状结构,这棵树的根结点位于足球比赛的现场,树叶为各个用户终端,其他中转站为该树的内部节点。
从转播站到转播站以及从转播站到所有用户终端的信号传输费用都是已知的,一场转播的总费用等于传输信号的费用总和。
现在每个用户都准备了一笔费用想观看这场精彩的足球比赛,有线电视网有权决定给哪些用户提供信号而不给哪些用户提供信号。
写一个程序找出一个方案使得有线电视网在不亏本的情况下使观看转播的用户尽可能多。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define P pair<int,int>
const double eps = 1e-8;
const ll N = 3e3+ 10;
struct edge {
int u, v, w;
};
vector<vector<edge> >e(N);
int n, m,cost[N],dp[N][N];
int dfs(int x) {
if (x > n - m) {
dp[x][1] = cost[x];
return 1;
}
int sum = 0;
for (auto i : e[x]) {
int cnt=dfs(i.v);
sum += cnt;
for (int j = sum; j >= 1; --j) {
for (int k = 1; k <= min(j,cnt); ++k) {
dp[x][j] = max(dp[x][j], dp[x][j - k] + dp[i.v][k] - i.w);
}
}
}
return sum;
}
void solve() {
cin >> n >> m;
memset(dp, 0xc0,sizeof(dp));
for (int i = 1; i <= n - m; ++i) {
int k;
cin >> k;
for (int j = 1; j <= k; ++j) {
int v, w;
cin >> v >> w;
e[i].push_back({ i, v, w });
}
dp[i][0] = 0;
}
for (int i = n - m + 1; i <= n; ++i) cin >> cost[i];
dfs(1);
for (int i = m; i >= 0; --i) {
if (dp[1][i] >= 0) {
cout << i;
return;
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int _t = 1;
//cin >> _t;
while (_t--) {
solve();
}
return 0;
}
换根dp
P3478 [POI2008] STA-Station
给定一个
n
n
n 个点的树,请求出一个结点,使得以这个结点为根时,所有结点的深度之和最大。一个结点的深度之定义为该节点到根的简单路径上边的数量。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define P pair<int,int>
const double eps = 1e-8;
const ll mod = 1e9 + 7;
const ll INF = 1e18;
const ll N = 1e6 + 10;
vector<vector<int> >e(N);
ll siz[N], dp[N], deep[N], ans, maxid = 1;
void dfs(int x, int fa) {
deep[x] = deep[fa] + 1;
siz[x] = 1;
for (int i : e[x]) {
if (i == fa) continue;
dfs(i, x);
dp[x] += dp[i] + deep[i];
siz[x] += siz[i];
}
}
void dfs2(int x, int fa) {
for (int i : e[x]) {
if (i == fa) continue;
dp[i] = dp[x] + siz[x] - 2 * siz[i];//dp[x]-dp[i]+siz[x]-siz[i]+dp[i]-siz[i]
siz[i] = siz[x];
if (dp[i] > ans) {
ans = dp[i];
maxid = i;
}
dfs2(i, x);
}
}
inline void solve() {
int n;
cin >> n;
for (int i = 1; i < n; ++i) {
int a, b;
cin >> a >> b;
e[a].push_back(b);
e[b].push_back(a);
}
deep[0] = -1;
dfs(1, 0);
ans = dp[1];
dfs2(1, 0);
cout << maxid;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int _t = 1;
while (_t--) {
solve();
}
return 0;
}
虚树优化
P2495 [SDOI2011] 消耗战
在一场战争中,战场由
n
n
n 个岛屿和
n
−
1
n-1
n−1 个桥梁组成,保证每两个岛屿间有且仅有一条路径可达。现在,我军已经侦查到敌军的总部在编号为
1
1
1 的岛屿,而且他们已经没有足够多的能源维系战斗,我军胜利在望。已知在其他
k
k
k 个岛屿上有丰富能源,为了防止敌军获取能源,我军的任务是炸毁一些桥梁,使得敌军不能到达任何能源丰富的岛屿。由于不同桥梁的材质和结构不同,所以炸毁不同的桥梁有不同的代价,我军希望在满足目标的同时使得总代价最小。
侦查部门还发现,敌军有一台神秘机器。即使我军切断所有能源之后,他们也可以用那台机器。机器产生的效果不仅仅会修复所有我军炸毁的桥梁,而且会重新随机资源分布(但可以保证的是,资源不会分布到
1
1
1 号岛屿上)。不过侦查部门还发现了这台机器只能够使用
m
m
m 次,所以我们只需要把每次任务完成即可。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define P pair<int,int>
const double eps = 1e-8;
const ll mod = 1e9 + 7;
const ll INF = 1e18;
const ll N = 5e5 + 10;
struct edge {
ll u, v, w;
};
vector<vector<edge> >e(N);
vector<vector<int> >p(N);
ll dep[N], dp[N], f[N][20], dfn[N], idx, a[N], st[N], tag[N];
void dfs(int x, int fa) {
dep[x] = dep[fa] + 1;
f[x][0] = fa;
dfn[x] = ++idx;
for (int i = 1; i < 20; ++i) f[x][i] = f[f[x][i - 1]][i - 1];
for (auto i : e[x]) {
if (i.v == fa) continue;
dp[i.v] = min(dp[x], i.w);//找到根所有边的最小值
dfs(i.v, x);
}
}
int lca(int x, int y) {
if (dep[x] < dep[y]) swap(x, y);
for (int i = 19; i >= 0; --i) {
if (dep[f[x][i]] >= dep[y]) x = f[x][i];
}
if (x == y) return x;
for (int i = 19; i >= 0; --i) {
if (f[x][i] != f[y][i]) x = f[x][i], y = f[y][i];
}
return f[x][0];
}
bool cmp(int x, int y) { return dfn[x] < dfn[y]; }
ll dfs2(int x) {
ll res = 0, sum = 0;
for (int i : p[x]) {
sum += dfs2(i);
}
if (tag[x]) res = dp[x];
else res = min(dp[x], sum);
tag[x] = 0;//清空
p[x].clear();
return res;
}
void solve() {
int n, m;
cin >> n;
for (int i = 1; i < n; ++i) {
ll u, v, w;
cin >> u >> v >> w;
e[u].push_back({ u,v,w });
e[v].push_back({ v,u,w });
}
dp[1] = INF;
dfs(1, 0);
cin >> m;
for (int i = 1; i <= m; ++i) {
int k;
cin >> k;
for (int j = 1; j <= k; ++j) cin >> a[j], tag[a[j]] = 1;
sort(a + 1, a + k + 1, cmp);//按dfs序排序
int top = 0;
st[++top] = a[1];
//虚树只保留有用的点(在这道题里是标记点和lca)
for (int j = 2; j <= k; ++j) {//建立虚树,用栈维护最右链,最右链上的边还没有加入虚树,随时会有某个lca插到最右链中
int d = lca(a[j], st[top]);
while (dfn[d] < dfn[st[top - 1]]) p[st[top - 1]].push_back(st[top]), top--;
if (d != st[top]) {//lca在st[top]和st[top−1]之间
p[d].push_back(st[top]);
if (d == st[top - 1]) top--;
else st[top] = d;
}
st[++top] = a[j];
}
for (int j = 1; j < top; ++j) p[st[j]].push_back(st[j + 1]);//最右链加入虚树
cout << dfs2(st[1]) << '\n';
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int _t = 1;
//_t = read();
while (_t--) {
solve();
}
return 0;
}
AcWing1077 皇宫看守
皇宫各个宫殿的分布,呈一棵树的形状,宫殿可视为树中结点,两个宫殿之间如果存在道路直接相连,则该道路视为树中的一条边。已知,在一个宫殿镇守的守卫不仅能够观察到本宫殿的状况,还能观察到与该宫殿直接存在道路相连的其他宫殿的状况。
大内保卫森严,三步一岗,五步一哨,每个宫殿都要有人全天候看守,在不同的宫殿安排看守所需的费用不同。可是陆小凤手上的经费不足,无论如何也没法在每个宫殿都安置留守侍卫。帮助陆小凤布置侍卫,在看守全部宫殿的前提下,使得花费的经费最少。
f[i][0/1/2]以结点 i 为根节点的子树,状态为 j 的方案。被父节点观察(0);被子节点观察(1);被自己来观察(2)
P3177 [HAOI2015] 树上染色
有一棵点数为 N 的树,树边有边权。给你一个在 0 ∼ N 之内的正整数 K,你要在这棵树中选择 K 个点,将其染成黑色,并将其他的 N − K 个点染成白色。将所有点染色后,你会获得黑点两两之间的距离加上白点两两之间距离的和的收益。问收益最大值是多少。
用 f[i][j] 表示在 i 的子树中选择 j 个黑点所能得到的最大收益(只考虑在 i 子树中的边的贡献)。转移时要用子树内的黑(白)点 × 子树外的黑(白)点个数 × 边权,将贡献加到 f 值上。直接枚举子树中有多少黑点,然后把 ( u , v ) 这条边的贡献加上。
int dfs(int u, int fa){
f[u][0] = f[u][1] = 0;
sz[u] = 1;
for(int i = h[u]; i != -1; i = ne[i]){
int son = e[i];
if(son == fa) continue;
sz[u] += dfs(son, u);
for(int j = min(sz[u], v); j >= 0; j--){
for(int k = 0; k <= min(sz[son], j); k++){
ll t = w[i] * (k * (v - k) + (sz[son] - k) * (n - v - (sz[son] - k)));
f[u][j] = max(f[u][j], f[u][j - k] + f[son][k] + t);
//如果f[u][j-k]为负无穷,说明仅凭左边的子树无法凑出j-k个黑点
}
}
}
return sz[u];
}
牛客多校6A Tree
给定一棵树,每个点选择黑、白有对应的代价,定义一棵树的收益为所有黑白点对间路径边权最大值的和,问如何选择每个点的颜色使得收益-代价最大?
注意到点对的价值等于路径上最大边权值,不难想到 Kruskal 重构树——即根据边权从小到大的顺序枚举边,考虑加入这条边前后该边两端点所在连通块合并的贡献。不难发现,一条边合并的贡献仅与两侧点集中黑点和白点数目有关。f[i][j]表示 i 集合(重构树子树)有 j 个白点的最大价值(该点集内已经有的价值减去该点集内翻转代价),初始时 f S u , a u = 0 f_{S_u,a_u}=0 fSu,au=0而 f S u , a u ⊕ 1 = − a i f_{S_u,a_u \oplus 1}=-a_i fSu,au⊕1=−ai ,即颜色不同时要减去翻转代价。
#include <bits/stdc++.h>
using namespace std;
const int N = 3000;
const long long inf = 0x3f3f3f3f3f3f3f3fll;
int father[N + 5], a[N + 5], siz[N + 5];
long long cost[N + 5];
int getfather(int x){
return father[x] == x ? x : father[x] = getfather(father[x]);
}
vector<long long> f[N + 5];
void merge(int u, int v, long long w){
u = getfather(u);
v = getfather(v);
if (u == v) return;
if (siz[u] > siz[v]) swap(u, v);
int n = f[u].size() - 1, m = f[v].size() - 1;
vector<long long> temp(n + m + 1);
for (auto &x : temp) x = -inf;
for (int i = 0; i <= n + m; i++)
for (int j = 0; j <= n; j++)
if (i - j <= m && i >= j){
int k = i - j;
int cnt = j * (m - k) + (n - j) * k;
temp[i] = max(temp[i], f[u][j] + f[v][k] + cnt * w);
}
f[v] = temp;
f[u].clear();
father[u] = v;
siz[v] += siz[u];
}
struct edge{
int u, v;
long long w;
bool operator<(const edge &b) const{
return w < b.w;
}
edge(int _u, int _v, long long _w) : u(_u), v(_v), w(_w) {}
};
int main(){
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++){
scanf("%d", &a[i]);
father[i] = i;
siz[i] = 1;
f[i].resize(2);
}
for (int i = 1; i <= n; i++){
scanf("%lld", &cost[i]);
f[i][a[i] ^ 1] = -cost[i];
}
vector<edge> q;
for (int i = 1, u, v, w; i < n; i++){
scanf("%d%d%d", &u, &v, &w);
q.emplace_back(u, v, w);
}
sort(q.begin(), q.end());
for (auto [u, v, w] : q) merge(u, v, w);
auto ans = f[getfather(1)];
printf("%lld", *max_element(ans.begin(), ans.end()));
return 0;
}
2021icpc沈阳L
给一个2 ∗ n 个点的完全图,从这个图里面抽出2 ∗ n − 1 条边,这些边形成一颗树,现在问你剩下的图里面点进行完美匹配有多少种方案?完美匹配方案可以理解为,对于一个2 ∗ n 个结点的图,找一个包含n条边的边集,由于每条边有两个端点,如果这个边集包含的点有2 ∗ n 个,则是完全匹配(边集内任意两边没有公共端点)。
先求不删边的情况下有多少种,之后减去边集里包含了被删除的边的个数。不删时,共有
C
(
2
n
,
n
)
∗
n
!
/
2
n
C(2n, n) * n! / 2^{n}
C(2n,n)∗n!/2n 种。C(2n, n)表示先选 n 条边的一个端点,n! 表示剩下的 n 个点与之前选的 n 个点的匹配方式,除掉的是重复计算的,对于边(x,y) 和(y,x) 是相同的,而一共有 n 条边(可以理解为每条边交换还是不交换)。
对于选择了 x 条来自被删除了的树上的边,剩下的n − x条边的选法有
C
(
2
n
−
2
x
,
n
−
x
)
∗
(
n
−
x
)
!
/
2
n
−
x
C(2n-2x,n-x) * (n - x)! / 2 ^ {n-x}
C(2n−2x,n−x)∗(n−x)!/2n−x ,从树上选 x 条满足条件的有多少种选法可以利用树上背包求解(树形dp),之后根据容斥原理减掉即可。树上背包:dp[i][j][0/1]表示以第 i 个点为根的子树,选择了 j 条符合条件的边,且 i 节点所在的边选不选的方案数。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 4005;
const ll mod = 998244353;
int n, x, y;
vector<int> vt[maxn];
ll num, cnt, dp[maxn][maxn][5], sum[maxn], tmp[maxn][2], fac[maxn], inv[maxn];//fac[i]是i!,inv[i]是2^(-i)的逆元
ll qim(ll a, ll b){
a %= mod;
ll res = 1;
while(b){
if(b & 1) res = a * res % mod;
a = a * a % mod;
b >>= 1;
}
return res % mod;
}
void init(){
fac[0] = 1;
for(int i = 1; i < maxn; ++ i) fac[i] = 1ll * fac[i-1] * i % mod;
inv[0] = 1;
inv[1] = qim(2,mod-2);
for(int i = 2; i < maxn; ++ i) inv[i] = 1ll * inv[i-1] * inv[1] % mod;
}
ll C(int a, int b){
if (a < b) return 0;
return 1ll * fac[a] * qim(fac[b],mod-2) % mod * qim(fac[a-b],mod-2)%mod;
}
void dfs(int u, int fa){
dp[u][0][0] = 1;
sum[u] = 1;
int p;
for(int k = 0; k < vt[u].size(); ++k){
p = vt[u][k];
if(p == fa) continue;
dfs(p, u);
memset(tmp, 0, sizeof(tmp));//辅助数组
for(int i = 0; i <= sum[u] / 2; ++i){
for(int j = 0; j <= sum[p] / 2; ++j){//枚举从当前这个子树中取多少个
tmp[i + j][0] = (tmp[i + j][0] + dp[u][i][0] * (dp[p][j][0] + dp[p][j][1]) % mod) % mod;
tmp[i + j][1] = (tmp[i + j][1] + dp[u][i][1] * (dp[p][j][0] + dp[p][j][1]) % mod) % mod;
tmp[i + j + 1][1] = (tmp[i + j + 1][1] + dp[u][i][0] * dp[p][j][0] % mod) % mod;
}
}
for(int i = 0; i <= sum[u] / 2 + sum[p] / 2 + 1; ++i){
dp[u][i][0] = tmp[i][0];
dp[u][i][1] = tmp[i][1];
}
sum[u] += sum[p];
}
}
int main(){
scanf("%d", &n);
for(int i = 1; i <= 2 * n - 1; ++i){
scanf("%d%d", &x, &y);
vt[x].push_back(y);
vt[y].push_back(x);
}
init();
dfs(1, 0);
//ll ans = ((C(2 * n, n) * fac[n]) % mod * inv[n]) % mod;//计算不删情况下的种类数,i从0开始循环的话就不需要计算了
ll ans = 0;
//i从0开始,i=0时,即从树中选了0条边,就是全部的取法
for(int i = 0; i <= n; ++i){
cnt = (dp[1][i][0] + dp[1][i][1]) % mod;
num = ((C(2 * n - 2 * i, n - i) * fac[n - i]) % mod * inv[n - i]) % mod;
num = (cnt * num) % mod;
if(i&1) ans = ((ans - num) % mod + mod) % mod;
else ans = (ans + num) % mod;
}
printf("%lld\n", ans % mod);
return 0;
}
7.概率期望dp
P2473 [SCOI2008] 奖励关
你正在玩你最喜欢的电子游戏,并且刚刚进入一个奖励关。在这个奖励关里,系统将依次随机抛出
k
k
k 次宝物,每次你都可以选择吃或者不吃(必须在抛出下一个宝物之前做出选择,且现在决定不吃的宝物以后也不能再吃)。
宝物一共有
n
n
n 种,系统每次抛出这
n
n
n 种宝物的概率都相同且相互独立。也就是说,即使前
(
k
−
1
)
(k-1)
(k−1) 次系统都抛出宝物
1
1
1(这种情况是有可能出现的,尽管概率非常小),第
k
k
k 次抛出各个宝物的概率依然均为
1
n
\frac 1 n
n1。
获取第
i
i
i 种宝物将得到
p
i
p_i
pi 分,但并不是每种宝物都是可以随意获取的。第
i
i
i 种宝物有一个前提宝物集合
s
i
s_i
si。只有当
s
i
s_i
si 中所有宝物都至少吃过一次,才能吃第
i
i
i 种宝物(如果系统抛出了一个目前不能吃的宝物,相当于白白的损失了一次机会)。注意,
p
i
p_i
pi 可以是负数,但如果它是很多高分宝物的前提,损失短期利益而吃掉这个负分宝物将获得更大的长期利益。
假设你采取最优策略,平均情况你一共能在奖励关得到多少分值?
概率是顺推,而期望需要逆推。
#include<bits/stdc++.h>
using namespace std;
int n, k, maxs, p[20], need[20];//need[i]是吃第i个宝物所需要吃的宝物
double dp[110][1 << 15];
int main(){
cin >> k >> n;
for (int i = 1; i <= n; i++){
cin >> p[i];
int u;
while (cin>>u && u) need[i] |= (1 << (u - 1));
}
for (int i = k; i >= 1; i--) {//逆推
for (int s = 0; s < (1 << n); s++) {//循环枚举状态
for (int j = 1; j <= n; j++) {
if ((need[j] & s) == need[j]) dp[i][s] += max(dp[i + 1][s], dp[i + 1][s | (1 << (j - 1))] + p[j]);//若能选宝物
else dp[i][s] += dp[i + 1][s];//若不能选宝物
}
dp[i][s] /= n;//每个宝物被选中的概率为1/n
}
}
cout << setiosflags(ios::fixed) << setprecision(6) << dp[1][0];
return 0;
}
8.状态机dp
AcWing 1053. 修复DNA
生物学家终于发明了修复DNA的技术,能够将包含各种遗传疾病的DNA片段进行修复。
为了简单起见,DNA看作是一个由’A’, ‘G’ , ‘C’ , ‘T’构成的字符串。
修复技术就是通过改变字符串中的一些字符,从而消除字符串中包含的致病片段。
例如,我们可以通过改变两个字符,将DNA片段”AAGCAG”变为”AGGCAC”,从而使得DNA片段中不再包含致病片段”AAG”,”AGC”,”CAG”,以达到修复该DNA片段的目的。
需注意,被修复的DNA片段中,仍然只能包含字符’A’, ‘G’ , ‘C’ , ‘T’。
请你帮助生物学家修复给定的DNA片段,并且修复过程中改变的字符数量要尽可能的少。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define P pair<int,int>
const double eps = 1e-8;
const ll mod = 1e9 + 7;
const ll INF = 1e18;
const ll N = 1e3 + 10;
int trie[N][4], cnt[N], fail[N], id, dp[N][N];
inline ll read() {
ll res = 0;
char ch = getchar();
while (ch < '0' || ch>'9') { ch = getchar(); }
while (ch >= '0' && ch <= '9') { res = (res << 3) + (res << 1) + (ch ^ 48); ch = getchar(); }
return res;
}
inline int get(char ch) {
if (ch == 'A') return 0;
else if (ch == 'G') return 1;
else if (ch == 'C') return 2;
else return 3;
}
void insert(string s) {
int p = 0;
int len = s.size();
for (int i = 0; i < len; ++i) {
int temp = get(s[i]);
if (trie[p][temp] == 0) {
trie[p][temp] = ++id;
}
p = trie[p][temp];
}
cnt[p]=1;
}
void creatfail() {
queue<int> q;
int p = 0;
for (int i = 0; i <4; ++i) {
if (trie[p][i]) q.push(trie[p][i]);
}
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = 0; i <4; ++i) {
if (trie[u][i]) {
fail[trie[u][i]] = trie[fail[u]][i];
cnt[trie[u][i]] |= cnt[fail[trie[u][i]]];
q.push(trie[u][i]);
}
else trie[u][i] = trie[fail[u]][i];
}
}
}
void solve() {
int n, num = 0;
while (cin >> n && n) {
num++;
memset(dp, 0x3f, sizeof(dp));
memset(cnt, 0, sizeof(cnt));
memset(fail, 0, sizeof(fail));
memset(trie, 0, sizeof(trie));
id = 0;
string s;
for (int i = 1; i <= n; ++i) {
cin >> s;
insert(s);//插入AC自动机
}
creatfail();
cin >> s;
int len = s.length();
s = " " + s;
dp[0][0] = 0;
for (int i = 0; i < len; ++i) {
for (int j = 0; j <= id; ++j) {
for (int k = 0; k < 4; ++k) {
int p = trie[j][k];
if (!cnt[p]) dp[i + 1][p] = min(dp[i + 1][p], dp[i][j] + (get(s[i+1]) != k));
}
}
}
int ans = 0x3f3f3f3f;
for (int i = 0; i <= id; ++i) ans = min(ans, dp[len][i]);
if (ans == 0x3f3f3f3f) ans = -1;
cout << "Case "<<num<<": "<<ans << '\n';
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int _t = 1;
//_t = read();
while (_t--) {
solve();
}
return 0;
}
9.dp套dp
P4590 [TJOI2018] 游园会
小豆参加了 NOI 的游园会,会场上每完成一个项目就会获得一个奖章,奖章只会是
N
\texttt{N}
N、
O
\texttt{O}
O、
I
\texttt{I}
I 的字样。在会场上他收集到了
K
K
K 个奖章组成的串。兑奖规则是奖章串和兑奖串的最长公共子序列长度为小豆最后奖励的等级。现在已知兑奖串长度为
N
N
N,并且在兑奖串上不会出现连续三个奖章为
NOI
\texttt{NOI}
NOI,即奖章中不会出现子串
NOI
\texttt{NOI}
NOI。现在小豆想知道各个奖励等级会对应多少个不同的合法兑奖串。
#include<bits/stdc++.h>
using namespace std;
const int mod = 1e9 + 7;
const int N = 1e3+10;
int n, m, f[2][N], a[N], b[N], nxt[3][3] = { {1,0,0},{1,2,0},{1,0,3} }, dp[2][32769][3], ans[N], now, sta[32769][3];
//对于同一个兑奖串,这个数列是单调递增的,而且每次递增不会超过1,因此考虑状压它们的差分数组。
//f表示当前兑奖串与前i位奖章串匹配后的lcs长度;dp(i,j,k)表示前i个字符中,状态压缩后的lcs串表示为j,与“NOI”的匹配到了第k位的方案数。
char ch[N];
void init(){
for (int s = 0; s < (1 << m); s++){
for (int k = 0; k < m; k++)f[0][k + 1] = f[0][k] + ((s >> k) & 1);
for (int j = 0; j <= 2; j++){ //枚举下一个字符
for (int k = 1; k <= m; k++){
f[1][k] = max(f[1][k - 1], f[0][k]);
if (b[k] == j)f[1][k] = max(f[1][k], f[0][k - 1] + 1); //最长上升子序列
}
int t = 0;
for (int i = 1; i <= m; i++)if (f[1][i] > f[1][i - 1])t |= (1 << (i - 1));
sta[s][j] = t; //状态转移
}
}
}
int main(){
cin >> n >> m;
cin >> (ch + 1);
for (int i = 1; i <= m; i++){
if (ch[i] == 'N')b[i] = 0;
if (ch[i] == 'O')b[i] = 1;
if (ch[i] == 'I')b[i] = 2;
}
init();
dp[0][0][0] = 1;
for (int i = 1; i <= n; i++){
memset(dp[now ^ 1], 0, sizeof(dp[now ^ 1]));
for (int s = 0; s < (1 << m); s++){
for (int j = 0; j <= 2; j++){ //匹配到第几位
for (int k = 0; k <= 2; k++){ //下一个字符
if (j == 2 && k == 2)continue;
int cnt = nxt[j][k];
dp[now ^ 1][sta[s][k]][cnt] += dp[now][s][j];
dp[now ^ 1][sta[s][k]][cnt] %= mod;
}
}
}
now ^= 1;
}
for (int s = 0; s < (1 << m); s++){
for (int j = 0; j <= 2; j++){
int cnt = 0, tmp = s; //最长子序列长度是01串中1的个数,因为这是dp数组中的最大值
while (tmp) { if (tmp % 2 == 1)cnt++; tmp /= 2; }
ans[cnt] += dp[now][s][j];
ans[cnt] %= mod;
}
}
for (int i = 0; i <= m; i++)cout << ans[i] << '\n';
return 0;
}
10.插头dp
P5056 【模板】插头 DP
给出
n
×
m
n\times m
n×m 的方格,有些格子不能铺线,其它格子必须铺,形成一个闭合回路。问有多少种铺法?
由于求的是一个回路,因此每个格子只有一条入边和一条出边,而每个格子只有四条边,从四条边中选两条边。记录连通性,对于分界线来说,有一条边上去就一定有一条边下来,因此所有出来的边必然是两两配对的。所有的路径本质上就是一个括号序列,对于每条路径,左边出来的边看成左括号,记为1,右边出来的边看成右括号,记为2,没有边的位置记为0,那么我们就可以用一个只有0,1,2的三进制状态将分界线上所有路径表示出来。
设
f
i
,
j
,
S
f_{i,j,S}
fi,j,S表示当前递推到(i,j)且递推完的格子和没有递推的格子之间的分界线的状态是 S的哈密顿回路的方案数。
O
(
S
n
3
)
O(Sn^3)
O(Sn3)
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define P pair<ll,ll>
const double eps = 1e-8;
const ll mod = 1e9 + 7;
const ll INF = 1e18;
const ll N = 5e4;//状态数
const ll M = N * 2 + 7;
ll n, m, endx, endy, a[20][20], cnt[2], cur, q[2][N], h[2][M], v[2][M];//q[i]插入的第i个数的位置,h[i]i位置上的值
ll find(ll x) {//手写哈希
ll t = x % M;
while (h[cur][t] != -1 && h[cur][t] != x) {
if (++t == M) t = 0;
}
return t;
}
void insert(ll x, ll y) {
ll t = find(x);
if (h[cur][t] == -1) {
h[cur][t] = x;
v[cur][t] = y;
q[cur][++cnt[cur]] = t;
}
else v[cur][t] += y;
}
ll Get(ll x, ll k) {// 求状态x下第k个格子状态,四进制数字
return x >> k * 2 & 3;
}
ll Set(ll x, ll k) {//构造四进制的第x位为k的数
return k * (1 << x * 2);
}
inline void solve() {
cin >> n >> m;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
char ch;
cin >> ch;
if (ch == '.') {
a[i][j] = 1;
endx = i, endy = j;
}
}
}
ll ans = 0;
memset(h, -1, sizeof(h));
insert(0, 1);
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= cnt[cur]; ++j) h[cur][q[cur][j]] <<= 2;
for (int j = 1; j <= m; ++j) {
cur ^= 1;
cnt[cur] = 0;
memset(h[cur], -1, sizeof(h[cur]));
for (int k = 1; k <= cnt[cur ^ 1]; ++k) {
ll sta = h[cur ^ 1][q[cur ^ 1][k]], w = v[cur ^ 1][q[cur ^ 1][k]];
ll x = Get(sta, j - 1), y = Get(sta, j);//左边的状态是x,上边的状态是y
if (!a[i][j]) {//障碍物,原本就是0,不变
if (!x && !y) insert(sta, w);
}
else if (!x && !y) {//需要一条出边和一条入边,但x,y都是0,因此z和w一个是1一个是2
if (a[i + 1][j] && a[i][j + 1]) insert(sta + Set(j - 1, 1) + Set(j, 2), w);
}
else if (!x && y) {//x是0,y不是0,说明y进来了一条边,就必须还有一条出去的边,因此分别枚举一下从z和w出去的情况
if (a[i][j + 1]) insert(sta, w);
if (a[i + 1][j]) insert(sta + Set(j - 1, y) - Set(j, y), w);
}
else if (x && !y) {//同上
if (a[i][j + 1]) insert(sta - Set(j - 1, x) + Set(j, x), w);
if (a[i + 1][j]) insert(sta, w);
}
//x,y都是1,则x和y都是各自所在路径的左端点,他们的右边都分别有一个右端点,此时x和y连上,那么右边的两个右端点中靠左的那个右端点就应该变成左端点
//因此需要将x原本所在的路径的右端点的状态改为1。而递推完后已经没有边了,将这两个位置改为0。
else if (x == 1 && y == 1) {
for (int u = j + 1, s = 1;; u++) {
ll z = Get(sta, u);
if (z == 1) s++;
else if (z == 2) {
if (--s == 0) {
insert(sta - Set(j - 1, x) - Set(j, y) - Set(u, 1), w);
break;
}
}
}
}
else if (x == 2 && y == 2) {//同上
for (int u = j - 2, s = 1;; u--) {
ll z = Get(sta, u);
if (z == 2) s++;
else if (z == 1) {
if (--s == 0) {
insert(sta - Set(j - 1, x) - Set(j, y) + Set(u, 1), w);
break;
}
}
}
}
//合并前x所在路径的左端点恰好就是合并后路径的左端点,合并前y所在路径的右端点恰好就是合并后路径的右端点,因此不变。已经没有边了,将这两个位置改为0。
else if (x == 2 && y == 1) insert(sta - Set(j - 1, x) - Set(j, y), w);
//x是1y是2,要让路径不相交x和y必属于同一条路径,此时x和y连上就会形成回路,所以这一步只会发生在最后一个合法格子中,统计一下总方案数。
else if (i == endx && j == endy) ans += w;
}
}
}
cout << ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int _t = 1;
//_t = read();
while (_t--) {
solve();
}
return 0;
}
11.悬线法dp
P4147 玉蟾宫
这片土地被分成
N
×
M
N\times M
N×M 个格子,每个格子里写着 ‘R’ 或者 ‘F’,R 代表这块土地被赐予了 rainbow,F 代表这块土地被赐予了 freda。
现在 freda 要在这里卖萌。。。它要找一块矩形土地,要求这片土地都标着 ‘F’ 并且面积最大。
但是 rainbow 和 freda 的 OI 水平都弱爆了,找不出这块土地,而蓝兔也想看 freda 卖萌(她显然是不会编程的……),所以它们决定,如果你找到的土地面积为
S
S
S,它们每人给你
S
S
S 两银子。
最大子矩阵,单调栈
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define P pair<int,int>
const double eps = 1e-8;
const ll mod = 1e9 + 7;
const ll INF = 1e18;
const ll N = 1e3 + 10;
int n, m, a[N][N], L[N][N], R[N][N], h[N][N]; //h往上延申的最大长度,L/R往左/右走最远到哪
void solve() {
cin >> n >> m;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
char ch;
cin >> ch;
if (ch == 'F') a[i][j] = h[i][j] = 1;
else a[i][j] = 0;
L[i][j] = R[i][j] = j;//等于自己
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (a[i][j] && a[i][j - 1]) L[i][j] = L[i][j - 1];
}
for (int j = m; j >= 1; j--) {
if (a[i][j] && a[i][j + 1]) R[i][j] = R[i][j + 1];
}
}
int ans = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (a[i][j] && a[i - 1][j]) {
h[i][j] = h[i - 1][j] + 1;//h延申
L[i][j] = max(L[i][j], L[i - 1][j]);//L、R向内收缩
R[i][j] = min(R[i][j], R[i - 1][j]);
}
if (a[i][j]) ans = max(ans, h[i][j] * (R[i][j] - L[i][j] + 1));
}
}
cout << ans * 3 << endl;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int _t = 1;
//_t = read();
while (_t--) {
solve();
}
return 0;
}
12.dp优化
前缀和,倍增,树状数组,线段树优化
单调队列优化
2022icpc南京B Ropeway
可以看作一个数轴上有n个点,选取一些点,相邻点要满足距离不大于k,且某些点必须选取,每个点使用的价格不同,接下去有q次临时修改一个点的价格,再求其对应的最小价格
如果不做修改的话,这是一个典型的DP问题,每一个点i 可以由前面的j点转移而来,j满足(i-k<=j<i),可以用单调队列来优化。
如果修改的话,对p的改变,会影响p之后所有的点的f[i]。由于只有一个方向的递推,所以影响会一直持续到最后,但是如果从反方向再来一遍递推,也就是预处理出来从n+1向0修建到i所需要的最小代价g[i]。那么,在修建当前点的前提下,总的修建代价就可以得到VALUE=g[i]+f[i]+w[i]。这样子做的好处在于,对于每一处的修改,它的影响在于自身的w,以及(i,i+k]范围内的f[j]。
#include<bits/stdc++.h>
#define INF ((long long) 1e18)
using namespace std;
const int N=5e5+10;
typedef pair<long long,int>pii;
int n,k,t,Q,p,v,q[N];
long long a[N],f[N],g[N],h[N];
char must[N];
void dp(long long f[]){
deque<pii>dq;
f[0]=0;
dq.push_back(pii(0,0));
for(int i=1;i<=n+1;i++){
while(dq.front().second<i-k)dq.pop_front();
f[i]=dq.front().first+a[i];
if(must[i]=='1')dq.clear();//必须选
while(!dq.empty()&&dq.back().first>=f[i])dq.pop_back();
dq.push_back(pii(f[i],i));
}
}
long long dp2(int x,int y){//把a[x]改成y
int tmp=a[x];
a[x]=y;
long long res=INF;
deque<pii> dq;
for (int i = k; i > 0; i--)
if (x - i >= 0) {
if (must[x - i] == '1') dq.clear();
while (!dq.empty() && dq.back().first >= f[x - i]) dq.pop_back();
dq.push_back(pii(f[x - i], x - i));
}
for (int i = x; i < x + k&& i <= n + 1; i++) {
while (dq.front().second < i - k) dq.pop_front();
h[i] = dq.front().first + a[i];
// 计算每个中间点的答案
res = min(res, h[i] + g[i]);
if (must[i] == '1') dq.clear();
while (!dq.empty() && dq.back().first >= h[i]) dq.pop_back();
dq.push_back(pii(h[i], i));
}
a[x]=tmp;
return res;
}
void solve(){
cin>>n>>k;
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
a[n+1]=0;
scanf("%s", must + 1);
dp(f);
reverse(a+1,a+n+1);
reverse(must+1,must+n+1);
dp(g);
reverse(a+1,a+n+1);
reverse(must+1,must+n+1);
reverse(g,g+n+2);
for(int i=1;i<=n;i++)g[i]-=a[i];//g表示i点建立下,后面所有点的最小支出
cin>>Q;
while (Q--) {
int x, y; scanf("%d%d", &x, &y);
printf("%lld\n", dp2(x, y));
}
}
int main(){
cin>>t;
while(t--){
solve();
}
}
斜率优化
四边形不等式优化
13.技巧&题型
任意区间不超过k
设状态结尾为i的所有后缀中最大值
P2592 [ZJOI2008] 生日聚会
今天是hidadz小朋友的生日,她邀请了许多朋友来参加她的生日party。 hidadz带着朋友们来到花园中,打算坐成一排玩游戏。为了游戏不至于无聊,就座的方案应满足如下条件:
对于任意连续的一段,男孩与女孩的数目之差不超过k。
很快,小朋友便找到了一种方案坐了下来开始游戏。hidadz的好朋友Susie发现,这样的就座方案其实是很多的,所以大家很快就找到了一种,那么到底有多少种呢?热爱数学的hidadz和她的朋友们开始思考这个问题……
假设参加party的人中共有n个男孩与m个女孩,你是否能解答Susie和hidadz的疑问呢?由于这个数目可能很多,他们只想知道这个数目除以12345678的余数。
dp[i][j][k][h]表示放了i个男生,j个女生,所有后缀中,男生减女生的差最大为k,女生减男生的差最大为h的方案数
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define P pair<int,int>
const double eps = 1e-8;
const ll mod = 12345678;
const ll INF = 1e18;
const ll N = 1e5 + 10;
ll dp[155][155][22][22];
inline void solve() {
int n, m, k;
cin >> n >> m >> k;
dp[0][0][0][0] = 1;
for (int i = 0; i <= n; ++i) {
for (int j = 0; j <= m; ++j) {
for (int h = 0; h <= k; ++h) {
for (int r = 0; r <= k; ++r) {
dp[i + 1][j][h + 1][max(r - 1, 0)] = (dp[i + 1][j][h + 1][max(r - 1, 0)] + dp[i][j][h][r]) % mod;
dp[i][j + 1][max(h - 1, 0)][r + 1] = (dp[i][j + 1][max(h - 1, 0)][r + 1] + dp[i][j][h][r]) % mod;
}
}
}
}
ll ans = 0;
for (int i = 0; i <= k; ++i) {
for (int j = 0; j <= k; ++j) {
ans = (ans + dp[n][m][i][j]) % mod;
}
}
cout << ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int _t = 1;
//_t = read();
while (_t--) {
solve();
}
return 0;
}
牛客多校4J Qu’est-ce Que C’est?
给定长度为 n 的数列,要求每个数都在 [−m,m] 范围,且任意长度大于等于 2 的区间和都大于等于 0,问方案数。
划分成两相等集合
2021icpc上海I Steadily Growing Steam
n 个卡牌,有价值 vi 和点数 ti ,你需要从中选出两组卡牌使得其点数和相等,然后最大化这两组的价值和。另外你可以选择不超过 k 个不同的卡牌,使得其 ti 变成 2ti。
状态可以定义为 d[i][j][w],表示前 i 张卡牌,翻转恰好 j 次,两个集合体积差为 w 时的最大价值和
任意m区间至少2个
hdu7296 Klee likes making friends
给一个长度为n的数组a和一个正整数m,问在a中每连续m个数中最少拿出两个,求拿出来的数和的最小值
我们发现如果只要求拿出一个,就是一个很明显的DP题,只要DP最后一个值拿出来是多少就行。那这题我们能不能也这么DP?我们令f[i][j]表示最后两个值下标是i(倒一),j,那么倒数第三个值下标
k
,
k
∈
[
i
−
m
,
j
−
1
]
k,k\in[i-m,j-1]
k,k∈[i−m,j−1] ,所以我们需要遍历k区间内的所有值取
m
i
n
(
f
[
j
]
[
k
]
)
min(f[j][k])
min(f[j][k]) 。很明显会T,我们考虑优化这个取最小值的过程,
我们发现这个取最小值就是,对最后一个值下标为j时,倒数第二个值下标从j - 1到 i - m,就是 j 左边 (j-1)-(i-m)+1 个的DP取最小值。我们根据这个性质,可以考虑再来一个DP表达式mn[i][j]表示对于下标i为最后一个取的值,倒数第二个取值在i左边j个的范围内,f函数的最小值。为了方便同时维护 f 和 mn 我们可以改 f[i][j] 表示对于最后一个取值下标为i,倒数第二个取值下标为i - j。这样两个函数都是表达一个向左的范围。所以
f
[
i
]
[
j
]
=
a
[
i
]
+
m
n
[
i
−
j
]
[
j
−
m
]
,
m
n
[
i
]
[
j
]
=
m
i
n
(
m
n
[
i
]
[
j
−
1
]
,
f
[
i
]
[
j
]
)
f[i][j]=a[i]+mn[i-j][j-m],mn[i][j]=min(mn[i][j-1],f[i][j])
f[i][j]=a[i]+mn[i−j][j−m],mn[i][j]=min(mn[i][j−1],f[i][j]) ,可以看到这两个函数可以一起维护。还有这题空间开的比较小,我们开的数组只能是m*m的,对于 i 位,我们每次可以对 m 取模。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 2e4 + 10, M = 2e3 + 10, INF = 0x3f3f3f3f;
const ll mod = 1e9 + 7;
int a[N], f[M][M], mn[M][M];
void solve(){
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 0; i <= m; i++) for (int j = 0; j <= m; j++) f[i][j] = mn[i][j] = INF;
for (int i = 2; i <= m; i++) {//跑第一个m
for (int j = 1; j < i; j++) {
f[i % m][j] = a[i] + a[i - j];
mn[i % m][j] = min(mn[i % m][j - 1], f[i % m][j]);
}
}
for (int i = m + 1; i <= n; i++) {
for (int j = 1; j < m; j++) {
f[i % m][j] = a[i] + mn[(i - j) % m][m - j];
mn[i % m][j] = min(mn[i % m][j - 1], f[i % m][j]);
}
}
int res = INF;
for (int i = n - m + 1; i <= n; i++) {
for (int j = 1; j < m && i - j > n - m; j++) {
res = min(res, f[i % m][j]);
}
}
cout << res << '\n';
}
int main(){
ios::sync_with_stdio(false);
cout.tie(0);
cin.tie(0);
int _ = 1;
cin >> _;
while (_--) {
solve();
}
return 0;
}
矩形dp
从三个矩形转移
hdu7369 Diagonal Fancy
给定一个 n*m 矩阵,求其中有多少个连续子正方形,满足该子正方形内每个从左上到右下的对角线内部的数字相同,而不同对角线之间数字互不相同。
定义 dp[i][j] 为以 为左上角的满足第一个条件(即同一个对角线内数字相同)的最大子正方形的边长。我们可以采用经典的 dp 方式,从 dp[i+1][j]、dp[i][j+1] 、dp[i+1][j+1] 转移到 dp[i][j],转移式如下:
d
p
[
i
]
[
j
]
=
{
1
a
[
i
]
[
j
]
≠
a
[
i
+
1
]
[
j
+
1
]
m
i
n
(
d
p
[
i
+
1
]
[
j
]
,
d
p
[
i
]
[
j
+
1
]
,
d
p
[
i
+
1
]
[
j
+
1
]
)
+
1
a
[
i
]
[
j
]
=
a
[
i
+
1
]
[
j
+
1
]
dp[i][j]=\begin{cases}1&a[i][j]\neq a[i+1][j+1]\\min(dp[i+1][j],dp[i][j+1],dp[i+1][j+1])+1&a[i][j]= a[i+1][j+1]\end{cases}
dp[i][j]={1min(dp[i+1][j],dp[i][j+1],dp[i+1][j+1])+1a[i][j]=a[i+1][j+1]a[i][j]=a[i+1][j+1]
在上述转移式的基础上,我们继续增加对第二个条件的保证。现在我们令 dp[i][j] 表示以 (i,j) 为左上角的同时满足两个条件的最大的子正方形的边长。假设
a
[
i
]
[
j
]
≠
a
[
i
+
1
]
[
j
+
1
]
a[i][j]\neq a[i+1][j+1]
a[i][j]=a[i+1][j+1],我们继续考虑类似的转移方式。设k=min(dp[i+1][j],dp[i][j+1],dp[i+1][j+1]),从而 dp[i+1][j] 保证了第 l~r-2 条对角线互不相同; dp[i][j+1] 保证了第 l+2~r 条对角线互不相同; dp[i+1][j+1] 保证了第 l+1~r-1 条对角线互不相同。于是,我们只需要检查第 l,l+1,r-1,r 是否互不相同,若是,则dp[i][j]=k+1 ,否则d[i][j]=k 。
以段划分
最后一段是什么
牛客多校3B Auspiciousness
2n 张牌面数字分别为 1, 2, . . . , 2n 的牌叠放成牌堆,完成以下流程:
- 从牌堆顶取走一张牌
- 若牌堆为空则结束流程,否则猜测牌堆顶数字是否大于上一张取走的牌,并从牌堆顶取走一张牌
- 若猜测正确则回到上一步继续,否则结束流程
依照「上一张牌不超过 n 则猜测牌堆顶大于上一张牌,否则猜测小于」的策略猜测,问对于所有可能的 (2n)! 种牌堆叠放顺序,总共能取走的牌的数量之和模 m 的结果
注意到,最后的排列一定是一段小数 [1,n] ,一段大数 [n+1,2n] 交替。考虑设 f[i][j][0/1] 表示填了 i 个小数, j 个大数,最后一段是小数或大数段的合法方案数(注意这里没包括最后一个错误的牌),有转移方程:
if (k <= i) f[i][j][0] += f[i - k][j][1] * C(n - i + k, k);
if (k <= j) f[i][j][1] += f[i][j - k][0] * C(n - j + k, k);
其中 k 为最后一段数字的个数。统计答案时,我们考虑每个位置猜对时产生的贡献。对于前 i 个小数, j 个大数猜对时的方案数量为:(f[i][j][0]+f[i][j][1])⋅(2n−i−j)!此时的所有方案,一定会在第 i+j 的位置产生一张猜对的牌的贡献。最后我们需要加上每个排列最后一张错误的牌,共 (2n)!−(f[n][n][0]+f[n][n][1]) 种方案会产生一张错误的牌。
作业
给一个长度为 N 的序列,序列中每个元素初始为无色,每个元素有一个目标颜色(黑或白),你有 K 次机会选择一个区间染色,注意颜色会覆盖。问最多能让多少个元素满足目标颜色。
注意染到最后一定是黑白相间的。而最多形成2k-1个黑白相间的段,并且个数小于2k-1的黑白相间的段都能被染出。dp[i][j][0/1]表示染到第 i 个,已经有了 j 段,最后一段是白/黑色,最多的满足目标颜色的元素个数。
字符串匹配
设计状态为匹配到字符串哪一位
BD202317 石碑文
现在小度在石碑上找到了一些文字,这些文字包含N个英文字符,这些文字依稀可以辨认出来,另一些文字难以辨认,在可以辨认出来的文字中,小度发现了他喜欢的文字“shs”,小度习惯把喜欢的事物说三遍及以上,他希望知道原始的石碑上有多少种可能性会出现三次及以上“shs”(三个“shs”不能出现重合,即“shshs”只能算出现一次“shs”),这样的碑文可能有很多,你只需要输出答案对1e9+7取模的结果即可。
dp[i][j] 字符串长度为 i 的已经匹配上"shsshsshs"中 j 个字符的方案数
void solve(){
cin >> n;
dp[0][0] = 1;
for(int i = 0; i < n; ++ i){
for(int j = 0; j < 10; ++ j){
if(j == 9){// "shsshsshs"已经完全匹配
dp[i + 1][j] = (dp[i + 1][j] + dp[i][j] * 26) % mod;
}
else if(j % 3 == 1){
dp[i + 1][j] = (dp[i + 1][j] + dp[i][j]) % mod;// "..s" -> "..ss"
dp[i + 1][j + 1] = (dp[i + 1][j + 1] + dp[i][j]) % mod; // "..s" -> "..sh"
dp[i + 1][j - 1] = (dp[i + 1][j - 1] + dp[i][j] * 24) % mod; //"..s" -> "..s*"(*代表除"sh"以外的任意字符)
}
else{// "...sh" / "...shs"
dp[i + 1][j + 1] = (dp[i + 1][j + 1] + dp[i][j]) % mod; // "..." + "s"
dp[i + 1][j / 3 * 3] = (dp[i + 1][j / 3 * 3] + dp[i][j] * 25) % mod; // 清空后缀
}
}
}
cout << dp[n][9] << '\n';
return ;
}
AcWing1052 设计密码
你现在需要设计一个密码 S,S 需要满足:S 的长度是 N;S 只包含小写英文字母;S 不包含子串 T;请问共有多少种不同的密码满足要求?
dp[i][j]表示长度为i的密码,与T匹配的最后位置为j的方案数,KMP转移状态。