字符串做题记录

[Codechef REBXOR] Nikitosh and xor

解法

由于异或满足 \(a\oplus a=0\),故可以将 \(\bigoplus_{i=l}^r a_i\) 写成 \((\bigoplus_{i=1}^r a_i)\oplus (\bigoplus_{i=1}^{l-1} a_i)\) 的形式。维护前缀异或和,然后问题转换成了找出两对异或最大的异或和,满足两对异或和对应的区间不交。

使用 01-Trie 维护前缀/后缀的(前缀)异或和,在插入某个异或和后找异或其最大的异或和并将之计入答案,可得前缀/后缀异或和中异或最大的一对。合并两个答案即可。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=400010;
const int maxl=30;
int n,i,j,t=1,u,v,p,s,l,ans;
int a[maxn],pr[maxn];
int trie[maxn*maxl][2];
bool o;
int main(){
    scanf("%d",&n);
    for(t=1;t<=maxl;++t) trie[t][0]=t+1;
    --t;
    for(i=1;i<=n;++i){
        scanf("%d",a+i);
        a[i]^=a[i-1];
        u=p=1;
        for(j=maxl-1;j>=0;--j){
            o=(a[i]>>j)&1; v=trie[u][o];
            if(!v) v=trie[u][o]=++t; 
            u=v; v=trie[p][!o];
            if(v){
                p=v;
                pr[i]|=(1<<j);
            }
            else p=trie[p][o];
        }
        pr[i]=max(pr[i],pr[i-1]);
    }
    memset(trie,0,sizeof(trie));
    t=u=1;
    for(j=maxl-1;j>=0;--j){
        o=(a[n]>>j)&1;
        trie[u][o]=++t;
        u=t;
    }
    for(i=n-1;i;--i){
        u=p=1;s=0;
        for(j=maxl-1;j>=0;--j){
            o=(a[i]>>j)&1; v=trie[u][o];
            if(!v) v=trie[u][o]=++t; 
            u=v; v=trie[p][!o];
            if(v){
                p=v;
                s|=(1<<j);
            }
            else p=trie[p][o];
        }
        l=max(l,s);
        ans=max(ans,pr[i]+l);
    }
    printf("%d",ans);
    return 0;
}

P2375 [NOI2014] 动物园

解法

考虑满足同时为串 \(s\) 的前缀和后缀的(非空)子串有怎样的性质。

如果某个串 \(s\) 的前缀同时是其的后缀,则这个子串一定同时为 \(s_1\sim s_{nxt_n}\) 的前缀和后缀(或是其本身);而若其长度小于 \(nxt_n\),则其长度最多为 \(nxt_{nxt_n}\),其又是 \(s_1\sim s_{nxt_{nxt_n}}\) 的前缀和后缀;以此类推,可得这个子串的长度一定在 \(\{n,nxt_n,nxt_{nxt_n},\cdots,1\}\)集合中。

这样来说单独求一个 \(num_i\) 也是简单的,直接不断跳 \(nxt\) 直到跳到得一个长度不大于 \(\lfloor\frac i2\rfloor\) 处。而求单个 \(num\) 的时间复杂度为 \(O(n)\);需要在能够保证的时间复杂度内求出每一个 \(num\),可以使用倍增法,一次性跳 \(2^k\)\(nxt\) 再进行检查,这样单次求 \(num\) 的时间复杂度为 \(O(\log n)\)

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxl=22;
const int maxn=1000010;
const int md=1000000007;
int t,n,i,j,k,a,p,m;
int dep[maxn],nxt[maxl][maxn];
char s[maxn];
int main(){
    scanf("%d",&t);
    dep[0]=-1;
    while(t--){
        scanf("%s",s+1);
        n=strlen(s+1);j=0;
        for(i=2;i<=n;++i){
            while(j&&(s[j+1]!=s[i])) j=nxt[0][j];
            if(s[j+1]==s[i]) nxt[0][i]=++j;
            else nxt[0][i]=0; 
            dep[i]=dep[nxt[0][i]]+1;
        }
        for(i=1;i<maxl;++i) for(j=1;j<=n;++j) nxt[i][j]=nxt[i-1][nxt[i-1][j]];
        k=1;a=1;
        for(i=2;i<=n;++i){
            p=i;m=0;
            for(j=maxl-1;j>=0;--j){
                if(nxt[j][p]>k){
                    m+=(1<<j);
                    p=nxt[j][p];
                }
            }
            m=dep[i]-m+1;
            a=(1LL*a*m)%md;
            if(i&1) ++k;
        }
        printf("%d\n",a);
    }
    return 0;
}

P2414 [NOI2011] 阿狸的打字机

解法

考虑某个串 \(S\) 在另外一个串 \(T\) 中出现多少次意味着串 \(T\) 中有多少个前缀的后缀是 \(S\)。对应在 AC 自动机上即为 \(S\) 对应的节点在 next 树上有多少个后代在 \(T\) 对应的 Trie 树链上。

