序列自动机

引入

给定一个长度为 \(n\) 的正整数序列 \(a\) ,有 \(q\) 次询问,第 \(i\) 次询问给定一个长度为 \(L_i\) 的序列 \(b_i\),请你判断 \(b_i\) 是不是 \(a\) 的子序列。序列 \(a\) 和所有 \(b_i\) 中的元素都不大于一个给定的正整数 \(m\)

\(1 \leq n, m, q \leq 10^5\)\(1 \leq a_i, b_{i, j} \leq m\)\(1 \leq l_i \leq 10^6\)\(\sum_{i = 1}^{q} l_i \leq 10^6\)

做法

考虑维护一个能够维护某个串的所有本质不同的子序列的自动机。


序列自动机

可能是最简单构造/理解的,附加内容最少的自动机。

构造

考虑贪心。对于一个串 \(s\),从某个位置开始维护子序列时,显然让每个字符均在 \(s\) 尽可能靠前的位置出现时,后面能够接的子序列数会尽量多。这样不会减少能够构造的数量的子序列数量。

\(son(i,c)\) 表示 \(s_i\) 之后第一个字符 \(c\) 出现的位置。将 \(son(i,c)\) 抽象成转移边,则所有的 \(son\) 转移边构成了自动机。而 \(son\) 的求法也很简单,逆推即可:

\[son(i,c)=\begin{cases}son(i+1,c)&s_{i+1}\ne c\\i+1&s_{i+1}=c\end{cases} \]

字符集较大时,可以用主席树维护转移边。

模板代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxl=18;
const int maxn=1000010;
int n,m,q,i,lt,rt,mt,pt,qt,tot;
int a[maxn];
struct node{
    int ls,rs,val;
}tr[maxn*maxl];
#define ls(p) tr[p].ls
#define rs(p) tr[p].rs
#define val(p) tr[p].val
int root[maxn];
int main(){
    scanf("%d",&n);
    scanf("%d%d%d",&n,&q,&m);
    for(i=1;i<=n;++i) scanf("%d",a+i);
    root[n]=tot=1;
    for(i=n;i;--i){
        pt=root[i-1]=++tot;
        lt=1;rt=m;qt=root[i];
        while(lt<rt){
            tr[pt]=tr[qt];
            mt=(lt+rt)>>1;
            if(a[i]<=mt){
                ls(pt)=++tot;
                rt=mt;qt=ls(qt);
            } 
            else{
                rs(pt)=++tot;
                lt=mt+1;qt=rs(qt);
            }
            pt=tot;
        }
        val(pt)=i;
    }
    while(q--){
        scanf("%d",&n);
        for(i=1;i<=n;++i) scanf("%d",a+i);
        pt=root[0];
        for(i=1;i<=n;++i){
            lt=1;rt=m;
            while(lt<rt){
                mt=(lt+rt)>>1;
                if(a[i]<=mt){
                    pt=ls(pt);
                    rt=mt;
                }
                else{
                    pt=rs(pt);
                    lt=mt+1;
                }
            }
            pt=val(pt);
            if(!pt){
                printf("No\n");
                break;
            }
            else pt=root[pt];
        }
        if(pt) printf("Yes\n");
    } 
    return 0;
}

例题:

给出字符串 \(S\),计算其本质不同的子序列数,答案取模。\(|S|\le 3\times 10^5\)

附加内容 1:无。

直接求自动机上不同路径数即可。

附加内容 2:求其 \(k\) 小子序列。\(k\le 10^{18}\)

附加内容 2.5\(k\le 10^9,|S|\le 10^6\)

在子序列自动机的 DAG 上找出从某个点出发的路径条数(根节点之外的点的路径条数要加一),大于 \(k\) 的直接赋为 \(\inf\)(打 \(\inf\) 标记),然后模拟 dfs 过程,按位填字符即可,方法可以参照 P3975 [TJOI2015]弦论 的做法。注意原题空间只有 125MB,故在 DAG 上 dp 找路径条数时,不应该建反图然后拓扑 dp,而是 dfs 以递归查找。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=1000010;
const int maxx=1000000000;
const int Inf=1000000001;
int n,k,i,j,l,r,u,v,tot;
int num[maxn],nxt[maxn][26];
char s[maxn];
bool vis[maxn];
void dfs(int p){
    if(vis[p]) return;
    vis[p]=1;
    int lp,to;
    for(lp=0;lp<26;++lp){
        to=nxt[p][lp];
        if(!to) continue;
        dfs(to);
        num[p]+=num[to];
        if(num[p]>maxx){
            num[p]=Inf;
            return;
        } 
    }
}
int main(){
    scanf("%d%d%s",&n,&k,s+1);
    for(i=n;i;--i){
        num[i]=1;
        for(j=25;j>=0;--j) nxt[i-1][j]=nxt[i][j];
        nxt[i-1][s[i]-'a']=i;
    }
    dfs(0);
    ++k;
    for(;;){
        if(k==1) return 0;
        --k;
        for(j=0;j<26;++j){
            v=nxt[u][j];
            if(!v) continue;
            if(k<=num[v]){
                u=v;
                putchar(j+'a');
                break;
            }
            else k-=num[v];
        }
    }
    return 0;
}

附加内容3:\(q\) 组询问,每次询问第 \(k\) 小子序列的最后 \(r\) 个字符。\(k\le 10^{18},q\le 10^5,\sum r\le 10^6\)

待补。

posted @ 2022-10-10 14:37  Fran-Cen  阅读(73)  评论(0编辑  收藏  举报