动态规划专题

背包

概率/期望DP

#include<iostream>
#include<cstdio>
#include<queue>
#include<cmath>
#include<cstring>
#include<map>
#define LL long long
using namespace std;
const int N = 150, M = 20, mod = 1e9 + 7;
// LL f[M][N][M]; 
LL sum[14] = {0,31,0,100000041,0,781250051,0,882102328,0,707741534,0,745749140,0,927105416};
// LL qpow(LL a, int b)
// {
//     LL ans = 1;
//     while(b)
//     {
//         if(b & 1)
//             ans = ans * a % mod;
//         a = a * a % mod;
//         b >>= 1;
//     }
//     return ans;
// }
// void pre()
// {
//     for(int i = 1; i < 14; i += 2) f[i][0][i] = 1;
//     int n = 34 * 4 - 13;
//     for(int i = 1; i < 14; i += 2)
//     {
//         for(int j = 0; j < n; j ++)
//         {
//             for(int k = 1; k < 14; k += 2)
//             {
//                 //凑成一对
//                 LL p = 3 * k % mod * qpow(n - j, mod - 2) % mod;
//                 if(k >= 2)
//                     f[i][j + 1][k - 2] = (f[i][j + 1][k - 2] + f[i][j][k] * p % mod) % mod;
//                 f[i][j + 1][k] = (f[i][j + 1][k] + f[i][j][k] * (1 - p + mod)) % mod;
//             }
//         }
//         for(int j = 0; j < n; j ++) //先玩j轮
//             sum[i] = (sum[i] + (j + 1) * f[i][j][1] % mod * 3 % mod * qpow(n - j, mod - 2) % mod) % mod;
//     }
// }
map<string, int> ma;
int main()
{
    // pre();
    int t; cin >> t; 
    int kase = 0;
    while(t --)
    {
        string s; cin >> s;
        ma.clear();
        int n2 = 0;
        for(int i = 0; i < s.length(); i += 2)
        {
            ma[s.substr(i, 2)] ++;
            if(ma[s.substr(i, 2)] == 2) n2 ++;
        }
        cout << "Case #" << ++kase << ": " << sum[13 - n2 * 2] << endl;
    }
    return 0;
}

CF837D Round Subset

链接
找从n个数里找k个数使得k个数乘积末尾的0的个数最多。将5的幂次总和作为体积,2的幂次总和作为价值。

#include<iostream>
#include<algorithm>
#include<cstring>
#define LL long long
using namespace std;
const int N = 210, M = 6100;
int dp[N][M], f[N], t[N];
int n, k, sum[N];
LL a[N];
int main()
{	
	scanf("%d%d", &n, &k);
	for(int i = 1; i <= n; i ++) 
	{
		scanf("%lld", &a[i]);
		LL tmp = a[i];
		
		while(tmp && tmp % 5 == 0)
			f[i] ++, tmp /= 5;

		while(tmp && tmp % 2 == 0)
			t[i] ++, tmp /= 2;
		sum[i] = sum[i - 1] + f[i];
	}

	memset(dp, -0x3f, sizeof dp);
	dp[0][0] = 0;
	int ans = 0;

	for(int i = 1; i <= n; i ++)
		for(int j = min(i, k); j >= 1; j --)
			for(int l = sum[i]; l >= f[i]; l --)
				dp[j][l] = max(dp[j][l], dp[j - 1][l - f[i]] + t[i]);

	for(int i = 1; i <= sum[n]; i ++)
		ans = max(ans, min(i, dp[k][i]));
	printf("%d", ans);
	return 0;
}

单调队列优化

单调队列就是用队列来维护一定大小区间的最大值,队列的头部放置最大值,若头部不在窗口中则弹出;队尾放入元素表示窗口滑动,若放入元素比队尾元素还要大,则将原本队尾元素弹出,因为原本队尾元素不可能作为当前区间的最大值(必然比放进来的新元素要小)。
解决问题的代码只有3行:为了维护滑动窗口大小弹出队头,为了维护最大值弹出队尾,将新元素弹入队尾。
单调队列只是一个工具,更重要的是知道在什么情况下使用它:有一个大小确定的区间,这个区间会往后移动,要在这个区间里求最值。

P1886 滑动窗口 /【模板】单调队列

链接

