字符串专题 2

开幕雷击打击复读。看了看题发现这波不能嗯敲了。

原先题单看见会了如果不是很有意义就不敲了。现在实在没什么可以敲的了就开始敲了。

机房好冷为什么不用迪卢克点火,好问题。

CF906E Reverses

可能是经典题,但是是回文 Series 经典题。那我不会。

在题解区随机游走来到了 CF932G。继续随机游走又来到牛子老师博客。感觉很玄妙啊!就写了。

首先你要会最小回文划分。oi-wiki 讲解很详细但是没有代码,我挂个代码在这里。

#include <cstdio>
#include <algorithm>
#include <iostream>
#include <cstring>
using namespace std;
int n;
char s[1000010];
struct PAM{
    int cnt,last,len[1000010],trie[1000010][26],fail[1000010],diff[1000010],slink[1000010];
    PAM(){cnt=1;len[1]=-1;fail[0]=1;slink[0]=1;}
    void ins(int ch,int i,char s[]){
        int p=last;
        while(i-len[p]<=0||s[i-len[p]-1]!=s[i])p=fail[p];
        if(!trie[p][s[i]-'a']){
            len[++cnt]=len[p]+2;
            int q=fail[p];
            while(i-len[q]<=0||s[i-len[q]-1]!=s[i])q=fail[q];
            fail[cnt]=trie[q][ch];
            trie[p][ch]=cnt;
            diff[cnt]=len[cnt]-len[fail[cnt]];
            slink[cnt]=(diff[cnt]==diff[fail[cnt]])?slink[fail[cnt]]:fail[cnt];
        }
        last=trie[p][ch];
    }
}pam;
int dp[1000010],g[1000010];
int main(){
    scanf("%s",s+1);n=strlen(s+1);
    for(int i=1;i<=n;i++){
        dp[i]=0x3f3f3f3f;
        pam.ins(s[i]-'a',i,s);
        for(int p=pam.last;p;p=pam.slink[p]){
            g[p]=dp[i-pam.len[pam.slink[p]]-pam.diff[p]];
            if(pam.slink[p]!=pam.fail[p])g[p]=min(g[p],g[pam.fail[p]]);
            dp[i]=min(dp[i],g[p]+1);
        }
    }
    printf("%d\n",dp[n]);
    return 0;
}

然后给这个题来点不太平凡的转化。设给的两个串为 \(a,b\),那构造串 \(a_1b_1a_2b_2\cdots a_nb_n\),给它做最小偶回文串划分就是答案。注意长度 \(2\) 的回文串不计入代价。注意记录方案。

最小偶回文串划分见 CF932G,把方案数改成最小值。

#include <cstdio>
#include <algorithm>
#include <iostream>
#include <cstring>
using namespace std;
int n;
char s[1000010],ch1[500010],ch2[500010];
struct PAM{
    int cnt,last,len[1000010],trie[1000010][26],fail[1000010],diff[1000010],slink[1000010];
    PAM(){cnt=1;len[1]=-1;fail[0]=1;slink[0]=1;}
    void clear(){
        for(int i=0;i<=cnt;i++){
            len[i]=fail[i]=diff[i]=slink[i]=0;
            for(int j=0;j<26;j++)trie[i][j]=0;
        }
        cnt=1;len[1]=-1;fail[0]=1;slink[0]=1;last=0;
    }
    void ins(int ch,int i,char s[]){
        int p=last;
        while(i-len[p]<=0||s[i-len[p]-1]!=s[i])p=fail[p];
        if(!trie[p][s[i]-'a']){
            len[++cnt]=len[p]+2;
            int q=fail[p];
            while(i-len[q]<=0||s[i-len[q]-1]!=s[i])q=fail[q];
            fail[cnt]=trie[q][ch];
            trie[p][ch]=cnt;
            diff[cnt]=len[cnt]-len[fail[cnt]];
            slink[cnt]=(diff[cnt]==diff[fail[cnt]])?slink[fail[cnt]]:fail[cnt];
        }
        last=trie[p][ch];
    }
}pam;
int dp[1000010],g[1000010],pre[1000010];
int main(){
    scanf("%s%s",ch1+1,ch2+1);n=strlen(ch1+1)<<1;
    for(int i=1;i<=n;i++){
        if(i&1)s[i]=ch1[i+1>>1];
        else s[i]=ch2[i>>1];
    }
    for(int i=1;i<=n;i++){
        dp[i]=0x3f3f3f3f;
        pam.ins(s[i]-'a',i,s);
        if(!(i&1)&&s[i]==s[i-1]&&dp[i-2]<dp[i]){
            dp[i]=dp[i-2];pre[i]=i-2;
        }
        for(int p=pam.last;p;p=pam.slink[p]){
            g[p]=i-pam.len[pam.slink[p]]-pam.diff[p];
            if(pam.slink[p]!=pam.fail[p]&&dp[g[p]]>dp[g[pam.fail[p]]])g[p]=g[pam.fail[p]];
            if(!(i&1)&&dp[i]>dp[g[p]]+1)dp[i]=dp[g[p]]+1,pre[i]=g[p];
        }
    }
    if(dp[n]==0x3f3f3f3f){
        puts("-1");return 0;
    }
    printf("%d\n",dp[n]);
    for(int i=n;i>=1;i=pre[i]){
        if(i-pre[i]!=2)printf("%d %d\n",(pre[i]>>1)+1,i>>1);
    }
    return 0;
}

