cw 字符串专题

模板

以下只是对一些知识做总结,写的很简略。

AC 自动机

不会,但我会背板子。

for(int i=0;i<26;i++) ch[0][i]=1;
queue<int> q;q.push(1);
while(!q.empty())
{
    int u=q.front();q.pop();
    for(int i=0;i<26;i++)
        if(!ch[u][i]) ch[u][i]=ch[fail[u]][i];
        else fail[ch[u][i]]=ch[fail[u]][i],q.push(ch[u][i]);
}

马拉车

\(O(n)\) 求出一个字符串每个位置 \(i\) 最大的 \(r_i\),使得 \([i-r_i+1,i+r_i-1]\) 是回文串。

考虑到一个可能有长度为偶数的回文串,不好搞,我们在每个字符前后都加上一个特殊字符,比如 #。

原串:abbaba

加入后: #a#b#b$a$b#a#

这样我们只用考虑 \(i\) 作为唯一一个回文中心的情况。

记一个位置 \(p,p<i\),使得 \(R=p+r_p\) 最大。

如果 \(i <R\)

我们找到 \(i\) 关于 \(p\) 的对称点 \(i'\)

然后继承 \(r_{i'}\)。并看能否继续拓展。

如图:

否则初始 \(r_i=0\) 然后拓展。

可以发现拓展的时候 \(R\) 始终会增大,所以是 \(O(n)\) 的。

对于边界问题,在字符串边界插入一对不相同的特殊字符即可。

s[0]='?';s[++m]='#';for(int i=1;i<=n;i++) s[++m]=c[i],s[++m]='#';
for(int i=1;i<=m;i++)
{
    if(i<=R) r[i]=min(r[p*2-i],R-i+1);
    while(s[i-r[i]]==s[i+r[i]]) r[i]++;
    if(i+r[i]>R) R=i+r[i]-1,p=i;
}

SA(后缀数组)

定义

\(sa_i\) 表示将 \(S\) 所有后缀排序后第 \(i\) 小的后缀的编号。

\(rk_i\)\(i\) 个后缀的排名。

显然有 \(sa_{rk_i}=rk_{sa_i}=1\)

算法

两个字符串比较时哈希+二分,复杂度 \(O(n\log^2 n)\)

倍增,每个后缀只考虑长度为 \(2^w\) 的前缀,每个前缀分为了 \([i,i+2^{w-1}-1],[i+2^{w-1},i+2^w-1]\) 两段,把他当作 pair 排序即可。复杂度 \(O(n\log^2 n)\)

中间排序的过程可以计数排序,复杂度 \(O(n\log n)\)

减小常数:

  • 不用对第 2 关键字排序,考虑按第 2 关键字排序后的数组为 id,我们可以先把后面为空的编号加入 id,再沿用之前排好序的编号依次加入 id。
  • 拍完序去重,如果去重后不同排名恰有 \(n\) 个,结束算法。
auto eql=[](int x,int y,int w){return _rk[x]==_rk[y]&&_rk[x+w]==_rk[y+w];};
n=strlen(s+1),m=127;
for(int i=1;i<=n;i++) cnt[rk[i]=s[i]]++;
for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--) sa[cnt[rk[i]]--]=i;
for(int w=1,k=0;m!=n;w<<=1,m=k,k=0)
{
    for(int i=n-w+1;i<=n;i++) id[++k]=i;
    for(int i=1;i<=n;i++) if(sa[i]>w) id[++k]=sa[i]-w;
    for(int i=0;i<=m;i++) cnt[i]=0;
    for(int i=1;i<=n;i++) cnt[rk[id[i]]]++;
    for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
    for(int i=n;i>=1;i--) sa[cnt[rk[id[i]]]--]=id[i];
    memcpy(_rk,rk,sizeof(rk));k=0;
    for(int i=1;i<=n;i++) rk[sa[i]]=eql(sa[i],sa[i-1],w)?k:++k;
}

\(height\) 数组

以下简记为 \(ht\)

定义 \(\operatorname{lcp}(i,j)\) 表示后缀 \(i,j\) 的最长公共前缀(长度)。

\(ht_{i}=\operatorname{lcp}(sa_i,sa_{i-1})\),其中 \(ht_{1}=0\)

引理:\(ht_{rk_i}\geq ht_{rk_{i-1}}-1\)

证明:

为方便观察,下面会直接用方括号表示下标。

\(ht[rk[i-1]] \leq 1\),显然成立。

\(ht[rk[i-1]] > 1\) 时,有 \(\operatorname{lcp}(sa[rk[i-1]],sa[rk[i-1]-1])=ht[rk[i-1]]>1\)

则后缀 \(i-1\)\(sa[rk[i-1]]=i-1\))和后缀 \(sa[rk[i-1]-1]\) 有长度为 \(ht[rk[i-1]]\) 的 lcp。

不妨设后缀 \(i-1\)\(aAD\),后缀 \(aAB\),其中 \(a\) 是一个字符,大写字母是字符串。

那么显然有 \(lcp=aA,B<D\)\(B\) 可能为空,\(D\) 非空。

则后缀 \(i\)\(AD\),存在后缀 \(sa[rk[i-1]-1]+1\)\(AB\)

因为 \(D\) 非空,且后缀 \(sa[rk[i]-1]\) 仅比后缀 \(i\) 的排名小一位,那么有 \(AB\leq sa[rk[i]-1]<AD\),即后缀 \(sa[rk[i]-1]\) 和后缀 \(i\) 有公共前缀 A。

于是有 \(ht[rk[i]]=\operatorname{lcp}(i,sa[rk[i]-1])\geq |A|=\operatorname{lcp}(sa[rk[i-1]],sa[rk[i-1]-1])-1=ht[rk[i-1]]-1\)

那么可以线性求。

for(int i=1,k=0;i<=n;i++)
{
    if(k) k--;
    while(s[i+k]==s[sa[rk[i]-1]+k]) k++;
    ht[rk[i]]=k;
}

\(height\) 简单应用

  • 两子串最长公共前缀:

\(\operatorname{lcp}(i,j)=\min\limits_{rk_i<k\leq rk_{j}} ht_{k}\),转化为 RMQ 问题。

  • 本质不同子串个数:

\(\frac{n(n+1)}{2}-\sum ht_i\),求本质不同第 \(k\) 小子串也能做。

SAM(后缀自动机)

不是很想写,先丢个板子。

void ins(int c)
{
    int p=lst,q=lst=++tot;len[q]=len[p]+1;
    while(p&&!ch[p][c]) ch[p][c]=q,p=fa[p];
    if(!p) fa[q]=1;
    else
    {
        int x=ch[p][c];
        if(len[p]+1==len[x]) fa[q]=x;
        else
        {
            int y=++tot;len[y]=len[p]+1;
            for(int i=0;i<26;i++) ch[y][i]=ch[x][i];
            while(p&&ch[p][c]==x) ch[p][c]=y,p=fa[p];
            fa[y]=fa[x],fa[x]=fa[q]=y;
        }
    }
}

题目

P2414 阿狸的打字机

可以发现一个结论:

对于 AC 自动机上的一个结点 \(u\),设其对应的字符串为 \(S_u\),那么 \(S_{fail_u}\)\(S_{u}\) 的子串。

于是建出失配树(连边 \(fail_u \rightarrow u\)),那么 \(u\) 子树内所有结点对应的串都包含 \(S_u\)

对于一组询问 \((x,y)\),设串 \(S_x\) 的结束结点为 \(u\),问题转化为在失配树上 \(u\) 的子树内有多少 \(y\) 的结点

离线后把询问挂到 \(y\) 对应的 trie 树的结束结点上,遍历 trie 树的同时维护树上差分。

#include<bits/stdc++.h>
using namespace std;

const int N=1e5+5;
char c;
int n,m,dn,tot=1,u=1;
int l[N],r[N],id[N],ch[N][26],trie[N][26],fa[N],fail[N],ans[N];
vector<int> G[N],ed[N];
vector<pair<int,int>> q[N];
struct BIT{
    int c[N];
    void upd(int x,int v) {for(;x<=dn;x+=x&-x) c[x]+=v;}
    int qsum(int x) {int r=0;for(;x;x^=x&-x) r+=c[x];return r;}  
}tr;

void build()
{
    memcpy(trie,ch,sizeof(ch));
    for(int i=0;i<26;i++) ch[0][i]=1;
    queue<int> q;q.push(1);
    while(!q.empty())
    {
        int u=q.front();q.pop();
        G[fail[u]].push_back(u);
        for(int i=0;i<26;i++)
            if(!ch[u][i]) ch[u][i]=ch[fail[u]][i];
            else fail[ch[u][i]]=ch[fail[u]][i],q.push(ch[u][i]);
    }
}

void dfs(int u)
{
    l[u]=++dn;
    for(int v:G[u]) dfs(v);
    r[u]=dn;
}

void solve(int u)
{
    tr.upd(l[u],1);
    for(int i:ed[u]) for(auto [x,j]:q[i]) ans[j]=tr.qsum(r[id[x]])-tr.qsum(l[id[x]]-1);
    for(int i=0;i<26;i++) if(trie[u][i]) solve(trie[u][i]);
    tr.upd(l[u],-1);
}

int main()
{
    while((c=getchar())!='\n')
    {
        if(c=='P') ed[u].push_back(++n),id[n]=u;
        else if(c=='B') u=fa[u];
        else {c-='a';if(!ch[u][c]) ch[u][c]=++tot;fa[ch[u][c]]=u,u=ch[u][c];}
    }
    build();dfs(0);
    cin>>m;
    for(int i=1;i<=m;i++)
    {
        int x,y;scanf("%d%d",&x,&y);
        q[y].push_back({x,i});
    }
    solve(1);
    for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
}

CF1207G Indie Album

同样的套路。

#include<bits/stdc++.h>
using namespace std;

const int N=4e5+5;
int n,m,tot=1,dn,ch[N][26],fail[N],ans[N],id[N];
char c[N],s[N];
vector<int> G[N],Tf[N],q[N];
struct BIT{
    int c[N];
    void upd(int x,int v) {for(;x<=dn;x+=x&-x) c[x]+=v;}
    int qsum(int x) {int r=0;for(;x;x^=x&-x) r+=c[x];return r;}  
}tr;

void insert(int i)
{
    int n=strlen(s),u=1;
    for(int i=0;i<n;i++)
    {
        char c=s[i]-'a';
        if(!ch[u][c]) ch[u][c]=++tot;
        u=ch[u][c];
    }
    id[i]=u;
}

void build()
{
    for(int i=0;i<26;i++) ch[0][i]=1;
    queue<int> q;q.push(1);
    while(!q.empty())
    {
        int u=q.front();q.pop();
        Tf[fail[u]].push_back(u);
        for(int i=0;i<26;i++)
            if(!ch[u][i]) ch[u][i]=ch[fail[u]][i];
            else fail[ch[u][i]]=ch[fail[u]][i],q.push(ch[u][i]);
    }
}

int st[N],ed[N];
void dfs(int u)
{
    st[u]=++dn;
    for(int v:Tf[u]) dfs(v);
    ed[u]=dn;
}

void solve(int x,int u)
{
    tr.upd(st[u],1);
    for(int i:q[x]) ans[i]=tr.qsum(ed[id[i]])-tr.qsum(st[id[i]]-1);
    for(int v:G[x]) solve(v,ch[u][c[v]-'a']);
    tr.upd(st[u],-1);
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        int op,j=0;
        cin>>op;if(op==2) cin>>j;cin>>c[i];
        G[j].push_back(i);
    }
    cin>>m;
    for(int i=1;i<=m;i++)
    {
        int x;cin>>x>>s;
        insert(i);q[x].push_back(i);
    }
    build();dfs(0);solve(0,1);
    for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
}

