Description
Solution
这题看上去是一道很典型的数位DP。由于又涉及字符串的匹配,所以我们同时考虑AC自动机。
我们按照数位DP的方法,算出[1, R]的答案,然后减去[1, L-1]的答案再相减。由于我太久没写过数位DP了,省赛前也没有好好学,所以对数位DP我仍然一头雾水。在膜了kekxy的博客后,我终于会做这题了。
数位DP需要记的东西,包括但不限于以下几个:当前做到数字的第几位,当前的状态(视题目而定),是否有前导零(可能不用记),是否已经达到了上界(即满位),dp数组的答案(方案数)。
在这题中,尽管进制不一定是十进制,但是做法也是一样的。这里要用到AC自动机,我发现写成记忆化搜索的形式比较麻烦,于是就改成了递推的写法。与记忆化不同的是,递推前导零和满位这两个状态也要保存在数组里。
这题求麻辣值小于k的豆腐数量,而k比较小,于是将k记作一维状态。我们需要知道当前的字符串(m进制数)有多少个数字串匹配,就对数字串建AC自动机,由于每个串匹配其后缀也必然匹配,于是就要在当前节点记录其fail指针一路向上的贡献值之和。而向下亦可以走到其任何一个fail指针的儿子,于是我们构建trie图,将节点没有的儿子指向其fail指针的儿子(fail指针是Root就指向Root)。这样fail指针的更新也就可以直接用父亲的fail指针的儿子。相当于本来要走上斜坡,现在已经将斜坡上有用的信息提前滚到坡底。然后我们就记当前在哪个节点,在自动机上向下走(枚举数字)同时更新各个状态,累计方案数即可。
注意构建trie图和构建AC自动机、fail树是略有区别的。
而本题数字串有前导零,所以为了避免多匹配,前导零是要记的,转移时如果遇到前导零就要回到Root(不放到Root的话就会少转移),当前做到哪个和是否满位是数位DP必须记的。
清楚地列出最终DP的状态:
F[第i位][字符串的状态][当前的麻辣值][是否有前导零][是否达到上界]=方案数
这样的时间复杂度理论上是超了5s的,但是依旧跑得比较快。
最后我想讲一下我做此题时的心情。比赛时写了个AC自动机的暴力,细节打错样例都没过。国庆好不容易有时间改题了,结果改得我“废寝忘食”,改了lz一个下午,连晚饭都忘了吃,一直70分,又拍又调,最后竟然又是忘了考虑相同的字符串??!!mmp!以后我一定会像记国歌一样将AC自动机一定要考虑相同字符串,权值什么的记得累加牢记于心。
最后只能说:我的数位DP太菜了,一定要增强考虑算法细节和题目特例的意识。
Code
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstdlib>
#include <cmath>
#include <cstring>
#define maxl 205
#define maxk 505
#define MOD 1000000007
using namespace std;
int n, m, K, cnt;
int L[maxl], R[maxl], num[maxl];
int f[maxl][maxl][maxk][2][2];
struct AC{
AC *son[20], *fail;
int val, id;
void Clear(){
for(int i = 0; i < 20; i++) son[i] = NULL;
val = 0;
id = cnt;
}
}Node[maxl], *q[maxl], *Root;
AC *NewTnode(){
Node[cnt].Clear();
return Node+cnt++;
}
void AC_Insert(int *x, int v){
AC *now = Root;
for(int i = 1; i <= x[0]; i++){
if(!now->son[x[i]]) now->son[x[i]] = NewTnode();
now = now->son[x[i]];
}
now->val += v;
}
void AC_Buildfail(){
Root->fail = NULL;
q[0] = Root;
int head = 0, tail = 0;
while(head <= tail){
AC *now = q[head++];
for(int i = 0; i < m; i++){
if(now->son[i]){
q[++tail] = now->son[i];
now->son[i]->fail = (now == Root) ? Root : now->fail->son[i];
now->son[i]->val += now->son[i]->fail->val;
}
else now->son[i] = (now == Root) ? Root : now->fail->son[i];
}
}
}
int Sol(int *A){
memset(f, 0, sizeof(f));
f[0][0][0][1][1] = 1;
for(int i = 0; i < A[0]; i++)
for(int j = 0; j < cnt; j++)
for(int k = 0; k <= K; k++)
for(int lead = 0; lead < 2; lead++)
for(int lim = 0; lim < 2; lim++){
if(!f[i][j][k][lead][lim]) continue;
int up = lim ? A[i+1] : m-1;
for(int d = 0; d <= up; d++){
int nj = (lead && !d) ? 0 : Node[j].son[d]->id, nk = k + Node[nj].val;
if(nk > K) continue;
int nlead = lead && !d, nlim = lim && (d==A[i+1]);
f[i+1][nj][nk][nlead][nlim] = (f[i+1][nj][nk][nlead][nlim] + f[i][j][k][lead][lim]) % MOD;
}
}
int ans = 0;
for(int j = 0; j < cnt; j++)
for(int k = 0; k <= K; k++)
for(int lead = 0; lead < 2; lead++)
for(int lim = 0; lim < 2; lim++)
ans = (ans + f[A[0]][j][k][lead][lim]) % MOD;
return ans;
}
int main(){
freopen("1981.in", "r", stdin);
freopen("1981.out", "w", stdout);
scanf("%d%d%d", &n, &m, &K);
scanf("%d", &L[0]);
for(int i = 1; i <= L[0]; i++) scanf("%d", &L[i]);
scanf("%d", &R[0]);
for(int i = 1; i <= R[0]; i++) scanf("%d", &R[i]);
L[L[0]] --;
for(int i = L[0]; L[i] < 0; i--){
L[i] += m;
L[i-1] --;
}
Root = NewTnode();
int v;
for(int i = 1; i <= n; i++){
scanf("%d", &num[0]);
for(int j = 1; j <= num[0]; j++) scanf("%d", &num[j]);
scanf("%d", &v);
AC_Insert(num, v);
}
AC_Buildfail();
printf("%d", (Sol(R) - Sol(L) + MOD) % MOD);
return 0;
}