CF666E Forensic Examination

你说的对,但是因为你说的对,所以你说的不对。

一眼题,无题解,就是码。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
int n,m;
string s,t[50010];
struct SAM{
    int cnt,fa[100010],len[100010],trie[100010][26];
    SAM(){cnt=1;}
    int ins(int ch,int last){
        if(trie[last][ch]){
            int p=last,q=trie[p][ch];
            if(len[p]+1==len[q])return q;
            else{
                len[++cnt]=len[p]+1;
                for(int i=0;i<26;i++)trie[cnt][i]=trie[q][i];
                fa[cnt]=fa[q];fa[q]=cnt;
                while(trie[p][ch]==q)trie[p][ch]=cnt,p=fa[p];
                return cnt;
            }
        }
        int p=last;last=++cnt;
        len[last]=len[p]+1;
        while(p&&!trie[p][ch])trie[p][ch]=cnt,p=fa[p];
        if(!p){
            fa[last]=1;return last;
        }
        int q=trie[p][ch];
        if(len[p]+1==len[q]){
            fa[last]=q;return last;
        }
        len[++cnt]=len[p]+1;
        for(int i=0;i<26;i++)trie[cnt][i]=trie[q][i];
        fa[cnt]=fa[q];fa[q]=cnt;fa[last]=cnt;
        while(trie[p][ch]==q)trie[p][ch]=cnt,p=fa[p];
        return last;
    }
}sam;
struct Data{
    int val,cnt;
    bool operator<(const Data&s)const{
        return cnt==s.cnt?val>s.val:cnt<s.cnt;
    }
    Data operator+(const Data&s)const{
        return {val,cnt+s.cnt};
    }
};
struct node{
    #define lson tree[rt].ls
    #define rson tree[rt].rs
    int ls,rs;
    Data val;
}tree[100010<<5];
int T,rt[100010];
void pushup(int rt){
    tree[rt].val=max(tree[lson].val,tree[rson].val);
}
void update(int &rt,int L,int R,int pos,int val){
    if(!rt)rt=++T;
    if(L==R){
        tree[rt].val.cnt+=val;tree[rt].val.val=pos;
        return;
    }
    int mid=(L+R)>>1;
    if(pos<=mid)update(lson,L,mid,pos,val);
    else update(rson,mid+1,R,pos,val);
    pushup(rt);
}
Data query(int rt,int L,int R,int l,int r){
    if(!rt)return {0,0};
    if(l<=L&&R<=r)return tree[rt].val;
    int mid=(L+R)>>1;
    Data val={0,0};
    if(l<=mid)val=max(val,query(lson,L,mid,l,r));
    if(mid<r)val=max(val,query(rson,mid+1,R,l,r));
    return val;
}
int merge(int x,int y,int l,int r){
    if(!x||!y)return x|y;
    int rt=++T;
    if(l==r){
        tree[rt].val=tree[x].val+tree[y].val;
        return rt;
    }
    int mid=(l+r)>>1;
    tree[rt].ls=merge(tree[x].ls,tree[y].ls,l,mid);
    tree[rt].rs=merge(tree[x].rs,tree[y].rs,mid+1,r);
    pushup(rt);
    return rt;
}
struct gra{
    int v,next;
}edge[100010];
int tot,head[100010];
void add(int u,int v){
    edge[++tot].v=v;edge[tot].next=head[u];head[u]=tot;
}
int dep[100010],fa[100010][20];
void dfs(int x,int f){
    fa[x][0]=f;dep[x]=dep[f]+1;
    for(int i=1;i<=__lg(sam.cnt);i++)fa[x][i]=fa[fa[x][i-1]][i-1];
    for(int i=head[x];i;i=edge[i].next){
        dfs(edge[i].v,x);
        rt[x]=merge(rt[x],rt[edge[i].v],1,n);
    }
}
int pos[500010],len[500010];
void calcpos(){
    int p=1;
    for(int i=0,l=0;i<s.length();i++){
        while(p!=1&&!sam.trie[p][s[i]-'a'])p=sam.fa[p],l=sam.len[p];
        if(sam.trie[p][s[i]-'a'])p=sam.trie[p][s[i]-'a'],l++;
        pos[i]=p;len[i]=l;
    }
}
int get(int l,int r){
    int p=pos[r],len=r-l+1;
    for(int i=__lg(sam.cnt);i>=0;i--)if(sam.len[fa[p][i]]>=len)p=fa[p][i];
    return p;
}
Data getans(int l,int r,int L,int R){
    if(len[r]<r-l+1)return {L,0};
    int p=get(l,r);
    Data ans=query(rt[p],1,n,L,R);
    if(ans.cnt==0)ans.val=L;
    return ans;
}
int main(){
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    cin>>s>>n;
    for(int i=1;i<=n;i++){
        cin>>t[i];int last=1;
        for(int j=0;j<t[i].length();j++)last=sam.ins(t[i][j]-'a',last);
    }
    for(int i=2;i<=sam.cnt;i++)add(sam.fa[i],i);
    for(int i=1;i<=n;i++){
        int p=1;
        for(int j=0;j<t[i].length();j++){
            p=sam.trie[p][t[i][j]-'a'];
            update(rt[p],1,n,i,1);
        }
    }
    dfs(1,0);
    calcpos();
    cin>>m;
    while(m--){
        int l,r,pl,pr;cin>>pl>>pr>>l>>r;
        Data ans=getans(l-1,r-1,pl,pr);
        cout<<ans.val<<' '<<ans.cnt<<'\n';
    }
    return 0;
}

