【洛谷1393】Mivik 的标题(容斥+border性质)
- 给定一个长度为\(k\)的子串,求有多少个长度为\(n\)、值域为\(1\sim m\)的序列包含这个子串。
- \(n\le2\times10^5,k\le10^5,m\le10^8\)
容斥+\(DP\)
考虑容斥,用至少出现一次的方案数减至少出现两次的方案数加至少出现三次的方案数减......
即,一个至少出现\(i\)次的方案,对于答案的贡献是\((-1)^{i-1}\)。
因此我们\(DP\),设强制\(i\)可以作为串的一个左端点,前\(i+k-1\)位所有状态的容斥系数之和。
转移分为三种:
- 当前是第一次出现,\(f_i=m^{i-1}\)。
- 上一次出现的位置\(j\le i-k\),乘上一个\(-1\)的系数,两个串中间的位置可以任填:\(f_i\texttt{-=}f_j\times m^{i-k-j}\)。
- 上一次出现的位置\(j>i-k\)(需要满足\(k-(i-j)\)是一个\(border\)),依旧乘上一个\(-1\)的系数:\(f_i\texttt{-=}f_j\)。
第一类转移非常简单。
第二类转移我们维护一个\(t=-\sum_{j=1}^{i-k}f_j\times m^{i-k-j}\),发现从\(i-1\)变化到\(i\)相当于给\(t\)先乘上了一个\(m\)然后减去\(f_{i-k}\)。
第三类转移有些复杂,因此需要考虑\(border\)的性质。
\(border\)的性质
一个经典结论,一个串的所有\(border\)可以分为至多\(logn\)个等差数列。
假设第\(i\)个等差数列公差为\(d_i\),记\(L_i\)为\(k\)减尾项,\(R_i\)为\(k\)减首项,则\(L_i\sim R_i\)中所有模\(d_i\)同余的数都能成为一个决策。
因此我们设\(g_{i,j}\)表示所有满足\(x\le i\)且\(x\equiv i(mod\ d_j)\)的\(f_x\)之和(相当于一个步长为\(d_j\)的前缀和),转移时用\(g_{i-L_j,j}\)减\(g_{i-R_j-d_j,j}\)差分一下即可。
代码:\(O(nlogn)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 200000
#define LN 20
#define X 998244353
using namespace std;
int n,m,k,a[N+5],f[N+5],g[N+5][LN+5];
I int QP(RI x,RI y) {RI t=1;W(y) y&1&&(t=1LL*t*x%X),x=1LL*x*x%X,y>>=1;return t;}
namespace FastIO
{
#define FS 100000
#define tc() (FA==FB&&(FB=(FA=FI)+fread(FI,1,FS,stdin),FA==FB)?EOF:*FA++)
char oc,FI[FS],*FA=FI,*FB=FI;
Tp I void read(Ty& x) {x=0;W(!isdigit(oc=tc()));W(x=(x<<3)+(x<<1)+(oc&15),isdigit(oc=tc()));}
Ts I void read(Ty& x,Ar&... y) {read(x),read(y...);}
}using namespace FastIO;
int nxt[N+5],w[N+5],ct,d[LN+5],L[LN+5],R[LN+5];I void KMP()//KMP预处理
{
RI i,j;for(i=2,j=0;i<=k;a[j+1]==a[i]&&++j,nxt[i++]=j) W(j&&a[j+1]^a[i]) j=nxt[j];//求出nxt数组
for(i=nxt[k];i;i=nxt[i]) {d[++ct]=i-nxt[i],L[ct]=k-i;W(nxt[i]&&i-nxt[i]==d[ct]) i=nxt[i];R[ct]=k-i;}//求出所有等差数列
}
int main()
{
RI i,j;for(read(n,m,k),i=1;i<=k;++i) read(a[i]);KMP();
RI p=1,t=0;for(i=1;i<=n-k+1;++i)//枚举每个位置,强制成为左端点
{
i>=k&&(t=(1LL*t*m-f[i-k]+X)%X),f[i]=(p+t)%X,p=1LL*p*m%X;//第一次出现;上一个位置≤i-k
for(j=1;j<=ct;++j) i>=L[j]&&(f[i]=(f[i]-g[i-L[j]][j]+X)%X),i>=R[j]+d[j]&&(f[i]=(f[i]+g[i-R[j]-d[j]][j])%X);//枚举每个等差数列,差分转移
for(j=1;j<=ct;++j) g[i][j]=((i>=d[j]?g[i-d[j]][j]:0)+f[i])%X;//以每种公差为步长做前缀和
}
for(p=1,t=0,i=n-k+1;i;--i) t=(t+1LL*f[i]*p)%X,p=1LL*p*m%X;return printf("%d\n",1LL*t*QP(QP(m,n),X-2)%X),0;//注意后面没考虑到的位置可以任填
}
待到再迷茫时回头望,所有脚印会发出光芒