点此看题
首先不考虑 \(2\) 操作,考虑怎么向串的末尾加入 \(x\) 个字符 \(c\),下文将其称之为“一段”。
注意到关键条件:对于加入的字符 \(c\),保证之前串尾的字符不是 \(c\),考虑整段整段地跑 \(\tt kmp\),在两个段完全相同(指个数和字符)时跳出。根据题目条件,如果两个段部分匹配,后面再加字符的话就会匹配不上,所以段不完全相同的时候还要暴力跳 \(nxt\) 指针。
考虑在跳跃的时候计算新增 \(\tt border\) 的总和,假设末尾的段是 \((x,c)\),匹配到前面的段是 \((y,c)\):
这个过程需要记录最后一段确定到的位置 \(cur\),但是注意跳到第一段时,匹配条件放宽成 \(x\geq y\),未确定的 \(\tt border\) 长度都应该是 \(y\),需要特判。时间复杂度和传统的 \(\tt kmp\) 是一致的,都是 \(O(n)\)
现在考虑怎么可持久化,把操作离线下来,发现会构成一棵操作树,每个时刻的串对应树上的一个状态,树上的边对应新添加的段。那么在操作树上处理询问,就只需要支持可撤销 \(\tt kmp\) 即可。
由于 \(\tt kmp\) 不会改变前面的信息,我们想限制单次插入在 \(O(\log n)\) 内的时间完成。用到的关键结论是:若最长 \(\tt border\) 长度 \(L>n/2\),所有长度大于 \(n/2\) 的 \(\tt border\) 构成一个公差为 \(n-L\) 的等差数列。
并且这些等差数列对应的匹配点都是一致的,所以在跳 \(nxt\) 指针至少一次之后,可以直接跳过一个等差数列。那么长度每次至少会减少一半,所以单次插入的时间复杂度 \(O(\log n)\)
时间复杂度 \(O(n\log n)\)
#include <cstdio> #include <vector> #include <iostream> using namespace std; const int M = 100005; const int MOD = 998244353; #define int long long int read() { int x=0,f=1;char c; while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;} while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();} return x*f; } int n,m,id[M],f[M],ans[M],len[M];vector<int> g[M]; struct node{int x,c;}s[M],a[M]; int C(int x) {return x*(x-1)/2;} void dfs(int u,int n) { if(n==-1) { for(int v:g[u]) dfs(v,n+1); return ; } s[n]=a[u];len[n+1]=len[n]+a[u].x; if(!n) ans[u]=C(s[n].x); else { int cur=0,j=f[n]; for(int tj=0;~j;j=tj) { if(j<f[n] && f[j]>j/2) tj=(j-1)%(j-f[j])+1; else tj=f[j]; if(s[j].c==s[n].c) { int i=min(s[j].x,s[n].x); if(cur<i) ans[u]+=C(len[j]+i+1) -C(len[j]+cur+1),cur=i; } if(s[j].x==s[n].x && s[j].c==s[n].c) break; } if(j==-1 && s[0].c==s[n].c && s[0].x<s[n].x) ans[u]+=s[0].x*(s[n].x-cur),j=0; f[n+1]=j+1; } for(int v:g[u]) ans[v]=ans[u],dfs(v,n+1); } signed main() { n=read(); for(int i=1;i<=n;i++) { int op=read(),x=read();char c; if(op==1) { cin>>c; a[++m]={x,c-'a'};id[i]=m; g[id[i-1]].push_back(id[i]); } else id[i]=id[x]; } f[0]=-1;f[1]=0;dfs(0,-1); for(int i=1;i<=n;i++) printf("%lld\n",ans[id[i]]%MOD); }