UR #20 机器蚤分组

UOJ 盛产神题、高质量题和不可做题。

首先这是个最小链覆盖。然后最小链覆盖等于最长反链。

然后手模样例得到结论:最长反链长度是某个长度的本质不同子串个数。它是对的。证明考虑假设最长反链不是全 \(len\),那么可以不断将最短的一个串的相邻一个字符加到这个串里而保持合法。

于是问题变成某个长度的本质不同子串个数最大值。

另一个结论:最长反链长度 \(\ge k\) 当且仅当长度 \(n-k+1\) 的子串两两不同。充分性显然,证下必要性。

考虑某个长度 \(len\le n-k+1\),那么如果有两个长 \(n-k+1\) 的子串相同,则对于所有 \(n-k+1-len\) 以内长度的它们的子串也都相同。于是长度 \(len\) 的不同子串个数不超过 \((n-len+1)-(n-k+1-len+1)=k-1\),显然矛盾。

由此我们知道答案即是 \(n-\max_{i,j}lcs(s\langle i],s\langle j])\)。考虑维护后边这个东西。

把询问离线下来扫描线,回答询问的时候二分 \(lcs\) 长度。判断的话按照封印那题,只要看结尾在 \([l+mid-1,r]\) 的串的 \(lcs\) 是否 \(\ge mid\)。这个东西可以 SAM + LCT,每次更新 parent 树的一条实链的时候在 endpos 结尾的前缀取个 max,可以树状数组。LCT 要维护某个串上一个 endpos 的位置,更新的时候树状数组在这个位置的前缀更新长度最大值,复杂度 \(O(n\log^2n)\),实际跑挺快。