把所有询问离线,同时将某个点是否在另一个点的子树上转为某个点是否在某段 dfn 序的一个区间里。这样可以先 dfs 一遍 next 树求 dfn 序,然后 dfs 一遍 Trie 树,在进入某个节点时维护当前节点和其祖先节点的所有 dfn 序(可以用树状数组);最后当某个节点 \(j\) 对应了某个询问 \((i,j)\) 时,查询当前维护的所有 dfn 序中有多少个在 \(i\) 对应的 dfn 序区间内。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=100010;
char s[maxn];
int i,p,c,j,l,r,u,v,m,tot,tim;
int fa[maxn],q[maxn],nxt[maxn],ed[maxn];
int ac[maxn][26],trie[maxn][26];
int h[maxn],nxe[maxn],dfn[maxn],edn[maxn];
int cnt[maxn<<1],hq[maxn],aq[maxn];
struct query{int to,nq;}Q[maxn];
inline void add(int p){
    for(;p<=tim;p+=(p&-p)) ++cnt[p];
}
inline void sub(int p){
    for(;p<=tim;p+=(p&-p)) --cnt[p];
}
inline int query(int p){
    int ret=0;
    for(;p;p^=(p&-p)) ret+=cnt[p];
    return ret;
}
void dfsn(int p){
    dfn[p]=++tim;
    for(int lp=h[p];lp;lp=nxe[lp]) dfsn(lp);
    edn[p]=++tim;
} 
void dfsa(int p){
    add(dfn[p]);
    int lp,to;
    for(lp=hq[p];lp;lp=Q[lp].nq){
        to=Q[lp].to;
        aq[lp]=query(edn[to])-query(dfn[to]-1);
    }
    for(lp=0;lp<26;++lp){
        to=trie[p][lp];
        if(!to) continue;
        dfsa(to);
    }
    sub(dfn[p]);
}
int main(){
    scanf("%s",s+1);
    for(i=1;s[i];++i){
        c=s[i];
        if(c=='B') p=fa[p];
        else if(c=='P') ed[++j]=p;
        else{
            c-='a';
            if(!trie[p][c]){
                trie[p][c]=ac[p][c]=++tot;
                fa[tot]=p;
            }
            p=trie[p][c];
        }
    }
    r=-1;
    for(i=0;i<26;++i) if(ac[0][i]) q[++r]=ac[0][i];
    while(l<=r){
        u=q[l++];
        for(i=0;i<26;++i){
            v=ac[u][i];
            if(!v) ac[u][i]=ac[nxt[u]][i];
            else{
                nxt[v]=ac[nxt[u]][i];
                q[++r]=v;
            }
        }
    }
    for(i=1;i<=tot;++i){
        nxe[i]=h[nxt[i]];
        h[nxt[i]]=i;
    }
    dfsn(0);
    scanf("%d",&m);
    for(i=1;i<=m;++i){
        scanf("%d%d",&u,&v);
        u=ed[u]; v=ed[v];
        Q[i]={u,hq[v]};hq[v]=i;
    }
    dfsa(0);
    for(i=1;i<=m;++i) printf("%d\n",aq[i]);
    return 0;
}

P3975 [TJOI2015]弦论

解法

对这个串建立 SAM。由于 SAM 上从起点出发的任意一条路径均对应了唯一的子串,所以每个点出发的路径条数就是有某个前缀的子串个数。在 SAM 的转移边组成的 DAG 上拓扑 dp 找到从每个点出发的路径条数。

考虑可重子串的计数。在求出每个节点对应的 endpos 之后,计算某个点开始的子串个数时,不能仅仅将这个点出发的路径条数相加。在 DAG 上从某个点向其前驱转移时,需要加上这个点的 endpos 大小(考虑这个前驱节点只加这一条转移边对应的字符的状态,此时需要取这个节点)。从根节点开始时不能减去对应大小(空串不计入排序)。

