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)\)
代碼就不貼了,有需要的可以私信我。