题解说有 parent 树启发式合并方法,不会。

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <vector>
using namespace std;
int n,m,ans[100010];
vector<pair<int,int> >q[100010];
char s[100010];
struct BIT{
    int c[100010];
    #define lowbit(x) (x&-x)
    void update(int x,int val){
        while(x)c[x]=max(c[x],val),x-=lowbit(x);
    }
    int query(int x){
        int ans=0;
        while(x<=n)ans=max(ans,c[x]),x+=lowbit(x);
        return ans;
    }
}c;
int pos[100010];
struct SAM{
    int cnt,last,len[200010],fa[200010],trie[200010][26];
    SAM(){cnt=last=1;}
    void ins(int ch,int ps){
        int p=last;last=++cnt;
        len[last]=len[p]+1;pos[ps]=last;
        while(p&&!trie[p][ch])trie[p][ch]=cnt,p=fa[p];
        if(!p){
            fa[last]=1;return;
        }
        int q=trie[p][ch];
        if(len[p]+1==len[q]){
            fa[last]=q;return;
        }
        len[++cnt]=len[p]+1;
        for(int j=0;j<26;j++)trie[cnt][j]=trie[q][j];
        fa[cnt]=fa[q];fa[q]=cnt;fa[last]=cnt;
        while(trie[p][ch]==q)trie[p][ch]=cnt,p=fa[p];
    }
}sam;
namespace LCT{
    #define lson tree[x].son[0]
    #define rson tree[x].son[1]
    struct lct{
        int fa,son[2],end,lz;
    }tree[200010];
    bool isroot(int x){
        return tree[tree[x].fa].son[0]!=x&&tree[tree[x].fa].son[1]!=x;
    }
    bool get(int x){
        return tree[tree[x].fa].son[1]==x;
    }
    void pushtag(int x,int val){
        tree[x].end=tree[x].lz=val;
    }
    void pushdown(int x){
        if(tree[x].lz){
            if(lson)pushtag(lson,tree[x].lz);
            if(rson)pushtag(rson,tree[x].lz);
            tree[x].lz=0;
        }
    }
    void rotate(int x){
        int y=tree[x].fa,z=tree[y].fa,tmp=get(x);
        if(!isroot(y))tree[z].son[tree[z].son[1]==y]=x;
        tree[y].son[tmp]=tree[x].son[tmp^1];
        if(tree[x].son[tmp^1])tree[tree[x].son[tmp^1]].fa=y;
        tree[x].son[tmp^1]=y;
        tree[y].fa=x;tree[x].fa=z;
    }
    void pushall(int x){
        if(!isroot(x))pushall(tree[x].fa);
        pushdown(x);
    }
    void splay(int x){
        pushall(x);
        for(int i=tree[x].fa;i=tree[x].fa,!isroot(x);rotate(x)){
            if(!isroot(i))rotate(get(i)==get(x)?i:x);
        }
    }
    void access(int id){
        int p,x=pos[id];
        for(p=0;x;p=x,x=tree[x].fa){
            splay(x);rson=p;
            if(tree[x].end)c.update(tree[x].end,sam.len[x]);
        }
        pushtag(p,id);
    }
}
int main(){
    scanf("%d%d%s",&n,&m,s+1);
    for(int i=1;i<=n;i++)sam.ins(s[i]-'a',i);
    for(int i=2;i<=sam.cnt;i++)LCT::tree[i].fa=sam.fa[i];
    for(int i=1;i<=m;i++){
        int l,r;scanf("%d%d",&l,&r);q[r].push_back(make_pair(l,i));ans[i]=r-l+1;
    }
    for(int i=1;i<=n;i++){
        LCT::access(i);
        for(pair<int,int>p:q[i]){
            int l=0,r=ans[p.second];
            while(l<r){
                int mid=(l+r+1)>>1;
                if(c.query(p.first+mid-1)>=mid)l=mid;
                else r=mid-1;
            }
            ans[p.second]-=l;
        }
    }
    for(int i=1;i<=m;i++)printf("%d\n",ans[i]);
    return 0;
}

[AGC007F] Shik and Copying String

板刷 AGC 产物。虽然现在好像早就把这活放下了。见 https://www.cnblogs.com/gtm1514/p/16897422.html。

gym103371B Cilantro

讨厌猜结论。不过这题结论还算好猜。

先判对应个数不相等无解。然后猜一个只要找到 \(s\) 最后一个能匹配 \(t\) 的第一个的位置,那么前边所有的都能匹配。

当然可以二分暴力模拟,但 0.5s 5e6 不要想了。