最后需要在 SAM 的 DAG 上 dfs 以按位确定答案的前缀。在 dfs 搜索到某个节点时,需要将 \(k\) 减去这个节点对应的 endpos(不可重计数则减 1),表示这个对应的前缀对答案造成的贡献,若此时 \(k\) 为负则已经求出答案。同时在节点上顺次访问转移边,将 \(k\) 减去目前扫描到的所有转移边对应的点的后缀数,直到 \(k\) 将在这一次操作时变为负,才不进行这次操作,而是确定答案当前位,并且访问这个子节点。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=500010;
const int maxl=1000010;
char s[maxn];
int o,k,i,j,u,v,l=1,r,te;
int deg[maxl],que[maxl];
int h[maxl],nxt[maxn*3],ver[maxn*3];
int p,q,cl,ch;
int cur,lst=1,tot=1;
struct SamNode{
    int lnk,len,siz;
    int nxt[26];
    long long str,sum;
}N[maxl];
#define lnk(p) N[p].lnk
#define len(p) N[p].len
#define siz(p) N[p].siz
#define nxt(p) N[p].nxt
#define str(p) N[p].str
#define sum(p) N[p].sum
int main(){
    scanf("%s",s+1);
    N[1].str=1;
    for(i=1;s[i];++i){
        ch=s[i]-'a';
        cur=++tot;
        p=lst;
        siz(cur)=1;
        len(cur)=i;
        while(p){
            if(nxt(p)[ch]) break;
            nxt(p)[ch]=cur;
            p=lnk(p);
        }
        if(!p) lnk(cur)=1;
        else{
            q=nxt(p)[ch];
            if(len(q)==len(p)+1) lnk(cur)=q;
            else{
                N[cl=++tot]=N[q];
                len(cl)=len(p)+1;
                siz(cl)=0;
                lnk(q)=lnk(cur)=cl;
                while(p&&nxt(p)[ch]==q){
                    nxt(p)[ch]=cl;
                    p=lnk(p);
                }
            }
        }
        lst=cur;
    }
    scanf("%d%d",&o,&k);
    if(!o) for(i=1;i<=tot;++i) siz(i)=1;
    else{
        for(i=2;i<=tot;++i) ++deg[lnk(i)];
        for(i=2;i<=tot;++i) if(!deg[i]) que[++r]=i;
        while(l<=r){
            u=que[l++]; v=lnk(u);
            siz(v)+=siz(u);
            if(!(--deg[v])) que[++r]=v;
        }
    }
    for(i=1;i<=tot;++i) str(i)=siz(i);
    str(1)=siz(1)=r=0;l=1;
    for(i=1;i<=tot;++i){
        for(j=0;j<26;++j){
            if(nxt(i)[j]){
                ++deg[i];
                ver[++te]=i;
                nxt[te]=h[nxt(i)[j]];
                h[nxt(i)[j]]=te;
            }
        }
    }
    for(i=1;i<=tot;++i) if(!deg[i]) que[++r]=i;
    while(l<=r){
        u=que[l++];
        for(i=h[u];i;i=nxt[i]){
            v=ver[i]; str(v)+=str(u);
            if(!(--deg[v])) que[++r]=v;
        }
    }
    if(k>str(1)){
        printf("-1");
        return 0;
    }
    u=1;
    for(;;){
        if(k<=siz(u)) return 0;
        k-=siz(u);
        for(i=0;i<26;++i){
            v=nxt(u)[i];
            if(!v) continue;
            if(str(v)<k) k-=str(v);
            else{
                putchar(i+'a');
                u=v; break;
            }
        }
    }
}

P3193 [HNOI2008]GT考试

解法

\(n\) 比较小时考虑 dp。设 \(dp_{i,j}\) 为当前确定了长为 \(i\) 的前缀,这个前缀的后缀与 \(s\) 最多能匹配 \(j\) 位的方案数。转移时,考虑在 \(s_1\sim s_j\) 后接某个数字 \(c\) 能够和 \(s\) 最多匹配到多少位,记最多能匹配到 \(nxt_{j,c}\) 位(这个值可以通过 KMP 求得的 \(nxt\) 数组求出)。同时记 \(M(j,k)=\sum_{i=0}^9[nxt_{j,i}=k]\),则 \(dp_{i+1,j}=\sum_{k=0}^{m-1} M(k,j)dp_{i,k}\)。 初值有 \(dp_{1,0}=9,dp_{1,1}=1\)

考虑矩阵乘法。定义 \(m\times m\) 的转移矩阵 \(T\),其中 \(T_{i,j}=M(i,j)\)(下标从 \(0\) 开始);则由 dp 定义式得:\(\begin{bmatrix}dp_{i,0}&dp_{i,1}&\cdots&dp_{i,m-1}\end{bmatrix}\times T=\begin{bmatrix}dp_{i+1,0}&dp_{i+1,1}&\cdots&dp_{i+1,m-1}\end{bmatrix}\)。求 \(T^{n-1}\) 即可。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxm=22;
int n,m,k,i,j,p,c,ans;
int nxt[maxm];
char s[maxm];
struct matrix{
    int mat[maxm][maxm];
    inline void operator *=(const matrix &a){
        matrix tmp;
        for(i=0;i<m;++i){
            for(j=0;j<m;++j){
                for(p=0;p<m;++p) c+=mat[i][p]*a.mat[p][j];
                tmp.mat[i][j]=c%k; c=0;
            }
        }
        *this=tmp;
    }
}d,r;
int main(){
    scanf("%d%d%d%s",&n,&m,&k,s+1); --n;
    d.mat[0][1]=d.mat[1][2]=
    r.mat[0][0]=r.mat[1][1]=1; 
    d.mat[0][0]=9; 
    if(s[1]==s[2]) d.mat[1][0]=9;
    else{
        d.mat[1][0]=8;
        d.mat[1][1]=1;
    }
    for(i=2;i<m;++i){
        while(j&&s[j+1]!=s[i]) j=nxt[j];
        if(s[j+1]==s[i]) ++j; nxt[i]=j;
        p=i; c=10; r.mat[i][i]=1; 
        for(;;){
            if(!((ans>>(s[p+1]-'0'))&1)){
                d.mat[i][p+1]=1;
                ans|=1<<(s[p+1]-'0');
                --c;
            }
            if(!p) break; 
            p=nxt[p]; 
        } 
        ans=0; d.mat[i][0]=c;
    }
    c=0;
    do{
        if(n&1) r*=d;
        d*=d;
    }while(n>>=1);
    ans=r.mat[0][0]*9;
    if(m>1){
        ans+=r.mat[1][0];
        for(i=1;i<m;++i) ans+=r.mat[1][i]+9*r.mat[0][i];
    }
    printf("%d",ans%k);
    return 0;
}

