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))

代码就不贴了,有需要的可以私信我。

原文地址:https://www.cnblogs.com/ljq-despair/p/10673009.html