其实可以贪心,用 \(s\) 的第一个贪心找 \(t\) 的最后一个相同的,然后在栈中匹配若干,然后在 \(t\) 前边找 \(s\) 第二个,以此类推,最后找到 \(t\) 第一个的位置。

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <vector>
using namespace std;
int n,sum1[5000010],sum2[5000010];
long long ans;
char s[5000010],t[5000010];
int main(){
    scanf("%d%s%s",&n,s+1,t+1);
    for(int i=1;i<=n;i++){
        sum1[i]=(sum1[i-1]+(s[i]=='Y'));
        sum2[i]=(sum2[i-1]+(t[i]=='Y'));
    }
    if(sum1[n]!=sum2[n]){
        puts("0");return 0;
    }
    int p=1,pos1=n,pos2=n;
    while(pos2>1){
        if(s[p]==t[pos2]){
            p++;pos2--;continue;
        }
        int p1=pos1,p2=pos2;
        while(pos1&&pos2){
            pos1--;pos2--;
            if(sum1[p1]-sum1[pos1]==sum2[p2]-sum2[pos2])break;
        }
    }
    for(int i=1;i<=p;i++)if(s[i]==t[1])ans+=i;
    printf("%lld\n",ans);
}

gym103409H Popcount Words

先打个表,把前若干位放过来。

0110 1001 1001 0110  1001 0110 0110 1001  1001 0110 0110 1001  0110 1001 1001 0110

形如这种分形。话说我小时候喜欢数这个数列。显然存在倍增结构,那么每个区间拆成 \(\log\) 个子区间并起来扫。可以设个 \(w_{n,0/1}\)表示倍增 \(n\) 次的结果,有显然转移 \(w_{n,i}=w_{n-1,i}+w_{n-1,1-i}\)

然后看到这种文本串巨大但是询问有规律的东西,考虑对询问整点什么串串数据结构。字符串匹配问题 SAM 不太合适,上 AC 自动机。那搞一个 dp 来转移经过多少次。

\(dp_{x,i,0/1}\) 为经过 \(x\) 节点的 \(w_{i,0/1}\) 转移有多少次,要倍增预处理一个 \(nxt_{x,i,0/1}\) 为在 \(x\)\(w_{i,0/1}\) 跑到哪里,跑着记录一个次数,倒着扫 \(i\) 进行 dp 就好了。

这题卡时卡空,建议出题人先用自己的马在 CF 上跑一遍。dp 可以滚动数组,然后数组三维顺序要调一下,具体看代码。

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
int n,q,cnt,trie[500010][2],fail[500010],pos[500010];
char t[500010];
void ins(char s[],int id){
    int p=0,len=strlen(s+1);
    for(int i=1;i<=len;i++){
        if(!trie[p][s[i]-'0'])trie[p][s[i]-'0']=++cnt;
        p=trie[p][s[i]-'0'];
    }
    pos[id]=p;
}
int dfn[500010];
void build(){
    int l=1,r=0;
    for(int i=0;i<2;i++)if(trie[0][i])dfn[++r]=trie[0][i];
    while(l<=r){
        int x=dfn[l];l++;
        for(int i=0;i<2;i++){
            if(trie[x][i]){
                fail[trie[x][i]]=trie[fail[x]][i];
                dfn[++r]=trie[x][i];
            }
            else trie[x][i]=trie[fail[x]][i];
        }
    }
}
struct node{
    int n,od;
}s[6000010];
int num,nxt[2][30][500010],c[2][30][500010];
long long dp[2][2][500010],sz[500010];
int main(){
    scanf("%d%d",&n,&q);
    for(int i=1;i<=n;i++){
        int l,r;scanf("%d%d",&l,&r);
        while(l+(l&-l)-1<=r){
            s[++num]={__lg(l&-l),__builtin_popcount(l)&1};
            l+=l&-l;
        }
        for(int j=29;j>=0;j--){
            if(l+(1<<j)-1<=r){
                s[++num]={j,__builtin_popcount(l)&1};
                l+=1<<j;
            }
        }
    }
    for(int i=1;i<=q;i++){
        scanf("%s",t+1);ins(t,i);
    }
    build();
    for(int x=0;x<=cnt;x++)nxt[0][0][x]=trie[x][0],nxt[1][0][x]=trie[x][1];
    for(int i=1;i<=29;i++){
        for(int x=0;x<=cnt;x++){
            nxt[0][i][x]=nxt[1][i-1][nxt[0][i-1][x]];
            nxt[1][i][x]=nxt[0][i-1][nxt[1][i-1][x]];
        }
    }
    int p=0;
    for(int i=1;i<=num;i++){
        c[s[i].od][s[i].n][p]++;
        p=nxt[s[i].od][s[i].n][p];
    }
    int cur=0;
    for(int i=29;i>=0;i--){
        cur^=1;
        for(int x=0;x<=cnt;x++)dp[0][cur][x]=dp[1][cur][x]=0;
        for(int x=0;x<=cnt;x++){
            dp[0][cur][x]+=c[0][i][x]+dp[0][cur^1][x];
            dp[1][cur][x]+=c[1][i][x]+dp[1][cur^1][x];
            dp[1][cur][nxt[0][i][x]]+=dp[0][cur^1][x];
            dp[0][cur][nxt[1][i][x]]+=dp[1][cur^1][x];
        }
    }
    for(int x=0;x<=cnt;x++)sz[trie[x][0]]+=dp[0][cur][x],sz[trie[x][1]]+=dp[1][cur][x];
    for(int i=cnt;i>=1;i--)sz[fail[dfn[i]]]+=sz[dfn[i]];
    for(int i=1;i<=q;i++)printf("%lld\n",sz[pos[i]]);
    return 0;
}