P3715 [BJOI2017]魔法咒语

解法

考虑 dp。建出所有非法串的 AC 自动机之后,则每个非法串的末尾节点和它们在 fail 树上的祖先都是在匹配时不能经过的节点。设 \(dp_{i,j}\) 为长为 \(i\) 且在自动机上匹配到节点 \(j\) 的字符串数量,转移时设 \(to_{i,j}\) 为基本串 \(s_i\) 在自动机上从 \(j\) 开始能匹配到的节点(可能在中间走过了非法节点,此时 \(to_{i,j}\) 不存在),则转移有 \(\forall k,dp_{i,j}\to dp_{i+|s_k|,to_{k,j}}\)

考虑 \(L\) 很大而 \(\max\limits_{k=1}^n |s_k|\) 很小的情况。此时每个 \(dp_{i,j}\) 只需要借助 \(dp_{i-1},dp_{i-2}\) 计算,可以使用矩阵快速幂优化转移。注意 \(\max\limits_{k=1}^n|s_k|\) 较大时不能用矩阵。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=55;
const int maxl=110;
const int maxb=220;
const int md=1000000007;
int n,m,l,i,j,u,v,c,pt,tot;
int le[maxn],trie[maxl][26];
int nxt[maxl],to[maxn][maxl],dp[maxl][maxl];
char s[maxn][maxl],t[maxl];
bool vis[maxl]; queue<int> q;
inline void Add(int &x,int y){x-=((x+=y)>=md)*md;}
struct mat{
    int m[maxb][maxb];
    inline mat operator *(const mat &a){
        mat ret;
        for(int i=0;i<=v;++i){
            for(int j=0;j<=v;++j){
                int u=0;
                for(int k=0;k<=v;++k)
                    u=(1LL*m[i][k]*a.m[k][j]+u)%md;
                ret.m[i][j]=u;
            }
        }
        return ret;
    }
}R,D; 
int main(){
    scanf("%d%d%d",&n,&m,&l);
    for(i=1;i<=n;++i){
        scanf("%s",s[i]+1);
        le[i]=strlen(s[i]+1);
    }
    for(i=1;i<=m;++i){
        scanf("%s",t+1); pt=0;
        for(j=1;c=t[j];++j){
            c-='a';
            if(!trie[pt][c])
                trie[pt][c]=++tot;
            pt=trie[pt][c];
        }
        vis[pt]=1;
    }
    for(j=0;j<26;++j) if(u=trie[0][j]) q.push(u);
    while(!q.empty()){
        u=q.front(); q.pop();
        for(j=0;j<26;++j){
            pt=trie[nxt[u]][j];
            if(!(v=trie[u][j]))
                trie[u][j]=pt;
            else{
                nxt[v]=pt;
                q.push(v);
            }
        }
    }
    for(i=0;i<=tot;++i)
        for(j=0;j<=tot;++j)
            vis[j]|=vis[nxt[j]];
    for(i=1;i<=n;++i){
        for(u=0;u<=tot;++u){
            if(vis[u]){
                to[i][u]=-1;
                continue;
            }
            pt=u;
            for(j=1;c=s[i][j];++j){
                pt=trie[pt][c-'a'];
                if(vis[pt]){pt=-1;break;}
            }
            to[i][u]=pt;
        }
    }
    if(l>1){
        c=*max_element(le+1,le+n+1); 
        v=tot<<1|1;
        for(j=0;j<=tot;++j) D.m[j][tot+j+1]=1;
        for(j=0;j<=v;++j) R.m[j][j]=1;
        for(i=1;i<=n;++i){
            u=(le[i]-1)*(tot+1); 
            for(j=0;j<=tot;++j)
                if(~to[i][j])
                    ++D.m[j+u][to[i][j]];
        }
        ++i; 
        do{
            if(l&1) R=R*D;
            D=D*D;
        }while(l>>=1);
        for(i=u=0;i<=tot;++i) Add(u,R.m[0][i]);
    }
    else{
        dp[0][0]=1;
        for(i=0;i<l;++i)
            for(u=0;u<=tot;++u)
                for(j=1;j<=n;++j)
                    if((c=i+le[j])<=l&&(~(v=to[j][u]))) 
                        Add(dp[c][to[j][u]],dp[i][u]);
        for(i=u=0;i<=tot;++i) Add(u,dp[l][i]);
    }
    printf("%d",u);
    return 0;
}