CF1437G Death DBMS

又是一道失配树板题。

根据上面失配树的性质,相当于单点修,维护一条树链上的 max 值。树剖即可。

注意到可能有重复串,故用一个 multiset 维护某个结点的 max。

一些细节:失配树以 0 为根,重儿子初始为 -1,同时注意叶子结点不要遍历重儿子导致数组越界。

#include<bits/stdc++.h>
using namespace std;

const int N=3e5+5;
int n,m,tot=1,a[N];
int id[N],ch[N][26],fail[N];
int dn,sz[N],fa[N],son[N],top[N],dfn[N],rev[N];
char s[N];
vector<int> G[N];
multiset<int> S[N];
struct SegTree{
    int mx[N*4];
    #define lc (k<<1)
    #define rc (k<<1|1)
    #define mid (l+r>>1)
    void build(int k=1,int l=1,int r=dn)
    {
        mx[k]=-1;
        if(l==r) {mx[k]=S[rev[l]].size()?*S[rev[l]].rbegin():-1;return;}
        build(lc,l,mid),build(rc,mid+1,r);
        mx[k]=max(mx[lc],mx[rc]);
    }
    void upd(int x,int k=1,int l=1,int r=dn)
    {
        if(l==r) {mx[k]=S[rev[l]].size()?*S[rev[l]].rbegin():-1;return;}
        x<=mid?upd(x,lc,l,mid):upd(x,rc,mid+1,r);
        mx[k]=max(mx[lc],mx[rc]);
    }
    int qmx(int x,int y,int k=1,int l=1,int r=dn)
    {
        if(l>=x&&r<=y) return mx[k];
        int res=-1;
        if(x<=mid) res=qmx(x,y,lc,l,mid);
        if(mid<y) res=max(res,qmx(x,y,rc,mid+1,r));
        return res; 
    }
}tr;