ULR #1 打击复读

噔 噔 咚

果然晚上不带脑子的时候就应该做两种题,一种是不带脑子打板子的题,如第一套的大部分。另一种是就算带脑子也不会的题,比如这玩意。

首先我们会暴力,跑个 SAM 出来对于每个点枚举 len 然后暴力维护 endpos,就得到如下式子:

设 SAM 上节点 \(x\) 对应的长度区间为 \(l_x,r_x\),那么枚举串长 \(k\),节点 \(x\) 的贡献就是

\[\begin{aligned} &\sum_{j=l_x}^{r_x}\left(\sum_{i\in \text{endpos}_x}wl_{i-j+1}\right)\left(\sum_{i\in \text{endpos}_x}wr_i\right)\\ =&\left(\sum_{i\in \text{endpos}_x}wr_i\right)\left(\sum_{i\in\text{endpos}_x}s_{i-l_x+1}-s_{i-r_x}\right) \end{aligned} \]

\(s\)\(wl\) 前缀和。这玩意暴力算的话总复杂度是 \(O(n^2)\) 的。

前一项容易简单做个子树和维护,问题在后边一项怎么搞。只考虑一项 \(s_{i-r_x}\),另一项类似。

发现每个节点的左端点是相同的,那么建立反串 SAM,对于原串 SAM 的一个节点 \(x\),在上面找到串 \(s[i-r_x+1,i]\) 对应的节点 \(y\),那么 \(\sum_{i\in\text{endpos}_x}s_{i-r_x}\) 就是 \(\sum_{i\in\text{endpos}_y}s_{i-1}\)。倍增爆跳就是 \(O(\log n)\) 的。

带修的话只改左权值,那倒过来就变成右权值。修改 \(i\) 要算以 \(i\) 为右端点的所有串的左权值。而刚才我们对每个点算了一个后缀的左权值之和,那么要求的其实就是从 \(i\) 结尾的后缀到根节点的链和,随便做就完了。

还有一个做法是设 \(f_i=\sum_{j=i}^nvr(s[i,j])\),那么把 \(s[i,n]\) 在正串 SAM 上跑,每跑过一个节点就算上它的贡献(子树和乘子树大小)就容易得到答案。贡献容易求,求这个是 DAG 的链和,可以 DAG 链剖分,但我不会。这两个做法修改显然都是单次 \(O(1)\)

然而还有一个简洁且扩展性较强的线性做法。