P5840 [COCI2015]Divljak

解法

将所有给出串建 AC 自动机后,考虑在新加一个串时将这个串在 AC 自动机上面进行匹配,标记经过的节点,每次查询多少个串包含了某个串 \(s_i\),就是哪些串匹配时访问到 \(s_i\) 结尾节点在 fail 树上的子树内。

考虑访问到的节点是否在某个节点的子树内可以想到 dfn + 树状数组,标记某些节点和它们在 fail 树上的节点的祖先的并也是一个经典问题,只需要将所有访问节点离线下来,按照 dfn 排序,设排序后的节点为 \(u_1,u_2,\cdots\),则 \(\forall i>1\),每次新标记 \(son_{{\rm{lca}}(u_{i-1},u_i)}\to u_i\) 上的点即可。这个问题可以转成链上加 + 单点求和的形式,可以转成单点加 + 子树求和(dfn 上区间求和)的形式。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxl=22;
const int maxm=100010;
const int maxn=2000010;
const int maxb=maxn<<1;
int n,m,i,j,u,v,o,p=1,ch,tot,tim;
int trie[maxn][26],nxt[maxn];
int dep[maxn],dfn[maxn],edn[maxn];
int lb[maxb],st[maxl][maxb],to[maxm];
int he[maxn],ne[maxn],c[maxb],pe[maxn];
char s[maxn]; queue<int> q;
void dfs(int p,int d){
    dep[p]=d; 
    st[0][++tim]=p; dfn[p]=tim;
    for(int to=he[p];to;to=ne[to]){
        dfs(to,d+1);
        st[0][++tim]=p;
    }
    edn[p]=tim;
}
inline bool cmp(const int &x,const int &y){return dfn[x]<dfn[y];}
inline int lca(int x,int y){
    x=dfn[x]; y=dfn[y]; if(x>y) swap(x,y);
    int le=lb[y-x+1];
    int u=st[le][x],v=st[le][y-(1<<le)+1];
    if(dep[u]>dep[v]) u=v; return u; 
}
inline void Add(int p,int x){
    for(;p<=tim;p+=p&-p) c[p]+=x;
}
inline void Que(){
    int ret=0,l=dfn[u]-1,r=edn[u];
    for(;r;r^=r&-r) ret+=c[r];
    for(;l;l^=l&-l) ret-=c[l];
    printf("%d\n",ret);
}
int main(){
    for(i=2;i<maxb;++i) lb[i]=lb[i>>1]+1;
    scanf("%d",&n);
    for(i=1;i<=n;++i){
        scanf("%s",s+1); p=0;
        for(j=1;ch=s[j];++j){
            ch-='a';
            if(!trie[p][ch])
                trie[p][ch]=++tot;
            p=trie[p][ch];
        }
        to[i]=p;
    }
    for(j=0;j<26;++j) if(u=trie[0][j]) q.push(u);
    while(!q.empty()){
        u=q.front(); q.pop();
        for(j=0;j<26;++j){
            p=trie[nxt[u]][j];
            if(v=trie[u][j]){
                nxt[v]=p;
                q.push(v);
            }
            else trie[u][j]=p;
        }
    }
    for(i=1;i<=tot;++i){
        u=nxt[i];
        ne[i]=he[u];
        he[u]=i;
    }
    dfs(0,1);
    for(j=1;j<maxl;++j){
        m=tim-(1<<j)+1;
        for(i=1;i<=m;++i){
            u=st[j-1][i];
            v=st[j-1][i+(1<<(j-1))];
            if(dep[u]>dep[v]) u=v;
            st[j][i]=u;
        }
    }
    scanf("%d",&m);
    while(m--){
        scanf("%d",&o);
        if(o==1){
            scanf("%s",s+1); 
            for(n=1;ch=s[n];++n)
                pe[n]=trie[pe[n-1]][ch-'a'];
            sort(pe+1,pe+n,cmp); 
            Add(dfn[v=pe[1]],1);
            for(i=2;i<n;++i){
                u=pe[i];
                Add(dfn[lca(u,v)],-1);
                Add(dfn[u],1); v=u;
            }
        }
        else{
            scanf("%d",&u);
            u=to[u]; Que();
        }
    }
    return 0;
}

P2336 [SCOI2012]喵星球上的点名

解法

考虑第一问中“某个串作为了多少个给定串的子串”的内容。可以对所有点名串建 AC 自动机,然后将每个名字串在自动机上进行匹配,可以按照上述 P5840 [COCI2015]Divljak 的方法统计答案。

第二问中“某个串有多少个给定串作为子串”,就是将名字串在 AC 自动机上匹配的时候,经过的所有点到 fail 树上的根节点的路径并内有多少个节点为给定串的末端节点。可以直接按照上述统计方法 + 树上差分统计答案。