void build()
{
    for(int i=0;i<26;i++) ch[0][i]=1;
    queue<int> q;q.push(1);
    while(!q.empty())
    {
        int u=q.front();q.pop();
        G[fail[u]].push_back(u);
        for(int i=0;i<26;i++)
            if(!ch[u][i]) ch[u][i]=ch[fail[u]][i];
            else fail[ch[u][i]]=ch[fail[u]][i],q.push(ch[u][i]);
    }
}

void dfs1(int u)
{
    sz[u]=1;son[u]=-1;
    for(int v:G[u])
    {
        fa[v]=u;
        dfs1(v);
        sz[u]+=sz[v];
        if(son[u]==-1||sz[v]>sz[son[u]]) son[u]=v;
    }
}

void dfs2(int u,int topf)
{
    dfn[u]=++dn,rev[dn]=u,top[u]=topf;
    if(~son[u]) dfs2(son[u],topf);
    for(int v:G[u]) if(v!=son[u]) dfs2(v,v);
}

int Qmx(int x)
{
    int mx=-1;
    while(top[x]) mx=max(mx,tr.qmx(dfn[top[x]],dfn[x])),x=fa[top[x]];
    return max(mx,tr.qmx(1,dfn[x]));
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        cin>>s;
        int u=1,len=strlen(s);
        for(int i=0;i<len;i++)
        {  
            int c=s[i]-'a';
            if(!ch[u][c]) ch[u][c]=++tot;
            u=ch[u][c];
        }
        id[i]=u;S[u].insert(0);
    }
    build();
    dfs1(0),dfs2(0,0);tr.build();
    while(m--)
    {
        int op,i,x;cin>>op;
        if(op==1)
        {
            cin>>i>>x;
            S[id[i]].erase(S[id[i]].find(a[i]));
            S[id[i]].insert(a[i]=x);
            tr.upd(dfn[id[i]]);
        }
        else
        {
            cin>>s;
            int u=1,len=strlen(s),mx=-1;
            for(int i=0;i<len;i++)
            {
                int c=s[i]-'a';
                u=ch[u][c];
                mx=max(mx,Qmx(u));
            }
            cout<<mx<<'\n';
        }
    }
}

P3167 通配符匹配

dp + hash 是最简单的。

注意到通配符数量很小。

定义 \(f_{i,j}\) 表示原串匹配到第 \(i\) 个通配符,文件串匹配到第 \(j\) 个字符是否可行。

假设第 \(i\) 个通配符在原串中位置为 \(p_i\)

如果 s[p[i]]='*',且 \(f_{i,j}=1\),那么 \(\forall k>j,f_{i,k}=1\)

\(l=p_i+1,r=p_{i+1}-1\),如果 s[l,r]=t[j+1,j+1+r-l+1],那么可以转移到 \(f_{i+1,j+1+r-l+1+1}\)

这个可以用哈希来判断。

由于 ? 不能匹配空字符,我们钦定它必须要匹配一个才可行,即如果 s[p[i+1]]='?',转移到的下标还要加一。

当然,为方便,我们保证原串结尾为通配符,就可以直接输出答案。

所以原串后面加一个 ?,文件串后面加任意一个字符(让这个问号匹配这个字符)。

#include<bits/stdc++.h>
#define ull unsigned long long
using namespace std;

const int N=1e5+5;
int n,m,q,k,pos[15],f[15][N];
ull pw[N],hs[N],ht[N];
char s[N],t[N];

int main()
{
    scanf("%s",s+1);
    n=strlen(s+1);s[++n]='?';
    pw[0]=1;for(int i=1;i<=1e5;i++) pw[i]=pw[i-1]*117;
    for(int i=1;i<=n;i++)
    {
        hs[i]=hs[i-1]*117+s[i];
        if(s[i]=='?'||s[i]=='*') pos[++k]=i;
    }
    cin>>q;
    while(q--)
    {
        scanf("%s",t+1);
        m=strlen(t+1);t[++m]='a';
        for(int i=1;i<=m;i++) ht[i]=ht[i-1]*117+t[i];
        memset(f,0,sizeof(f));
        f[0][0]=1;
        for(int i=0;i<=k;i++)
        {
            if(s[pos[i]]=='*') for(int j=1;j<=m;j++) f[i][j]|=f[i][j-1];
            for(int j=0;j<=m;j++)
            {
                if(!f[i][j]) continue;
                int ls=pos[i]+1,rs=pos[i+1]-1;
                int lt=j+1,rt=lt+rs-ls;
                if(hs[rs]-hs[ls-1]*pw[rs-ls+1]==ht[rt]-ht[lt-1]*pw[rt-lt+1]) f[i+1][rt+(s[pos[i+1]]=='?')]=1;
            }
        }
        f[k][m]?puts("YES"):puts("NO");
    }
}

P2336 喵星球上的点名

人类智慧 + hash 是最简单的,跑得也很快。

智慧结论:字符长度 \(\leq O(\sqrt{ \sum len})\) 种。

考虑 \(1 \sim n\) 的字符长度全部出现一遍,那么总长度为 \(\sum\limits_{1\leq i\leq n} i \approx n^2\)

对于所有模式串,哈希一下,然后用哈希表存一下出现次数。

