HNOI2019 JOJO
HNOI2019 JOJO
被鱼那题送退役了,很生气。
然后我Day1快下考的时候口胡了一个做法
今天想起来之后就写了一下,发现它过了,它过了,它过了。
woc要是不写鱼,我可以多出3个小时写T2,T3,随便打也能进队啊啊啊啊啊啊
好了,不扯了,我们言归正传。
我们发现,若两个串匹配,那么他们中间的每一段的长度也必须相同,只有第一段和最后一段可能不是完整的一段,因此我们可以直接将每一段相同的字符看成一个新的字符(我们暂且把新的字符叫做字段),求next数组。
由于其中一个匹配串必须是整个串的前缀,因此,第一字段的处理几乎与一般的KMP相同,只有一种特殊情况:若某个字段与第一字段的字符相同,并且长度大于第一字段,也是可以匹配的,因为你可以用这一段的后面一段字符去匹配第一段字符
比如说 对于串aabbbaabbaaabbb,我们可以把它看成 a2 b3 a2 b2 a3 b3 共6个字段的串
这个新串的next数组为 0 0 1 0 1 2
此时\(next[4]=0\)而不是\(2\),因为后一个字符一定与\(b\)不同,一定匹配不上下一个字符,这个next对于后面的匹配来说是无意义的,所以我们可以近似的认为,b2与b3不能匹配,但是在统计b2这段字符的答案会出现问题,因为实际上这一段是可以与前面的匹配的,所以我们需要在跳next的过程中,顺便将每个字符的next求出来计入答案。
以上是我的考场做法,可以获得50分,但由于一些奇怪的错误我只获得了20分。
贴下50分代码(考场代码,注释标注了错误)
#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
const int _=1e5+5,M=2e4,yl=998244353;
void inc(int &x,int y){x+=y;if(x>=yl)x-=yl;}
int n;
namespace task1{
struct Node{
int a[305],b[305],f[305],len,ans;
int js(ll l,ll r){
if(l>r)return 0;
return (l+r)*(r-l+1)/2%yl;
}
void insert(int x){
if(len==0)return ans=js(1,x%M-1),b[1]=x%M,f[1]=0,a[++len]=x,void();
ll k=f[len],mx=0; a[++len]=x;
while(1){
if(a[k+1]/M==x/M){
ll l=min(x%M,a[k+1]%M);
inc(ans,js(b[k]+mx+1,b[k]+l));
mx=l;
}
if(a[k+1]==x||!k)break; k=f[k];
}
if(a[k+1]==x)f[len]=k+1,b[len]=b[k]+x%M;
else if(a[1]/M==x/M&&a[1]%M<x%M)
f[len]=1,inc(ans,max(0ll,(x%M-mx))*b[1]%yl);
b[len]=b[len-1]+x%M;
}
}T[305];
void main(){
for(int i=1;i<=n;++i){
int opt,x;cin>>opt;
if(opt==1){
char c;cin>>x>>c;
T[i]=T[i-1];
T[i].insert(c*M+x);
cout<<T[i].ans<<endl;
}
else cin>>x,T[i]=T[x],cout<<T[i].ans<<endl;
}
}
}
namespace task2{
int a[_],b[_],f[_],len,ans;
ll js(ll l,ll r){
if(l>r)return 0;
return (l+r)*(r-l+1)/2%yl;
}
void insert(int x){
if(len==0)return ans=js(1,x%M-1),b[1]=x%M,f[1]=0,a[++len]=x,void();
ll k=f[len],mx=0; a[++len]=x;
while(1){
if(a[k+1]/M==x/M){
ll l=min(x%M,a[k+1]%M);
inc(ans,js(b[k]+mx+1,b[k]+l));
mx=l;
}
if(a[k+1]==x||!k)break; k=f[k];
}
if(a[k+1]==x)f[len]=k+1,b[len]=b[k]+x%M;
else if(a[1]/M==x/M&&a[1]%M<x%M)
f[len]=1,inc(ans,max(0ll,(x%M-mx))*b[1]%yl);
//我考场上把b[1]写成了a[1]丢了30分
b[len]=b[len-1]+x%M;
}
void main(){
for(int i=1;i<=n;++i){
int opt,x;cin>>opt;
char c;cin>>x>>c;
insert(c*M+x);
cout<<ans<<endl;
}
}
}
int main(){
ios::sync_with_stdio(false);
cin>>n;
if(n<=300)task1::main();
else task2::main();
}
至于扩展到100分。
但是暴力跳next的复杂度是均摊的,我们还是不能接受。
因此我们考虑建立一个大概可以叫做KMP自动机的东西,由于经过转化后字符集很大,我们考虑通过可持久化线段树,来维护这个KMP自动机的转移,每次只需要将一个字符的next的转移拷贝过来,并把后一个字段加进转移即可。
再考虑如何统计答案,我们发现,假设有一个字段是长度为L,那么每次匹配到它的另一个字段的前L个字符会被它匹配掉,而我们要求匹配最长,所以我们只需要找到最后一段可以匹配的,依然考虑通过可持久化线段树维护一类字段的第\(i\)个能匹配多长的字符。每次需要进行的操作就变成了前缀赋值与前缀求和。
因此我们就可以通过可持久化线段树的,单点修改,前缀赋值,前缀求和来完成所有1操作。
而这样做可以顺便解决2操作带来的可持久化的问题。
时空复杂度均为\(O(nlog n)\)
代码就不贴了,有需要的可以私信我。