考虑结合以上两个算法,即正反串后缀树和自动机。同时建立 SAM 和后缀树,然后在后缀树上 dfs 同时累加每个节点贡献,那么到后缀 \(s[i,n]\) 节点时的贡献和就是上面的 \(f_i\)。贡献可以通过在 SAM 上经过后缀树一条边上的字符串得到的路径求和得到。实际上这就是走了压缩后缀自动机中的一条边,直接建立压缩后缀自动机并在上边跑即可。

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <vector>
using namespace std;
int n,m;
char s[500010];
unsigned long long wl[500010],wr[500010];
struct node{
    int v,next;
}edge[2000010];
int t,head[1000010][2];
void add(int u,int v,int id){
    edge[++t].v=v;edge[t].next=head[u][id];head[u][id]=t;
}
struct SAM{
    int cnt,last,len[1000010],fa[1000010],trie[1000010][26],pos[1000010],id[1000010];
    SAM(){cnt=last=1;}
    void ins(int ch,int ps){
        int p=last;last=++cnt;
        len[last]=len[p]+1;pos[last]=id[last]=ps;
        while(p&&!trie[p][ch])trie[p][ch]=cnt,p=fa[p];
        if(!p){
            fa[last]=1;return;
        }
        int q=trie[p][ch];
        if(len[p]+1==len[q]){
            fa[last]=q;return;
        }
        len[++cnt]=len[p]+1;
        for(int j=0;j<26;j++)trie[cnt][j]=trie[q][j];
        fa[cnt]=fa[q];id[cnt]=id[q];
        fa[q]=cnt;fa[last]=cnt;
        while(trie[p][ch]==q)trie[p][ch]=cnt,p=fa[p];
    }
}sam1,sam2;
int sz[1000010],nxt[1000010];
unsigned long long sum[1000010],f[1000010];
bool vis[1000010];
void dfs1(int x){
    for(int i=head[x][0];i;i=edge[i].next){
        dfs1(edge[i].v);
        sz[x]+=sz[edge[i].v];
        sum[x]+=sum[edge[i].v];
    }
}
void build(){
    static int cnt[1000010],dfn[1000010];
    for(int i=1;i<=sam1.cnt;i++)cnt[sam1.len[i]]++;
    for(int i=1;i<=n;i++)cnt[i]+=cnt[i-1];
    for(int i=1;i<=sam1.cnt;i++)dfn[cnt[sam1.len[i]]--]=i;
    for(int i=sam1.last;i!=1;i=sam1.fa[i])vis[i]=true;
    for(int i=sam1.cnt;i>=2;i--){
        int x=dfn[i];sum[x]*=sz[x];
        int cnt=0,p=0;
        for(int j=0;j<4;j++)if(sam1.trie[x][j])cnt++,p=sam1.trie[x][j];
        if(!vis[x]&&cnt==1)nxt[x]=nxt[p],sum[x]+=sum[p];
        else nxt[x]=x;
    }
}
void dfs(int x,int y,unsigned long long S){
    if(sam2.pos[x])f[sam2.pos[x]]=S;
    for(int i=head[x][1];i;i=edge[i].next){
        char ch=s[sam2.id[edge[i].v]+sam2.len[x]];
        dfs(edge[i].v,nxt[sam1.trie[y][ch]],S+sum[sam1.trie[y][ch]]);
    }
}
int main(){
    scanf("%d%d%s",&n,&m,s+1);
    for(int i=1;i<=n;i++)scanf("%llu",&wl[i]);
    for(int i=1;i<=n;i++)scanf("%llu",&wr[i]);
    for(int i=1;i<=n;i++){
        if(s[i]=='A')s[i]=0;
        if(s[i]=='T')s[i]=1;
        if(s[i]=='G')s[i]=2;
        if(s[i]=='C')s[i]=3;
        sam1.ins(s[i],i);
        sz[sam1.last]=1;sum[sam1.last]=wr[i];
    }
    for(int i=2;i<=sam1.cnt;i++)add(sam1.fa[i],i,0);
    dfs1(1);build();
    for(int i=n;i>=1;i--)sam2.ins(s[i],i);
    for(int i=2;i<=sam2.cnt;i++)add(sam2.fa[i],i,1);
    dfs(1,1,0);
    unsigned long long ans=0;
    for(int i=1;i<=n;i++)ans+=wl[i]*f[i];
    printf("%llu\n",ans);
    while(m--){
        unsigned long long x,y;scanf("%llu%llu",&x,&y);
        ans+=(y-wl[x])*f[x];
        wl[x]=y;
        printf("%llu\n",ans);
    }
    return 0;
}
posted @ 2023-05-19 15:07  gtm1514  阅读(21)  评论(0编辑  收藏  举报