在建 AC 自动机的时候,转移边数量可能很多,不能直接用 map 存储,可以使用 map 预处理 Trie 树上原有的转移边方便处理,使用主席树维护每个点的转移边,在建某个节点 \(u\) 的转移边时只需要用 \(u\) 在 Trie 树上的转移边覆盖 \(fail_u\) 的转移边即可,该部分时空复杂度为 \(O(|{\rm{Trie}}|\log\Sigma)\),其中 \(\Sigma\) 为字符集大小。(似乎有其他的正确复杂度的做法)

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxl=20;
const int maxb=50010;
const int maxn=100010; 
const int INF=10000;
int n,m,i,j,u,v,p,a,tp,pt,lt,rt,mt,te=1,tim,tot;
int fa[maxl][maxn],he[maxn],ne[maxn];
int c[maxn],st[maxb],val[maxn],ans[maxb];
int rd[maxn],dep[maxn],dfn[maxn],edn[maxn];
int to[maxb],pe[maxb*3],ph[maxn],nxt[maxn];
inline bool cmp(const int &x,const int &y){return dfn[x]<dfn[y];}
queue<int> q; 
map<int,int> trie[maxn];
#define fi first
#define se second
struct seg{int ls,rs;}tr[maxn*maxl];
#define ls(p) tr[p].ls
#define rs(p) tr[p].rs
void dfs(int p,int d,int v){
    dep[p]=d; val[p]=v; dfn[p]=++tim;
    for(int to=he[p];to;to=ne[to])
        fa[0][to]=p,dfs(to,d+1,v+val[to]);
    edn[p]=tim;
}
inline int lca(int x,int y){
    if(dep[x]<dep[y]) swap(x,y);
    for(int j=maxl-1;~j;--j) if(dep[fa[j][x]]>=dep[y]) x=fa[j][x];
    if(x==y) return x;
    for(int j=maxl-1;~j;--j) if(fa[j][x]!=fa[j][y]) x=fa[j][x],y=fa[j][y];
    return fa[0][x];
}
inline void Add(int p,int x){
    for(;p<=tim;p+=p&-p) c[p]+=x;
}
inline void Que(){
    int ret=to[i];
    int l=dfn[ret]-1,r=edn[ret];
    for(ret=0;r;r^=r&-r) ret+=c[r];
    for(;l;l^=l&-l) ret-=c[l];
    printf("%d\n",ret);
}
int main(){
    scanf("%d%d",&n,&m);
    for(i=p=1;i<=n;++i){
        st[i]=p;scanf("%d",&u);
        while(u--){scanf("%d",&v);pe[p++]=v;}
        pe[p++]=-1;scanf("%d",&u);
        while(u--){scanf("%d",&v);pe[p++]=v;}
    }
    st[n+1]=p;
    for(i=1;i<=m;++i){
        scanf("%d",&u); p=0;
        while(u--){
            scanf("%d",&v);
            if(!trie[p][v]) trie[p][v]=++tot; 
            p=trie[p][v];
        }
        ++val[p]; to[i]=p;
    } 
    rd[0]=1;
    for(auto it:trie[0]){
        u=it.fi; v=it.se;
        lt=0; pt=1; rt=INF;
        while(lt<rt){
            mt=(lt+rt)>>1;
            if(u<=mt){
                if(!ls(pt)) ls(pt)=++te;
                pt=ls(pt); rt=mt;
            }
            else{
                if(!rs(pt)) rs(pt)=++te;
                pt=rs(pt); lt=mt+1;
            }
        }
        rs(pt)=v; q.push(v);
    }
    while(!q.empty()){
        u=q.front(); q.pop();
        rd[u]=++te; tr[te]=tr[p=rd[nxt[u]]];
        for(auto it:trie[u]){
            lt=0; rt=INF;
            pt=p; v=it.fi;
            while(lt<rt){
                mt=(lt+rt)>>1; 
                if(v<=mt) pt=ls(pt),rt=mt;
                else pt=rs(pt),lt=mt+1;
                if(!pt) break;
            }
            if(pt) nxt[it.se]=rs(pt);
            lt=0; rt=INF; pt=rd[u]; 
            while(lt<rt){
                mt=(lt+rt)>>1; ++te;
                if(v<=mt){
                    tr[te]=tr[ls(pt)]; ls(pt)=te;
                    pt=ls(pt); rt=mt;
                }
                else{
                    tr[te]=tr[rs(pt)]; rs(pt)=te;
                    pt=rs(pt); lt=mt+1; 
                }
            }
            v=rs(pt)=it.se; q.push(v);
        }
    }
    for(i=1;i<=tot;++i){
        u=nxt[i];
        ne[i]=he[u];
        he[u]=i;
    }
    dfs(0,1,0);
    for(j=1;j<maxl;++j)
        for(i=1;i<=tot;++i)
            fa[j][i]=fa[j-1][fa[j-1][i]];
    for(i=1;i<=n;++i){
        p=0; tp=1; u=st[i+1]; 
        for(j=st[i];j<u;++j){
            if((v=pe[j])<0){p=0;goto ed;}
            lt=0; rt=INF; pt=rd[p];
            while(lt<rt){
                mt=(lt+rt)>>1;
                if(v<=mt) pt=ls(pt),rt=mt;
                else pt=rs(pt),lt=mt+1;
                if(!pt) break;
            }
            if(pt) p=rs(pt); else p=0; 
            ed: ph[++tp]=p;
        }
        sort(ph+1,ph+tp+1,cmp); u=a=0;
        for(j=2;j<=tp;++j){
            v=ph[j]; p=lca(u,v);
            a+=val[v]-val[p];
            Add(dfn[p],-1);
            Add(dfn[v],1); u=v;
        }
        ans[i]=a;
    }
    for(i=1;i<=m;++i) Que();
    for(i=1;i<=n;++i) printf("%d ",ans[i]);
    return 0;
}