那么对于一个字符串 \(S\),对于当前位置 \(i\),截取所有在模式串中出现过的长度为 \(l\) 的字符串,然后哈希判断是否在模式串中出现。

复杂度 \(O(n\sqrt{\sum len})\),非常巧妙。

#include<bits/stdc++.h>
#define ull unsigned long long
using namespace std;

const int N=1e5+5,base=10007;
int n,m,ans[N];
ull pw[N],hs[N],ht[N];
vector<int> v,s[N];
unordered_map<ull,int> cnt,vis,res;

char buf[1<<15],*p1=buf,*p2=buf;
#define nc() (p1==p2&&(p2=buf+fread(p1=buf,1,1<<15,stdin),p1==p2)?-1:*p1++)
inline int rd()
{
    int x=0,f=1;char c=nc();
    for(;!isdigit(c);c=nc()) if(c=='-') f=-1;
    for(; isdigit(c);c=nc()) x=(x<<3)+(x<<1)+(c^48);
    return x*f;
}

int main()
{
    n=rd(),m=rd();
    for(int i=1;i<=n;i++)
    {
        int l=rd();
        while(l--) s[i].push_back(rd()+1);
        s[i].push_back(10002);//中间加个不存在的字符,拼成一个串。
        l=rd();
        while(l--) s[i].push_back(rd()+1);
    }
    pw[0]=1;for(int i=1;i<=1e5;i++) pw[i]=pw[i-1]*base;
    for(int i=1;i<=m;i++)
    {
        int l=rd();ull t=0;
        v.push_back(l);
        while(l--) t=t*base+rd()+1;
        ht[i]=t,cnt[t]++;
    }
    sort(v.begin(),v.end());v.erase(unique(v.begin(),v.end()),v.end());
    for(int i=1;i<=n;i++)
    {
        vis.clear();
        hs[0]=s[i][0];
        for(int j=1;j<s[i].size();j++) hs[j]=hs[j-1]*base+s[i][j];
        for(int j=0;j<s[i].size();j++)
            for(int len:v)
            {
                int l=j-len+1,r=j;
                if(l<0) break;
                ull t=hs[r]-(l-1<0?0:hs[l-1])*pw[len];
                if(cnt.count(t)&&!vis.count(t)) ans[i]+=cnt[t],vis[t]=1,res[t]++;
            }
    }
    for(int i=1;i<=m;i++) cout<<res[ht[i]]<<'\n';
    for(int i=1;i<=n;i++) cout<<ans[i]<<' ';
}

P3975 弦论

只说 \(t=1\) 的情况。

我会枚举!

枚举第 \(i\) 个字符是什么,计算当前有多少个子串就能求第 \(k\) 小。

关键是怎么快速计算当前有多少子串。

考虑前 \(i-1\) 个字符相同的后缀是 \(sa\) 数组下标的一段区间 \([L,R]\)

那么在这一段区间,第 \(i\) 个字符是单调不降的。于是可以二分,然后用前缀和维护一段区间后缀有多少子串。

设前缀数组为 \(sum\),当前二分到最大的区间为 \(r\),那么和当前枚举的字符串相等的就有 \((r-L+1)\) 个,以当前枚举的字符串作为前缀的有 \(sum_r-sum_{L-1}-(r-L+1)\times (i-1)\) 个。

复杂度 \(O(26n\log n)\)

#include<bits/stdc++.h>
using namespace std;

const int N=5e5+5;
char s[N];
long long sum[N];
int n,m=127,k,t,sa[N],rk[N],_rk[N],cnt[N],id[N],ht[N];
bool eql(int x,int y,int w) {return _rk[x]==_rk[y]&&_rk[x+w]==_rk[y+w];}

int main()
{
    scanf("%s%d%d",s+1,&t,&k);
    n=strlen(s+1);
    for(int i=1;i<=n;i++) cnt[rk[i]=s[i]]++;
    for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
    for(int i=n;i>=1;i--) sa[cnt[rk[i]]--]=i;
    for(int w=1,k=0;m!=n;w<<=1,m=k,k=0)
    {
        for(int i=n-w+1;i<=n;i++) id[++k]=i;
        for(int i=1;i<=n;i++) if(sa[i]>w) id[++k]=sa[i]-w;
        for(int i=0;i<=m;i++) cnt[i]=0;
        for(int i=1;i<=n;i++) cnt[rk[id[i]]]++;
        for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
        for(int i=n;i>=1;i--) sa[cnt[rk[id[i]]]--]=id[i];
        memcpy(_rk,rk,sizeof(rk));k=0;
        for(int i=1;i<=n;i++) rk[sa[i]]=eql(sa[i],sa[i-1],w)?k:++k;
    }
    for(int i=1,k=0;i<=n;i++)
    {
        if(k) k--;
        while(s[i+k]==s[sa[rk[i]-1]+k]) k++;
        ht[rk[i]]=k;
    }
    int now=0;
    if(t)
    {
        for(int i=1;i<=n;i++) sum[i]=sum[i-1]+(n-sa[i]+1);
        int L=1,R=n;
        for(int i=1;i<=n;i++)
        {
            for(int j='a';j<='z';j++)
            {
                int l=L,r=R,res=0;
                while(l<=r)
                {
                    int mid=l+r>>1;
                    if(s[sa[mid]+i-1]<=j) res=mid,l=mid+1;
                    else r=mid-1;
                }
                if(!res) continue;
                long long t=sum[res]-sum[L-1]-1ll*(res-L+1)*(i-1);
                if(k<=res-L+1)
                {
                    for(int k=sa[L];k<=sa[L]+i-1;k++) cout<<s[k];
                    return 0;
                }
                if(k<=t) {k-=res-L+1,R=res;break;}
                L=res+1,k-=t;
            }
            if(n-sa[L]+1==i) L++;
        }
    }
    else
    {
        for(int i=1;i<=n;i++)
        {
            int _now=now+n-sa[i]+1-ht[i];
            if(k<=_now)
            {
                int l=sa[i],r=sa[i]+ht[i]-1;
                while(now!=k) r++,now++;
                for(int i=l;i<=r;i++) cout<<s[i];
                return 0;
            }
            now=_now;
        }
        cout<<-1<<'\n';
    }
}

P4094 字符串