#include<iostream>
#include<algorithm>
#include<cstring>
#define ll long long
using namespace std;
const int N=1e6+7;
int n, k, head, tail, q[N];
int a[N];
int main()
{
    scanf("%d%d", &n, &k);
    for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);

    head = 0, tail = -1;
    for(int i = 1; i <= n; i ++)
    {
    	while(head <= tail && i - q[head] + 1 > k) head ++; //维护窗口大小为k
    	while(head <= tail && a[q[tail]] >= a[i]) tail --; //将不可能为最小值的数弹出队列
    	q[++ tail] = i; //弹入当前值
    	if(i >= k)
    		printf("%d ", a[q[head]]);
    }
    puts("");

    head = 0, tail = -1;
    for(int i = 1; i <= n; i ++) 
    {
    	while(head <= tail && i - q[head] + 1 > k) head ++;
    	while(head <= tail && a[q[tail]] <= a[i]) tail --;
    	q[++ tail] = i;
    	if(i >= k)
    		printf("%d ", a[q[head]]);
    }
    return 0;
}

P3522 [POI2011]TEM-Temperature

链接
单调队列队头保存当前连续区间中的最小温度的最大值对应的天数,在此连续区间每一天的最高温度都不低于队头的最低温度,否则无法满足温度不降的要求。

因此队头的最小温度比当前天的最大温度还要大时,就将队头弹出;队尾的最小温度比当前天的最小温度还要小时,就将队尾弹出,以此保持队列的单调性。

比较难想的是计算以当前天 i i i结尾的最长连续天数。这里不能直接将队头的天数作为连续天数的第一天,队头的天数可能是在序列中间的,只是它的最小温度是当前序列的最大值而已。因此反向思考找到不在以 i i i结尾的最长连续不降序列的前一天是 p r e pre pre,则序列长度为 i − p r e i-pre ipre。至此可想到 p r e pre pre是最后一个被弹出的队头,因为它是最后一个最小温度比第 i i i天的最高温度还大的天数,因此 p r e = q [ h e a d − 1 ] pre=q[head-1] pre=q[head1]

#include<iostream>
#include<algorithm>
#include<cstring>
#define ll long long
using namespace std;
const int N=2e6 + 10;
int n, head, tail, a[N], b[N], q[N], f[N];
int main()
{
	scanf("%d", &n);
	for(int i = 1; i <= n; i ++) 
		scanf("%d%d", &a[i], &b[i]);
	head = 0, tail = -1;
	q[++ tail] = 1;
	int ans = 1;
	for(int i = 2; i <= n; i ++)
	{
		while(head <= tail && a[q[head]] > b[i]) head ++;
		if(head <= tail)
			ans = max(ans, i - q[head - 1]);
		while(head <= tail && a[q[head]] < a[i]) tail --;
		q[++ tail] = i;	
	}
	printf("%d\n", ans);
	return 0;
}

P3572 [POI2014]PTA-Little Bird

链接
朴素的状态定义和压缩:设f[i]表示跳到位置i最少的体力花费值, f [ i ] = m i n   f [ j ] + ( h [ i ] > = h [ j ] ) ,   j = i − 1 , i − 2 , . . . , i − k f[i]=min\ f[j]+(h[i] >= h[j]), \ j=i - 1,i-2,...,i-k f[i]=min f[j]+(h[i]>=h[j]), j=i1,i2,...,ik,这样复杂度为 q n 2 qn^2 qn2。但是f[i]的更新只需要在从 i − k , i − k + 1 , . . . , i − 1 i-k,i-k+1,...,i-1 ik,ik+1,...,i1中体力花费值最小(比较第一关键字)或者是当体力花费值相同时,更高的树转移过来即可。因此用单调队列维护,将花费值最小且树最高的下标放在队头,维护一个递减队列。每次用队头来更新当前的 f [ i ] f[i] f[i],再用 f [ i ] f[i] f[i]和队尾比较来弹出体力花费多或者树相对更矮的队尾。

#include<iostream>
#include<algorithm>
#include<cstring>
#define ll long long
using namespace std;
const int N=2e6 + 10;
int n, head, tail, a[N], q[N], f[N];
int main()
{
	scanf("%d", &n);
	for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);
	int t; scanf("%d", &t);
	while(t --)
	{
		int k; scanf("%d", &k);
		int last = 1, ans = 0;
		head = 0, tail = -1;
		f[1] = 0, q[++ tail] = 1;
		for(int i = 2; i <= n; i ++)
		{	
			while(head <= tail && i - q[head] > k) 
				head ++;
			f[i] = f[q[head]] + (a[i] >= a[q[head]]);
			while(head <= tail 
				&& (f[q[tail]] > f[i] ||(f[q[tail]] == f[i] && a[q[tail]] <= a[i])))
				tail --;
			q[++ tail] = i;
		}
		printf("%d\n", f[n]);
	}
	return 0;
}