P3167 [CQOI2014]通配符匹配

解法

考虑将匹配串按照星号位置分成若干段,然后每一段分别和文件名进行匹配,每匹配好一段则下一段直接从该位置开始匹配(显然每一段越早完整匹配越好)。但是这样做显然行不通,因为在有通配符 ? 的情况下不能使用普通的 KMP 解决该问题。

考虑将匹配串按照问号一起分开。然后使用哈希判断某个位置能匹配到哪些位置。设 \(dp_{i,j}\)\(i\) 位置能否匹配到 \(j\) 部分串 \(s_j\) 的结尾处 的问号,则转移有 \(dp_{i,j}=(hash(i-|s_j|,i-1)=hash(s_j))dp_{i-|s_j|-1,j-1}\)。如果某个位置能够匹配完整,则及时从这一部分断掉开始匹配下一部分。然而分开之后有一些连续段不是以问号结尾的,需要特判掉,同时需要调整 dp 方式。通配符数量有限,所以可以这样进行 dp。

注意开头如果存在 * 则可以 \(\forall i,\)\(dp_{i,0}\) 的初值设为 \(1\);结尾如果存在 * 则可以将所有 \(dp_i\) 计入答案。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int P=1997;
const int maxk=12;
const int maxn=100010;
int n,m,i,j,k,p,c,u,v,l,ch,ct,th;
int col[maxk],le[maxk];
char s[maxn],t[maxn];
bool cd,dp[maxn][maxk],vis[maxk];
uint64_t H,pw[maxn],hs[maxk],ht[maxn];
int main(){
    for(i=pw[0]=1;i<maxn;++i) pw[i]=pw[i-1]*P;
    memset(col,-1,sizeof(col));
    scanf("%s",s+1);
    m=strlen(s+1); 
    for(i=1;i<=m;++i){
        ch=s[i]; 
        if(ch!='*') cd=1;
        if(!isalpha(ch)){
            if(cd){
                hs[++th]=H; 
                col[th]=c; 
                le[th]=ct;
                if(ch=='*'){++c;cd=0;} 
                else vis[th]=1;
            }
            H=ct=0; 
            continue;
        }
        H+=pw[ct++]*ch;
    }
    if(cd){
        hs[++th]=H; 
        col[th]=c; 
        le[th]=ct;
    } 
    scanf("%d",&n);
    while(n--){
        scanf("%s",t+1); p=strlen(t+1); 
        for(i=1;i<=p;++i) ht[i]=ht[i-1]+pw[i-1]*t[i];
        memset(dp,0,sizeof(dp)); 
        k=u=0; v=1; 
        if(s[1]=='*') for(i=0;i<=p;++i) dp[i][0]=1;
        else dp[0][0]=1; 
        for(;k<=c;++k){
            for(i=u+1;i<=p;++i){
                for(j=v;col[j]==k;++j){
                    if(vis[j]){
                        l=i-le[j];
                        if(l>u&&ht[i-1]-ht[l-1]==hs[j]*pw[l-1])
                            dp[i][j]|=dp[l-1][j-1];
                    }
                    else{
                        l=i-le[j]+1;
                        if(l>u&&ht[i]-ht[l-1]==hs[j]*pw[l-1])
                            dp[i][j]|=dp[l-1][j-1];
                    }
                }
                if(dp[i][j-1]&&k<c){u=i;break;}
            }
            if(i>p&&k<c){u=0;goto st;}
            if(k==c) break;
            while(col[v]==k) ++v;
            for(i=u;i<=p;++i) dp[i][v-1]=1;
        }
        u=dp[p][th]; 
        if(s[m]=='*') for(i=p;i;--i) u|=dp[i][th];
        st: if(u) printf("YES\n"); else printf("NO\n");
    }
    return 0;
}

DarkBzoj2741 【FOTILE模拟赛】L

解法