这题本来是让用 SA 做的,但我发现 SAM 做更简单(

好像是不能直接得出答案的,不妨二分答案长度为 \(mid\),即二分最长公共前缀为 \(s[c,c+mid-1]\)

设前缀 \(c+mid-1\) 的状态为 \(u\),我们倍增找到 parent tree 上 \(u\) 深度最小的祖先 \(f\),满足 \(mxlen_{f}\geq mid\)

我们看 \(f\) 的子树内是否出现了任意一个前缀 \([a+mid-1,b]\) 对应的结点,如果有,则存在以 \([a+mid-1,b]\) 结尾的一个串与 \(s[c,c+mid-1]\) 的最长公共前缀 \(\geq mid\),且一定会有一个长度为 \(mid\) 的串。这样也保证有一个是 \(s[a,b]\) 的子串。

可以线段树合并来做,注意合并时需要再新建一个点保存信息,因为结点被合并后还可能被访问到。

#include<bits/stdc++.h>
using namespace std;

const int N=200005;
char s[N];
int n,m,id[N],f[N][18];
int tot=1,lst=1,ch[N][26],len[N],fa[N];
vector<int> G[N];

int ins(int c)
{
    int p=lst,q=lst=++tot;len[q]=len[p]+1;
    while(p&&!ch[p][c]) ch[p][c]=q,p=fa[p];
    if(!p) fa[q]=1;
    else
    {
        int x=ch[p][c];
        if(len[p]+1==len[x]) fa[q]=x;
        else
        {
            int y=++tot;len[y]=len[p]+1;
            for(int c=0;c<26;c++) ch[y][c]=ch[x][c];
            while(p&&ch[p][c]==x) ch[p][c]=y,p=fa[p];
            fa[y]=fa[x],fa[x]=fa[q]=y;
        }
    }
    return q;
}

#define mid (l+r>>1)
int Tcnt,rt[N],sum[N*60],lc[N*60],rc[N*60];
void upd(int &k,int x,int l=1,int r=n)
{
    if(!k) k=++Tcnt;
    sum[k]++;
    if(l==r) return;
    x<=mid?upd(lc[k],x,l,mid):upd(rc[k],x,mid+1,r);
}
int merge(int p,int q,int l=1,int r=n)
{
    if(!p||!q) return p+q;
    int x=++Tcnt;sum[x]=sum[p]+sum[q];
    if(l==r) return x;
    lc[x]=merge(lc[p],lc[q],l,mid);
    rc[x]=merge(rc[p],rc[q],mid+1,r);
    return x;
}
int qsum(int k,int x,int y,int l=1,int r=n)
{
    if(!k) return 0;
    if(l>=x&&r<=y) return sum[k];
    int res=0;
    if(x<=mid) res+=qsum(lc[k],x,y,l,mid);
    if(mid<y) res+=qsum(rc[k],x,y,mid+1,r);
    return res;
}
#undef mid

void dfs(int u)
{
    for(int i=1;i<18;i++) f[u][i]=f[f[u][i-1]][i-1];
    for(int v:G[u]) f[v][0]=u,dfs(v),rt[u]=merge(rt[u],rt[v]);
}

bool chk(int mid,int a,int b,int c)
{
    c=id[c+mid-1];
    for(int i=17;~i;i--) if(len[f[c][i]]>=mid) c=f[c][i];
    return qsum(rt[c],a+mid-1,b);
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n>>m>>s;
    for(int i=1;i<=n;i++) upd(rt[id[i]=ins(s[i-1]-'a')],i);
    for(int i=2;i<=tot;i++) G[fa[i]].push_back(i);
    dfs(1);
    while(m--)
    {
        int a,b,c,d;cin>>a>>b>>c>>d;
        int l=1,r=min(b-a+1,d-c+1),res=0;
        while(l<=r)
        {
            int mid=l+r>>1;
            if(chk(mid,a,b,c)) res=mid,l=mid+1;
            else r=mid-1;
        }
        cout<<res<<'\n';
    }
}

LOJ6041 事情的相似度

实际就是求 \(\max\limits_{l\leq i,j \leq r,i\neq j}\{\min\limits_{rk_i < k\leq rk_j} ht_k\}\)

如果我们把 \(rk_i,i\in[l,r]\) 标记一下,那么只用求一个位置到一个被标记的前驱这段区间的 \(\min\{ht\}\) 即可。

可以用链表维护,每次可以维护删除某个标记,用回滚莫队。

对于维护 max,可以用值域分块,做到 \(O(1)\) 插入删除一个值,\(O(\sqrt n)\) 查询。

#include<bits/stdc++.h>
using namespace std;

const int N=1e5+5,T=300;
char s[N];
int n,q,ans[N],st[17][N],pre[N],nxt[N];
int m='1',sa[N],rk[N],_rk[N],id[N],cnt[N],ht[N];
struct node{int l,r,id;} a[N];
bool eql(int x,int y,int w) {return _rk[x]==_rk[y]&&_rk[x+w]==_rk[y+w];}
int qmn(int l,int r){if(!l||!r) return 0;int t=__lg(r-l++);return min(st[t][l],st[t][r-(1<<t)+1]);}

int L[N],R[N],bel[N],c[N],ct[N];
void upd(int x) {if(!x) return;c[x]++,ct[bel[x]]++;}
void ers(int x) {if(!x) return;c[x]--,ct[bel[x]]--;}
int qmx()
{
    for(int i=bel[n];~i;i--)
        if(ct[i])
        {
            i=R[i];
            while(i&&!c[i]) i--;
            return i;
        }
    return 0;
}

void del(int x)
{
    ers(qmn(pre[x],x)),ers(qmn(x,nxt[x])),upd(qmn(pre[x],nxt[x]));
    nxt[pre[x]]=nxt[x],pre[nxt[x]]=pre[x];
}

int main()
{
    scanf("%d%d%s",&n,&q,s+1);
    reverse(s+1,s+1+n);
    for(int i=1;i<=n;i++) cnt[rk[i]=s[i]]++;
    for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
    for(int i=n;i>=1;i--) sa[cnt[rk[i]]--]=i;
    for(int w=1,k=0;m!=n;w<<=1,m=k,k=0)
    {
        for(int i=n-w+1;i<=n;i++) id[++k]=i;
        for(int i=1;i<=n;i++) if(sa[i]>w) id[++k]=sa[i]-w;
        for(int i=0;i<=m;i++) cnt[i]=0;
        for(int i=1;i<=n;i++) cnt[rk[id[i]]]++;
        for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
        for(int i=n;i>=1;i--) sa[cnt[rk[id[i]]]--]=id[i];
        memcpy(_rk,rk,sizeof(rk));k=0;
        for(int i=1;i<=n;i++) rk[sa[i]]=eql(sa[i],sa[i-1],w)?k:++k;
    }
    for(int i=1,k=0;i<=n;i++)
    {
        if(k) k--;
        while(s[i+k]==s[sa[rk[i]-1]+k]) k++;
        ht[rk[i]]=st[0][rk[i]]=k;
    }
    for(int j=1;j<17;j++)
        for(int i=1;i+(1<<j)-1<=n;i++)
            st[j][i]=min(st[j-1][i],st[j-1][i+(1<<j-1)]);
    for(int i=1;i<=q;i++)
    {
        int l,r;scanf("%d%d",&l,&r);
        a[i]={n-r+1,n-l+1,i};
    }
    for(int i=0;i<=n/T;i++)
    {
        L[i]=max(1,i*T),R[i]=min(n,i*T+T-1);
        for(int j=L[i];j<=R[i];j++) bel[j]=i;
    }
    sort(a+1,a+1+q,[&](node a,node b){return a.l/T==b.l/T?a.r>b.r:a.l<b.l;});
    int l,r;
    for(int i=1;i<=q;i++)
    {
        auto [ql,qr,id]=a[i];
        if(i==1||a[i].l/T!=a[i-1].l/T)
        {
            l=L[bel[ql]],r=qr;
            for(int i=1;i<=n;i++) cnt[i]=c[i]=ct[i]=0;
            for(int i=l;i<=r;i++) cnt[rk[i]]++;
            int _pre=0,_nxt=0;
            for(int i=1;i<=n;i++) if(cnt[i]) pre[i]=_pre,_pre=i;
            for(int i=n;i>=1;i--) if(cnt[i]) nxt[i]=_nxt,_nxt=i;
            for(int i=1;i<=n;i++) if(cnt[i]) upd(qmn(pre[i],i));
        }
        while(qr<r) del(rk[r--]);
        int _l=l;
        while(ql>l) del(rk[l++]);
        ans[id]=qmx();
        for(int i=l-1;i>=_l;i--)
        {
            int x=rk[i];
            upd(qmn(pre[x],x)),upd(qmn(x,nxt[x])),ers(qmn(pre[x],nxt[x]));
            nxt[pre[x]]=x,pre[nxt[x]]=x;
        }
        l=_l;
    }
    for(int i=1;i<=q;i++) cout<<ans[i]<<'\n';
}

P6640 封印

板题。

实际上是询问 \(s[l,r]\) 里最长的一个能和 \(t\) 匹配的子串。

对于 \(s\) 的一个前缀 \(i\),维护一个最长的能和 \(t\) 匹配的后缀,记长度为 \(a_i\)

这个对 \(t\) 建完 SAM 后就很好维护了,但一个细节是 \(a_i\) 要和 \(a_{i-1}+1\) 取 min。

那么对于一个询问,就是求 \(\max\limits_{l\leq i\leq r}\{\min(i-l+1,a_i)\}\)

二分 + ST 表即可。

#include<bits/stdc++.h>
using namespace std;

const int N=4e5+5;
char s[N],t[N];
int n,m,q,lst=1,tot=1,ch[N][26],len[N],fa[N],st[20][N];
int qmx(int l,int r) {int t=__lg(r-l+1);return max(st[t][l],st[t][r-(1<<t)+1]);}

void ins(int c)
{
    int p=lst,q=lst=++tot;len[q]=len[p]+1;
    while(p&&!ch[p][c]) ch[p][c]=q,p=fa[p];
    if(!p) fa[q]=1;
    else
    {
        int x=ch[p][c];
        if(len[p]+1==len[x]) fa[q]=x;
        else
        {
            int y=++tot;len[y]=len[p]+1;
            for(int i=0;i<26;i++) ch[y][i]=ch[x][i];
            while(p&&ch[p][c]==x) ch[p][c]=y,p=fa[p];
            fa[y]=fa[x],fa[x]=fa[q]=y;
        }
    }
}

int main()
{
    scanf("%s%s%d",s+1,t+1,&q);
    n=strlen(s+1),m=strlen(t+1);
    for(int i=1;i<=m;i++) ins(t[i]-'a');
    int u=1;
    for(int i=1;i<=n;i++)
    {
        int c=s[i]-'a';
        while(u!=1&&!ch[u][c]) u=fa[u];
        st[0][i]=min(st[0][i-1]+1,len[u]+1);u=ch[u][c];
    }
    for(int j=1;j<=18;j++)
        for(int i=1;i+(1<<j)-1<=n;i++)
            st[j][i]=max(st[j-1][i],st[j-1][i+(1<<j-1)]);
    while(q--)
    {
        int ql,qr;scanf("%d%d",&ql,&qr);
        int l=1,r=qr-ql+1,res=0;
        while(l<=r)
        {
            int mid=l+r>>1;
            if(qmx(ql+mid-1,qr)>=mid) res=mid,l=mid+1;
            else r=mid-1;
        }
        cout<<res<<'\n';
    }
}

CF700E Cool Slogans

感觉非常简单啊,不知道怎么评的 3300。

剪出 SAM,考虑 parent tree 上的一个结点 \(v\)

定义 \(S_v\) 表示结点 \(v\) 的状态里任意一个串。

对于一个结点 \(u\),考虑什么情况下 \(S_u\)\(S_v\) 里出现了两次。

  • \(u\)\(v\) 的祖先结点。

  • 对于 \(v\) 里的任意一个 endpos,记为 \(p_v\),那么 \(u\) 的 endpos 集合里一定包含至少一个 \([p_v-len_v+len_u,p_v-1]\) 里的数。判断这个用线段树合并。

解释一下为什么任意一个 endpos,因为 \(S_u\)\(S_v\) 的一个后缀。

\(f_u\) 表示结点 \(u\) 的答案,树上 dp 即可。

但注意到 \(u\) 可能会从祖先转移过来,那么我们再设 \(g_u\) 表示 \(u\) 的一个深度最小的祖先,满足 \(f_u=f_{g_u}\)

如果满足条件,则 \(f_v=f_{g_u}+1,g_v=v\)

否则 \(f_v=f_u,g_v=g_u\)

#include<bits/stdc++.h>
using namespace std;

const int N=4e5+5;
char s[N];
int n,ans=1,lst=1,tot=1,ch[N][26],len[N],pos[N],fa[N],f[N],g[N];
vector<int> G[N];

void ins(int c,int i)
{
    int p=lst,q=lst=++tot;len[q]=len[p]+1,pos[q]=i;
    while(p&&!ch[p][c]) ch[p][c]=q,p=fa[p];
    if(!p) fa[q]=1;
    else
    {
        int x=ch[p][c];
        if(len[p]+1==len[x]) fa[q]=x;
        else
        {
            int y=++tot;len[y]=len[p]+1,pos[y]=i;
            for(int i=0;i<26;i++) ch[y][i]=ch[x][i];
            while(p&&ch[p][c]==x) ch[p][c]=y,p=fa[p];
            fa[y]=fa[x],fa[x]=fa[q]=y;
        }
    }
}

#define mid (l+r>>1)
int Tcnt,rt[N],lc[N*40],rc[N*40],sum[N*40];
void upd(int &k,int x,int l=1,int r=n)
{
    if(!k) k=++Tcnt;
    sum[k]++;
    if(l==r) return;
    x<=mid?upd(lc[k],x,l,mid):upd(rc[k],x,mid+1,r);
}
int merge(int p,int q,int l=1,int r=n)
{
    if(!p||!q) return p+q;
    int x=++Tcnt;sum[x]=sum[p]+sum[q];
    if(l==r) return x;
    lc[x]=merge(lc[p],lc[q],l,mid);
    rc[x]=merge(rc[p],rc[q],mid+1,r);
    return x;
}
int qsum(int k,int x,int y,int l=1,int r=n)
{
    if(!k) return 0;
    if(l>=x&&r<=y) return sum[k];
    if(x<=mid&&qsum(lc[k],x,y,l,mid)) return 1;
    if(mid<y&&qsum(rc[k],x,y,mid+1,r)) return 1;
    return 0;
}

void dfs(int u) {for(int v:G[u]) dfs(v),rt[u]=merge(rt[u],rt[v]);}
void dp(int u)
{
    for(int v:G[u])
    {
        if(u==1) {f[v]=1,g[v]=v,dp(v);continue;}
        if(qsum(rt[g[u]],pos[v]-len[v]+len[g[u]],pos[v]-1)) f[v]=f[u]+1,g[v]=v;
        else f[v]=f[u],g[v]=g[u];
        ans=max(ans,f[v]);dp(v);
    }
}


int main()
{
    scanf("%d%s",&n,s+1);
    for(int i=1;i<=n;i++) ins(s[i]-'a',i),upd(rt[lst],i);
    for(int i=2;i<=tot;i++) G[fa[i]].push_back(i);
    dfs(1);dp(1);
    cout<<ans<<'\n';
}

51nod1600 Simple KMP

刚刚讲了这个题,感觉讲的很烂。。。

\(F(S)\) 表示字符串 \(S\) 的答案,那么有:

\(F(S)=\sum\limits_{i=1}^{n}\sum\limits_{j=i+1}^{n}(\sum\limits_{k=i}^{j}\sum\limits_{x=1}^{k-i}[S[i,i+x-1]=S[k-x+1,k]])\)

考虑到这题有后缀自动机的标签,设 \(T\)\(S\) 的一个前缀,长度为 \(m\)\(G(T)\) 表示字符串 \(T\) 的所有后缀的贡献,那么有:

\(G(T)=\sum\limits_{i=1}^{m}\sum\limits_{x=1}^{m-i} [T[i,i+x-1]=T[m-x+1,m]]\)

枚举最上面那个式子的 \(j\)

\(F(S)=\sum\limits_{j=1}^{n}\sum\limits_{i=1}^{j} G(S[1...i])\)

求出 \(G\) 后前缀和一下即可。

考虑如何求原串的某个后缀 \(T\)\(G(T)\)

先对整个串建出 SAM,枚举当前后缀结尾 \(i\),那么所有以 \(i\) 结尾的后缀就是 parent tree 上的一条链。

考虑 \(S[1..i-1]\) 的所有子串的贡献。

对于 parent tree 上一个结点 \(x\),贡献为 \((len_x-len_{fa_x})\times |endpos_x|\)

算完后插入 \(i\) 的贡献就是把这条链上所有节结点的 endpos 集合大小 +1。

树剖维护即可。

#include<bits/stdc++.h>
#define ll long long
using namespace std;

const int N=2e5+5,mod=1e9+7;
char s[N];
ll f[N];
int n,lst=1,tot=1,id[N],ch[N][26],len[N],fa[N];
vector<int> G[N];

int ins(int c)
{
    int p=lst,q=lst=++tot;len[q]=len[p]+1;
    while(p&&!ch[p][c]) ch[p][c]=q,p=fa[p];
    if(!p) fa[q]=1;
    else
    {
        int x=ch[p][c];
        if(len[p]+1==len[x]) fa[q]=x;
        else
        {
            int y=++tot;len[y]=len[p]+1;
            for(int i=0;i<26;i++) ch[y][i]=ch[x][i];
            while(p&&ch[p][c]==x) ch[p][c]=y,p=fa[p];
            fa[y]=fa[x],fa[x]=fa[q]=y;
        }
    }
    return q;
}

int dn,sz[N],son[N],top[N],dfn[N],rev[N];
void dfs1(int u)
{
    sz[u]=1;
    for(int v:G[u])
    {
        dfs1(v);
        sz[u]+=sz[v];
        if(sz[v]>sz[son[u]]) son[u]=v;
    }
}
void dfs2(int u,int topf)
{
    dfn[u]=++dn,rev[dn]=u,top[u]=topf;
    if(son[u]) dfs2(son[u],topf);
    for(int v:G[u]) if(v!=son[u]) dfs2(v,v);
}

#define lc (k<<1)
#define rc (k<<1|1)
#define mid (l+r>>1)
ll sum[N*4];
int val[N*4],add[N*4];
void pushdown(int k)
{
    if(!add[k]) return;
    add[lc]+=add[k],add[rc]+=add[k];
    sum[lc]+=1ll*val[lc]*add[k],sum[rc]+=1ll*val[rc]*add[k];
    sum[lc]%=mod,sum[rc]%=mod;
    add[k]=0;
}
void build(int k=1,int l=1,int r=tot)
{
    if(l==r) {val[k]=len[rev[l]]-len[fa[rev[l]]];return;}
    build(lc,l,mid),build(rc,mid+1,r);
    val[k]=(val[lc]+val[rc])%mod;
}
int ask(int x,int y,int k=1,int l=1,int r=tot)
{
    if(l>=x&&r<=y)
    {
        int t=sum[k];
        add[k]++;sum[k]=(sum[k]+val[k])%mod;
        return t;
    }
    pushdown(k);
    int res=0;
    if(x<=mid) res+=ask(x,y,lc,l,mid);
    if(mid<y) res=(res+ask(x,y,rc,mid+1,r))%mod;
    sum[k]=sum[lc]+sum[rc];
    return res;
}

int upd(int x)
{
    int res=0;
    while(top[x]!=1) res=(res+ask(dfn[top[x]],dfn[x]))%mod,x=fa[top[x]];
    return (res+ask(1,dfn[x]))%mod;
}

int main()
{
    scanf("%d%s",&n,s+1);
    for(int i=1;i<=n;i++) id[i]=ins(s[i]-'a');
    for(int i=2;i<=tot;i++) G[fa[i]].push_back(i);
    dfs1(1),dfs2(1,1);build();
    for(int i=1;i<=n;i++) f[i]=(f[i-1]+upd(id[i]))%mod;
    for(int i=1;i<=n;i++)
    {
        f[i]=(f[i-1]+f[i])%mod;
        cout<<f[i]<<'\n';
    }
}

P6292 区间本质不同子串个数

先建出 parent tree。

扫描线,把询问挂到右端点。

从左到右插入字符,对每一个本质不同字串 \(T\) 维护一个 \(pos\) 表示当前串最后一次出现的右端点。

那么对于一个长度为 \(T\) 的串,会让 \(pos_T-|T|+1\) 这个位置的答案 \(+1\)

考虑每次右端点往右移,相当于把 parent tree 上一条链的 \(pos\) 更新为 \(i\),并且更改后,\(pos\) 相同的所有字符串的长度是一个区间。

可以用 LCT 维护这个过程,每一个 splay 维护一个相同的 \(pos\)。那么插入相当于 access 操作。假设当前节点为 \(k\),由于我们进行了 \(splay(k)\),所以 \(pos_k\neq pos_{fa_k}\)。那么以当前 \(pos\) 结尾的,且在修改链上的,字符串长度区间为 \([len_{fa_k}+1,len_k]\),也就是还要维护一个区间修改操作。

对于询问直接查询 \([l,i]\) 的答案即可。随便用一个数据结构维护区间修改区间查。

复杂度 \(O(n\log ^2 n)\)

#include<bits/stdc++.h>
#define ll long long
using namespace std;

const int N=2e5+5;
int n,m;
ll ans[N];
char s[N];
struct node{int l,id;};
vector<node> q[N];

int tot=1,lst=1,id[N],fa[N],len[N],Ch[N][26];
int ins(int c)
{
    int p=lst,q=lst=++tot;len[q]=len[p]+1;
    while(p&&!Ch[p][c]) Ch[p][c]=q,p=fa[p];
    if(!p) fa[q]=1;
    else
    {
        int x=Ch[p][c];
        if(len[p]+1==len[x]) fa[q]=x;
        else
        {
            int y=++tot;len[y]=len[p]+1;
            for(int i=0;i<26;i++) Ch[y][i]=Ch[x][i];
            while(p&&Ch[p][c]==x) Ch[p][c]=y,p=fa[p];
            fa[y]=fa[x],fa[x]=fa[q]=y;
        }
    }
    return q;
}

struct BIT{
    ll c1[N],c2[N];
    void upd(int x,int v) {for(int V=x*v;x<=n;x+=x&-x) c1[x]+=v,c2[x]+=V;}
    ll qsum(int x) {ll s1=0,s2=0;for(int y=x;y;y^=y&-y) s1+=c1[y],s2+=c2[y];return (x+1)*s1-s2;}
    void upd(int l,int r,int v) {upd(l,v),upd(r+1,-v);}
    ll qsum(int l,int r) {return qsum(r)-qsum(l-1);}
}tr;

#define lc(k) ch[k][0]
#define rc(k) ch[k][1]
int top,ch[N][2],val[N],stk[N];
bool isroot(int k) {return lc(fa[k])!=k&&rc(fa[k])!=k;}
bool getson(int k) {return rc(fa[k])==k;}
void pushdn(int k) {if(val[k]) val[lc(k)]=val[rc(k)]=val[k];}
void rotate(int k)
{
    int f=fa[k],g=fa[f],p=getson(k),s=ch[k][p^1];
    fa[k]=g;
    if(!isroot(f)) ch[g][getson(f)]=k;
    fa[s]=f,ch[f][p]=s;
    fa[f]=k,ch[k][p^1]=f;
}
void splay(int k)
{
    stk[top=1]=k;
    for(int x=k;!isroot(x);) stk[++top]=(x=fa[x]);
    while(top) pushdn(stk[top--]);
    while(!isroot(k))
    {
        int f=fa[k];
        if(!isroot(f)) rotate(getson(f)==getson(k)?f:k);
        rotate(k);
    }
}
void access(int k,int v)
{
    int i=0;
    for(;k;i=k,k=fa[k])
    {
        splay(k);
        if(val[k]) tr.upd(val[k]-len[k]+1,val[k]-len[fa[k]],-1);
        rc(k)=i;
    }
    val[i]=v;
    tr.upd(1,v,1);
}

int main()
{
    scanf("%s%d",s+1,&m);
    n=strlen(s+1);
    for(int i=1;i<=n;i++) id[i]=ins(s[i]-'a');
    for(int i=1;i<=m;i++)
    {
        int l,r;scanf("%d%d",&l,&r);
        q[r].push_back({l,i});
    }
    for(int i=1;i<=n;i++)
    {
        access(id[i],i);
        for(auto [l,id]:q[i]) ans[id]=tr.qsum(l,i);
    }
    for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
}
posted @ 2023-12-05 20:25  spider_oyster  阅读(11)  评论(0编辑  收藏  举报