【2018.10.10】[HNOI2008] GT考试(bzoj1009)
10pts:
暴力枚举字符串,Hash判是否出现。(真会有人写么)
时间复杂度$O(10^n*n)$。
40pts:
学过OI的人都会写的dp
如果这道题的40pts($n\le 250000$)设成100pts的话那就是水的一批的提高组题目了。可惜这是省选题目
设$f(i,j)$表示以准考证号为基准递推,准考证号匹配到第$i$位,不吉利数字匹配到第$j$位时(即准考证的后$j$位等于不吉利数字的前$j$位),不出现不吉利数字的字符串数量。
发现$m$只有20,$n*m$的dp可过。
那怎么转移?
既然要以考号为基准递推,就要考虑一位考号对下一位的影响。
而考号是可以随便写的,那我们就要考虑10种数字了。
对于一个新数字$new$,有以下几种情况:
1.$new$与不吉利数字的$j+1$位匹配,$dp(i+1,j+1)$的答案数+dp(i,j)
2.上述两者不匹配。
不匹配怎么办?这个不匹配的$new$一定没有贡献了?
当然不一定。
你有没有听说过一个叫$AC$自动机的垃圾。
啥,你没听说过?那放水点,你有没有听说过一个叫$KMP$的垃圾。
可能存在与$new$后缀与它相同的不吉利数字前缀,而这些前缀位置是要被更新答案的。
怎么讲呢,举个例子吧,不吉利数字是$12212112$。
然后你的准考证号枚举到第$9$位时,前面$8$位已经枚举成了$11112212$。可以发现已经匹配了不吉利数字的前$5$位,现在要匹配第$6$位。
如果第$9$位枚举$1$,那它就匹配了,$dp(9,6)+=dp(8,5)$。
如果第$9$位枚举$2$,那它就不匹配。但是会发现,存在 与当前已匹配的不吉利数字的后缀相同 的前缀,可以匹配上这个$2$!
上一个位置就是不吉利数字的第$2$位。
它的下一位,第$3$位$2$,刚好可以匹配枚举的第$9$位!
所以此时最多能匹配不吉利数字的前$3$位,有转移$dp(9,3)+=dp(8,5)$。
2019.8.23 update:这段好像解释错了,建议无视
我们只需要沿着不吉利数字的失配指针往前走,找到第一个下一位与$new$匹配的位置就可以了。
为什么不用考虑再往前的下一位可以匹配$new$的位置?
因为这是递推,从前往后每一种状态都会被考虑,所以在考虑匹配后面的位之前,前面的位已经匹配好更前面的情况了。
比如不吉利数字$1221221211$。你目前枚举的准考证号前$8$位是$12212212$,现在你要枚举第$9$位。
很明显当枚举$2$时,通过找不吉利数字中 后缀相同的前缀,可知$dp(8,8)$可以转移到$dp(9,6)$。
但是我们发现也可以转移到$dp(9,3)$诶!
事实上,在这之前$dp(8,5)$已经转移到$dp(9,3)$过了。而$dp(8,5)$表示什么?它表示准考证号枚举8位,后$5$位与不吉利数字的前$5$位匹配上。
$dp(8,8)$同理,表示准考证号枚举前$8$位,后$8$位与不吉利数字的前$5$位匹配上。
这样直观看起来没什么答案关联。但是仔细考虑以下,不吉利数字的第$8$位的失配指针指向第$5$位。
这说明什么?
不吉利数字的前$8$位中,前$5$位等于后$5$位!
还不够直观,能再明白点么?
匹配$5$位的情况包含匹配$8$位的情况!(因为你匹配了$8$位,根据上推论可知也算在匹配了$5$位的情况中)
这就是别人博客中此题题解经常提到的计数方案会包含的问题。蒟蒻之前也一直没看懂,想了一通才明白……
所以我们在转移时,把$dp(8,8)$的情况加给$dp(8,5)$,然后让$dp(8,5)$捎带加给$dp(9,3)$。
其实按照$AC$自动机的构造方式,如果一个点没有字符为$new$的儿子边的话,它会建出一条对应的虚拟儿子边和点,儿子点上存的是沿着失配指针往回走的上一个实际存在这条字符边所指向的儿子。
当然这题$m$很小,不吉利数字串自己匹配自己的复杂度很小,可以直接暴力跑失配指针找第一个。
转移就这样
$dp[i+1][j]=\sum_{0<=k<=m-1}dp[i][k] \times g[k][j]$
其中$g(i,j)$表示准考证号匹配不吉利数字的前$i$位时,准考证号增加一个字符,使不吉利数字沿失配指针(自己也可以)找到的最大的匹配位数$j$(算上新匹配的一位)的方案数。
$dp$套$dp$?
其实不用,不吉利数字已经知道了,$g$数组可以预处理出来($KMP$)。
注意一个事情,沿着失配指针走时,可能找不到与准考证号枚举位匹配的位置。此时新的最大匹配位数是$0$,仍然可以转移!
时间复杂度$O(n*m^2)$。
100pts:
然后我们惊奇地发现$n\le 10^9$,不让你循环推,直接就想到矩阵快速幂优化了。
观察一下转移方程,发现$dp[i+1][?]$总是由$dp[i][?]$推来,而且$i$还是$n$这个级别的。
又发现每次实际上都是乘一个固定的矩阵$g$,也就是说整个$dp$数组的某一位的值其实都是通过一些$g$数组乘过来的。
所以可以把$dp$数组直接当成$g$数组自己乘自己。
比如说,$g(4,2)=4$。
表示有4种转移情况可以让 与不吉利数字的前4位匹配的情况 在准考证号增加1位后 与不吉利数字的前2位匹配。
所以$dp(x+1,2)=dp(x,4)*g(4,2)=dp(x,4)*4$ | $x$是正整数。
而$dp(x,4)$又是哪来的?
它是通过$\sum_{i=1}^{m}dp(x-1,i)*g(i,4)$转移过来的。
所以一直往前推到x=0,发现$dp$值的$n$次转移都只跟转移矩阵$g$有关,是否与不吉利数字完全匹配等情况可以在弄$g$数组时就处理掉,即$g$数组只考虑不吉利数字被匹配$0$~$m-1$位的情况,不让它转移到$j$位都被匹配的情况。
所以就是对$g$矩阵做快速幂,求它的$n$次方。
时间复杂度$O(log(n)*m^3)$,有点常数。
最后说明:
1. 为什么答案是$\sum_{i=0}^{j-1}g(0,i)$:
考虑$g(i,j)$的意义。如果$g$矩阵自己对自己连续进行$k$次转移(不进行快速幂而循环推),$g(i,j)$就表示:在进行$k$次转移前 准考证号匹配不吉利数字的前$i$位时,准考证号增加$k$个字符后,使不吉利数字沿失配指针(自己也可以)找到的最大的匹配位数$j$(算上新匹配的$k$位)的方案数。这是矩阵转移的基本概念。
所以进行$n$次转移后,$g(i,j)$就表示一开始准考证号匹配不吉利数字的前$i$位时,准考证号增加$n$个字符后,使不吉利数字沿失配指针(自己也可以)找到的最大的匹配位数$j$(算上新匹配的$k$位)的方案数。
我们需要取全局情况的答案,而这是很显然的。开始时准考证号匹配不吉利数字的前$0$位(准考证号还一位都没枚举),所以$i$为$0$;而由于已经定义过$g$数组只考虑不吉利数字被匹配 $0$~$m-1$ 位的情况,所以$j$在这个区间取任意值,$g(0,j)$都是答案的一部分。把它们都算上就是答案。
2. (2018.11.26 update)初始矩阵是一维线性矩阵,而不是二维的!之前一直以为是二维的导致理解很麻烦!
下面是code,不过我的转移矩阵$b$(就是$g$)的$i,j$维是反过来写的,即 匹配后的匹配位数 指向 匹配前的匹配位数。
1 #include<bits/stdc++.h> 2 #define M 21 3 #define ll long long 4 using namespace std; 5 inline int read(){ 6 int x=0; bool f=1; char c=getchar(); 7 for(;!isdigit(c);c=getchar()) if(c=='-') f=0; 8 for(; isdigit(c);c=getchar()) x=(x<<3)+(x<<1)+(c^'0'); 9 if(f) return x; 10 return 0-x; 11 } 12 int n,m,mod,Fail[M]; 13 char ch[M]; 14 ll a[M][M],b[M][M]; 15 void mul(ll a[M][M],ll b[M][M]){ 16 ll tmp[M][M]; 17 for(int i=0;i<m;++i) 18 for(int j=0;j<m;++j){ 19 tmp[i][j]=0; 20 for(int k=0;k<m;++k) 21 (tmp[i][j]+=a[i][k]*b[k][j])%=mod; 22 } 23 for(int i=0;i<m;++i) 24 for(int j=0;j<m;++j) 25 a[i][j]=tmp[i][j]; 26 } 27 int main(){ 28 n=read(),m=read(),mod=read(); 29 scanf("%s",ch+1); 30 int i,j=0; 31 for(i=2;i<=m;++i){ 32 while(j>0 && ch[j+1]!=ch[i]) j=Fail[j]; 33 if(ch[j+1]==ch[i]) ++j; 34 Fail[i]=j; 35 } 36 int t; 37 for(i=0;i<m;++i) 38 for(j=0;j^10;++j){ 39 t=i; 40 while(t>0 && ch[t+1]-'0'!=j) t=Fail[t]; 41 if(ch[t+1]-'0'==j) ++t; 42 if(t^m) b[t][i]=(b[t][i]+1)%mod; 43 } 44 for(i=0;i<m;++i) a[i][i]=1; 45 while(n){ 46 if(n&1) mul(a,b); 47 mul(b,b); 48 n>>=1; 49 } 50 ll sum=0; 51 for(i=0;i^m;++i) (sum+=a[i][0])%=mod; 52 printf("%lld\n",sum); 53 return 0; 54 }