\(\bigoplus_{k=i}^j a_k\) 拆成 \((\bigoplus_{k=1}^{i-1} a_k)\oplus(\bigoplus_{k=1}^j a_k)\) 的形式,则问题转化成了求 \(\max_{l-1\le i<j\le r}\{(\bigoplus_{k=1}^i a_k)\oplus(\bigoplus_{k=1}^j a_k)\}\),也就是区间选两个数满足异或值最大。这个问题可以使用 01-Trie,每次加入一个数时查询与这个数异或最大的数,同时插入这个数,在 \(O(n\log a)\) 时间内解决。

考虑多组在线询问的情况,我们可以预处理其中一部分。

将序列(此时序列最左端需要插入 \(0\),表示空前缀)分成长为 \(S\) 的块,然后预处理从每一块开头到任意位置的答案,记 \(blk_{i,j}\) 为从第 \(i\) 块开头到 \(j\) 这个区间的任意两数最大异或和。此时在询问某个区间 \([l,r]\) 时(只讨论 \([l,r]\) 在不同块的情形,否则直接暴力 \(O(S)\) 求),先找出 \(l-1\) 之后的第一个整块(令其为第 \(i\) 个),此时需要查询 \(blk_{i,r}\) 和对应的 Trie,然后在这个 Trie 上暴力插入 \(l-1\) 到第 \(i\) 块开头前的所有异或和并查询与它们异或最大的数,然后更新答案。

查询某个序列的任意前缀对应的 Trie 可以用可持久化 Trie,然后查询从任意位置 \(l\) 开始的序列的任意前缀 \(l\sim r\) 可以把两个时刻的 Trie 相减(只统计 \(l\sim r\) 时刻更新的节点,在统计 \(r\) 时刻的 Trie 中部统计出现在 \(l\) 中的节点)。注意由于原题空间限制,不能暴力开 \(\boldsymbol{O(\frac nS)}\) 个可持久化 Trie。

时间复杂度为 \(O((\frac{n^2}S+mS)\log a)\),此处时间限制很宽,\(S\) 设为 \(\sqrt n\) 可以通过。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxl=32;
const int maxb=120;
const int maxn=12010;
int n,m,i,j,k,c,s,v,x,y,l,r,pt,px,tmp,tot,lst,ans;
bool f;
int a[maxn],rt[maxn],blk[maxb][maxn];
int tr[(maxn+maxb)*(maxl+3)][2];
int main(){
    scanf("%d%d",&n,&m);
    s=sqrt(n);
    for(i=1;i<=n;++i){
        scanf("%d",a+i);
        a[i]^=a[i-1];
    }
    for(i=0;i<=n;++i){
        pt=lst; rt[i]=++tot;
        tr[tot][0]=tr[lst][0];
        tr[tot][1]=tr[lst][1];
        lst=tot; v=a[i];
        for(k=maxl-1;k>=0;--k){
            f=(v>>k)&1; 
            ++tot; pt=tr[pt][f];
            tr[tot][0]=tr[pt][0];
            tr[tot][1]=tr[pt][1];
            tr[tot-1][f]=tot;
        }
    }
    for(i=c=0;i<=n;i+=s,++c){
        for(j=i;j<=n;++j){
            v=a[j]; 
            if(i) pt=rt[i-1];
            else pt=0;
            px=rt[j]; tmp=0;
            for(k=maxl-1;k>=0;--k){
                f=(v>>k)&1;
                if(tr[pt][!f]!=tr[px][!f]){
                    tmp|=1<<k;
                    pt=tr[pt][!f]; 
                    px=tr[px][!f];
                }
                else{
                    pt=tr[pt][f];
                    px=tr[px][f];
                }
            }
            blk[c][j]=ans=max(ans,tmp);
        }
        ans=0;
    }
    j=tot;
    while(m--){
        scanf("%d%d",&x,&y);
        l=(((long long)x)+ans)%n+1; 
        r=(((long long)y)+ans)%n+1;
        if(l>r) swap(l,r); 
        c=(--l)/s+1;
        y=min(r,c*s-1);
        ans=blk[c][r]; 
        lst=rt[r];
        for(i=l;i<=y;++i){
            v=a[i]; ++tot; tmp=0;
            tr[tot][0]=tr[lst][0];
            tr[tot][1]=tr[lst][1];
            x=px=lst; lst=tot;
            if(l) pt=rt[l-1];
            else pt=0;
            for(k=maxl-1;k>=0;--k){
                f=(v>>k)&1; 
                ++tot; x=tr[x][f];
                tr[tot][0]=tr[x][0];
                tr[tot][1]=tr[x][1];
                tr[tot-1][f]=tot; 
                if(tr[pt][!f]!=tr[px][!f]){
                    tmp|=1<<k;
                    pt=tr[pt][!f]; 
                    px=tr[px][!f];
                }
                else{
                    pt=tr[pt][f];
                    px=tr[px][f];
                }
            }
            ans=max(ans,tmp);
        }
        printf("%d\n",ans);
        tot=j;
    }
    return 0;
}
posted @ 2022-10-26 15:50  Fran-Cen  阅读(40)  评论(0编辑  收藏  举报