P1776 宝物筛选(单调队列优化多重背包)

链接
多重背包的朴素转移为
f [ i ] [ j ] = m a x { f [ i − 1 ] [ j − k w ] + k v } ,   k = 0 , 1 , 2 , . . . , m i n ( m , W / w ) f[i][j]=max\{f[i-1][j-kw]+kv\},\ k=0,1,2,...,min(m,W/w) f[i][j]=max{f[i1][jkw]+kv}, k=0,1,2,...,min(m,W/w)复杂度为 W n m Wnm Wnm,比较难过(;′⌒`)😔。开始优化!

首先是普通的维度优化,用 g [ j ] g[j] g[j]表示 f [ i − 1 ] [ j ] f[i-1][j] f[i1][j]。就有
f [ j ] = m a x { g [ j − k w ] + k v } ,   k = 0 , 1 , 2 , . . . , m i n ( m , W / w ) f[j]=max\{g[j-kw]+kv\},\ k=0,1,2,...,min(m,W/w) f[j]=max{g[jkw]+kv}, k=0,1,2,...,min(m,W/w)从上式可以发现 f [ j ] f[j] f[j]只能从在模 w w w意义下相同的 g g g转移过来,即 f [ m o d + k ∗ w ] = m a x { g [ m o d + s w ] + s v } ,   m o d = 0 , 1 , . . . , v − 1 ,   s = 0 , 1 , . . . , m i n ( k , W / w ) ,   m o d = 0 , 1 , . . . , v − 1 f[mod+k*w]=max\{g[mod+sw]+sv\},\ mod=0,1,...,v-1,\ s=0,1,...,min(k,W/w),\ mod=0,1,...,v-1 f[mod+kw]=max{g[mod+sw]+sv}, mod=0,1,...,v1, s=0,1,...,min(k,W/w), mod=0,1,...,v1
更具体地用例子表示有:

f[mod] = g[mod]
f[mod + w] = max(g[mod + w], g[mod] + v)
f[mod + 2w] = max(g[mod + 2w], g[mod + w] + v, g[mod] + 2v)
...
f[mod + kw] = max(g[mod + kw], g[mod + (k -1) w] + v, ..., g[mod] + kv)

为了使表示更加方便,将每个 m a x max max求值的 + k v +kv +kv都提取到 m a x max max外面,里面 g g g变成减法,并且减v的系数和加w的系数相同。

f[mod] = g[mod]
f[mod + w] = max(g[mod + w] - v, g[mod]) + v
f[mod + 2w] = max(g[mod + 2w] - 2v, g[mod + w] - v, g[mod]) + 2v
...
f[mod + kw] = max(g[mod + kw] - kv, g[mod - (k -1) w] - (k - 1) v, ..., g[mod]) + kv

到这里单调队列的轮廓已经出来了,窗口大小就是宝物的数量 m m m,队头保存上式中 m a x max max括号内的最大值对应的系数,现在用 g [ m o d + q [ h e a d ] ∗ w ] g[mod + q[head] * w] g[mod+q[head]w]来更新 f [ m o d + k ∗ w ] f[mod + k * w] f[mod+kw] q [ h e a d ] q[head] q[head]保存的系数如果太小,小到 k − q [ h e a d ] > m k-q[head]>m kq[head]>m的地步,此时相当于要拿多于 m m m件第 i i i种宝物才能从状态 g [ m o d + q [ h e a d ] ∗ w ] g[mod + q[head] * w] g[mod+q[head]w]转移到 f [ m o d + k ∗ w ] f[mod + k * w] f[mod+kw]。这里的宝物数量 m m m就是单调队列的窗口大小,用这个大小限制来弹出队头,保证状态的合理转移。

#include<iostream>
#include<algorithm>
#include<cstring>
#define ll long long
using namespace std;
const int N=2e6 + 10;
int n, f[N], g[N], q[N], W;
int main()
{
	scanf("%d%d", &n, &W);
	for(int i = 1; i <= n; i ++)
	{
		int v, w, m; 
		scanf("%d%d%d", &v, &w, &m);
		memcpy(g, f, sizeof g);
		
		for(int mod = 0; mod < w; mod ++)
		{
			int head = 0, tail = -1;
			for(int k = 0; mod + k * w <= W; k ++)
			{
				//维护区间长度不大于m
				while(head <= tail && k - q[head] > m) head ++; 
				//将不可能用来更新f[mod + k * w]的倍数均弹出
				while(head <= tail && 
					g[mod + q[tail] * w] - q[tail] * v <= g[mod + k * w] - k * v)
					tail --;
				//用队头对应的g更新当前f
				if(head <= tail)
					f[mod + k * w] = g[mod + q[head] * w] + (k - q[head]) * v;
				q[++ tail] = k;
			}
		}
	}
	printf("%d", f[W]);
	return 0;
}

P4544 [USACO10NOV]Buying Feed G

链接
这道题和上一道几乎是相同的套路,甚至还比较简单,因为没有重量,不需要模数。首先将店家按照距离排序,使得拿物品的路线从起始位置直接到结束位置,而不走回头路。列出朴素的转移方程: f [ i ] [ j ] = m i n { f [ i − 1 ] [ s ] + s 2 ∗ ( x [ i ] − x [ i − 1 ] ) + ( j − s ) ∗ c [ i ] } , 0 < = j − s < = F [ i ] f[i][j] = min\{f[i-1][s]+s^{2}*(x[i]-x[i-1])+(j-s)*c[i]\},0<=j-s<=F[i] f[i][j]=min{f[i1][s]+s2(x[i]x[i1])+(js)c[i]},0<=js<=F[i]
同样地进行简单变化,使得表达式 m i n min min里面的值和 j j j无关,简单地将 j ∗ c [ i ] j*c[i] jc[i]提取出来,得到可由单调队列优化的状态转移方程: f [ i ] [ j ] = m i n { f [ i − 1 ] [ s ] + s 2 ∗ ( x [ i ] − x [ i − 1 ] ) + s ∗ c [ i ] } + j ∗ c [ i ] , 0 < = j − s < = F [ i ] f[i][j] = min\{f[i-1][s]+s^{2}*(x[i]-x[i-1])+s*c[i]\}+j*c[i],0<=j-s<=F[i] f[i][j]=min{f[i1][s]+s2(x[i]x[i1])+sc[i]}+jc[i],0<=js<=F[i],然后用 j − s < = F [ i ] j-s<=F[i] js<=F[i]的区间大小弹出队头,用 m i n min min弹出过大的队尾保持单调性,最后将 j j j入队。

#include<iostream>
#include<algorithm>
#include<cstring>
#define LL long long
using namespace std;
const int K = 1e4 + 10, N = 510;
int k, e, n, q[K];
LL f[K], g[K], INF = 1e18;
struct  Node
{
	int x, f, c;
	bool operator<(const Node & tmp){ return x < tmp.x; }

}node[N];
inline LL cal(int i, int s)
{
	return g[s] + 1ll * s * s * (node[i].x - node[i - 1].x) - 1ll * s * node[i].c; 
}
int main()
{
	scanf("%d%d%d", &k, &e, &n);
	for(int i = 1; i <= n; i ++)
		scanf("%d%d%d", &node[i].x, &node[i].f, &node[i].c);
	sort(node + 1, node + 1 + n);
	node[0].x = node[1].x;
	for(int i = 1; i <= k; i ++) f[i] = INF;
	for(int i = 1; i <= n; i ++)
	{
		memcpy(g, f, sizeof g);
		int head = 0, tail = -1;
		for(int j = 0; j <= k; j ++)
		{
			while(head <= tail && j - q[head] > node[i].f) head ++;
			while(head <= tail && cal(i, j) < cal(i, q[tail])) tail --;
			q[++ tail] = j;
			f[j] = cal(i, q[head]) + 1ll * j * node[i].c;
		}
	}
	printf("%lld", f[k] +  (e - node[n].x) * k * k);
	return 0;
}

计数DP

Rikka with Nash Equilibrium

链接
在行和列中均是最大值的只能有一个,因为矩阵数字是1~n * m,因此最大的必然是n * m, 而次大的必须受到最大的约束,因此必然在n * m所在的位置相同的行或相同列,否则n * m - 1也会变成其对应行列的峰值…因此从大到小确定数字放置的方案数,因为小数字只能放置在大数字放置位置对应的行和列,因此考虑状态和大数字已经放置的行和列的数量有关。
f[k][i][j]为当前放置到数字k,且已放置的行数量为i,列数量为j的方案数

接下来考虑状态转移:每放置一个数字,会产生3种情况,行数量增加,列数量增加,或者行列数量都不增加(不可能行列数量都增加是因为当前数字不能放置在之前大数字都未放置的行和列)。
若用 f [ k ] [ i ] [ j f[k][i][j f[k][i][j]更新 f [ k ] [ . ] [ . ] f[k][.][.] f[k][.][.],行增加的情况为:接下来的数字只能放置在还未放置的行的任意位置,共 n − i n - i ni行,同时该列又有大数字,有更大数字共j列,因此 f [ k + 1 ] [ i + 1 ] [ j ] + = f [ k ] [ i ] [ j ] ∗ ( n − i ) ∗ j f[k + 1][i + 1][j] += f[k][i][j] * (n - i ) * j f[k+1][i+1][j]+=f[k][i][j](ni)j。其他情况同理。
note: 减少无用状态的转移。

#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 100;
int f[2][N][N];
int n, m, mod;
int main()
{
	int t;
	scanf("%d", &t);
	while(t--)
	{
		scanf("%d %d %d", &n, &m, &mod);
		for(int i = 0; i <= n; i ++)
			for(int j = 0; j <= m; j ++)
				f[0][i][j] = f[1][i][j] = 0;
		f[1][1][1] = n * m % mod;
		
		for(int k = 1, now = 0; k <= n * m; k ++)
		{
			memset(f[(k + 1) & 1], 0, sizeof f[(k + 1) & 1]);
			for(int i = 1; i <= n; i ++)
			{
				for(int j = 1; j <= m; j ++)
				{
					//用当前状态更新之后状态
					int s = k & 1;
					if(f[s][i][j])
					{
						if(i < n)
							f[s ^ 1][i + 1][j] = (f[s ^ 1][i + 1][j] + 1ll * f[s][i][j] * (n - i) % mod * j) % mod;
						if(j < m) 
							f[s ^ 1][i][j + 1] = (f[s ^ 1][i][j + 1] + 1ll * f[s][i][j] * (m - j) % mod * i) % mod;
						f[s ^ 1][i][j] = (f[s ^ 1][i][j] + 1ll * f[s][i][j] * (i * j - k) % mod) % mod; 
					}
				}
			}
		}
		printf("%d\n", f[(n * m) & 1][n][m]);
	}
	return 0;
}

序列计数 HDU - 6348

链接
按照最常见的思路,将答案作为 d p dp dp的结果,为了方便大小比较,要记录序列末端的数字,将 d p [ i ] [ j ] dp[i][j] dp[i][j]设成长度为 i i i j j j作为序列尾部的序列数量。当 a k < a j a_{k} < a_{j} ak<aj d p [ i ] [ j ] = ∑ k = 1 j − 1 d p [ i − 1 ] [ j ] dp[i][j] = \sum_{k = 1}^{j - 1}dp[i-1][j] dp[i][j]=k=1j1dp[i1][j]。这样的时间复杂度为 n 3 n^{3} n3。考虑简化。比较难的部分是每次需要判断 a k < a j a_{k} < a_{j} ak<aj,可以用树状数组避免这种判断, 即将数字作 为下标去求前缀和。

进行新的状态设计:
a n s [ i ] ans[i] ans[i]表示长度为 i i i的上升序列的数量。

d p [ n o w ] [ j ] dp[now][j] dp[now][j]表示以数字 a [ j ] a[j] a[j]为结尾的长度为 i i i的上升序列的个数, d p [ n o w   x o r   1 ] [ j ] dp[now\ xor \ 1][j] dp[now xor 1][j]表示以数字 a [ j ] a[j] a[j]为结尾的长度为 i − 1 i - 1 i1的上升序列的个数。用 n o w now now标记的好处是不用考虑逆序或者顺序枚举 j j j,必然是从 i − 1 i - 1 i1的状态更新来。

接下来可用 d p [ j ] dp[j] dp[j]更新 a n s [ i ] ans[i] ans[i],利用树状数组的有序性在时间 l g n lgn lgn中计算以 1 , 2 , 3 , . . . , a [ j ] − 1 1,2,3,...,a[j] - 1 1,2,3,...,a[j]1结尾的长度为 i − 1 i-1 i1的子序列数量和 s u m [ j ] sum[j] sum[j]。则 a n s [ i ] = ∑ j = 1 n s u m [ j ] ans[i]=\sum_{j=1}^{n}sum[j] ans[i]=j=1nsum[j]

树状数组求和操作 a s k ( i ) ask(i) ask(i)计算的是以 1 , 2 , 3 , . . . , a [ j ] − 1 1,2,3,...,a[j] - 1 1,2,3,...,a[j]1结尾的长度为 i − 1 i-1 i1的子序列数量和。我们假设一个数组 a r r arr arr(其实在代码里并不需要显式定义), a r r [ j ] arr[j] arr[j]表示以 j j j结尾的长度为 i − 1 i-1 i1的子序列数量。因此直觉上在更新 a n s [ i ] ans[i] ans[i]之前一股脑将 a r r [ j ] arr[j] arr[j]都加到数组中就好了,但是注意原来 n 3 n^3 n3朴素状态更新还要求 k < j k < j k<j,因此需要在更新过程中进行 a d d add add操作(代码中*位置),否则可能导致 k > = j k>=j k>=j的状态也被加上。

参考博客

#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 1E4 + 10;
LL dp[2][N], ans[N], a[N], c[N], mod = 1e9 + 7;
inline int lb(int x){ return x & (-x);}
int n;
void add(LL x, int i)
{
	while(i <= n) 
		c[i] = (x + c[i]) % mod, i += lb(i);
}
LL ask(int i)
{
	LL ans = 0;
	while(i) 
		ans = (ans + c[i]) % mod, i -= lb(i);
	return ans;
}
int main()
{
	int t; scanf("%d", &t);
	int kase = 0;
	while(t --)
	{
		scanf("%d", &n);
		for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);
		memset(dp, 0, sizeof dp);
		memset(ans, 0, sizeof ans);
		memset(c, 0, sizeof c);
		ans[1] = n;
		for(int i = 1; i <= n; i ++) dp[1][i] = 1;
		int now = 0;
		for(int i = 2; i <= n; i ++)
		{
			if(!ans[i - 1]) break;
			for(int j = 1; j <= n; j ++)
			{
				LL sum = ask(a[j] - 1);
				ans[i] = (sum + ans[i]) % mod;
				add(dp[now ^ 1][j], a[j]); //*
				dp[now][j] = sum;
			}
			now ^= 1;
			memset(c, 0, sizeof c);
		}
		printf("Case #%d:", ++kase);
		for(int i = 1; i <= n; i ++) 
			printf(" %lld", ans[i]);
		puts("");
	}
	return 0;
}

Nun Heh Heh Aaaaaaaaaaa HDU - 7131

链接
计算合法子序列的数量。
因为要求特定的前缀,可先将合法前缀的数量求出来,再考虑每个合法前缀将其后面的 a a a接上去后的数量。
d p [ i ] [ j ] dp[i][j] dp[i][j]表示主串 s s s i i i个位置与子串 p = " n u n h e h h e h " p="nunhehheh" p="nunhehheh"匹配长度为 j j j的序列数量,转移较简单,不赘述。然后再用 d p dp dp更新答案,当 j = = 9 j == 9 j==9 i i i位置后的 a a a可选可不选,设 i i i后共有 n u m num num a a a a a a共有 2 i − 1 2^{i}-1 2i1种合法序列。并且应该是用 i i i结尾的合法前缀的个数,否则会重复计算。

#include<iostream>
#include<algorithm>
#include<cstring>
#include<vector>
#define LL long long
using namespace std;
const int N = 1e5 + 10, mod = 998244353;
char p[] = " nunhehheh";
char s[N];
LL dp[N][10], g[N], num[N];
LL qpow(LL a, int b)
{
    LL ans = 1;
    while(b)
    {
        if(b & 1) ans = (ans * a) % mod;
        a = a * a % mod;
        b >>= 1;
    }
    return ans;
}
int main()
{
    int t; scanf("%d", &t);
    while(t --)
    {
        scanf("%s", s + 1);
        memset(dp, 0, sizeof dp);
        int n = strlen(s + 1); //计算后缀和
        num[n + 1] = 0;
        for(int i = n; i >= 1; i --)
        {
            num[i] = num[i + 1];
            if(s[i] == 'a') num[i] ++;
        }
        LL ans = 0;
        for(int i = 0; i <= n; i ++) dp[i][0] = 1;
        for(int i = 1; i <= n; i ++)
        {
            for(int j = 1; j < 10; j ++)
            {
                if(j > i) continue;
                if(s[i] == p[j])
                {
                    dp[i][j] = dp[i - 1][j - 1] % mod;
                    if(j == 9) ans = (ans + dp[i][j] * (qpow(2, num[i + 1]) - 1 + mod) % mod) % mod;
                }
                dp[i][j] = (dp[i][j] + dp[i - 1][j]) % mod;
            }
        }
        printf("%lld\n", ans);
    }
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值