====== 序列自动机 ====== ===== 算法思想 ===== 序列自动机是接受且仅接受一个字符串的子序列的自动机。字符串设为 $s$ 。 若 $s$ 包含 $n$ 个字符,那么序列自动机包括 $n+1$ 个状态。 令 $t$ 是 $s$ 的一个子序列,则 $d(start,t)$ 是 $t$ 在 $s$ 第一次出现时末端的位置。 也就是说,一个状态 $i$ 表示前缀 $s[1…i]$ 的子序列与前缀 $s[1…i-1]$ 的子序列的差集。 序列自动机上的所有状态都是接受状态。 $d(u,c)=min\{i|i>u,s[i]=c\}$ ,也就是字符 $c$ 下一次出现的位置。因为若 $i>j$ ,后缀 $s[i…|s|]$ 的子序列是后缀 $s[j…|s|]$ 的子序列的子集,一定要选尽量靠前的,才能最优。 构建复杂度是 $O(n|\sum|)$ 。 ===== 算法实现 ===== ==== 构造 ==== struct SQAM { int ch[MAXL][MAXM],las[MAXM]; int root; void init() { root=1;//构造根节点 } void work(char s[]) { int len=strlen(s); for(int i=len-1;i>=0;i--) { for(int j=0;j ===== 代码练习 ===== 问题导入: 给定一个字符串 $S$ , $q$ 次询问,每次给定另一个字符串 $T$ ,询问 $T$ 是否是 $S$ 的子序列。 $len(S)≤10^{5},q≤10^{5},\sum len(T)≤10^{6}$ 最暴力的做法:建一个 $t_{0}$ ,指向所有点,然后每个点的后继是后面的点,最后每个点打结束标记,这样光建边就要 $O(len^{2})$ 再加上查询是 $O(\sum |T|)$ 。显然爆炸。显然对于相同的字符贪心取前面的就好了,记录每种字符在哪些位置出现过。这个复杂度是 $O(len)$ 的,然后对于查找串的当前字符 $upper_bound(pos[c].begin(),pos[c].end(),now_pos);$ ,如果查找到 $end$ 说明不存在,如果一直找到最后说明存在。这个的复杂度是 $O(|S|+\sum |T|*log(|S|))$ 的。 const int MAXN=1001000; const int MAXM=26; int q; char s[MAXN],t[MAXN]; vector pos[MAXM]; bool work() { scanf("%s",t); int len=strlen(t); int nowpos=-1; for(int i=0;i:: iterator y=upper_bound(pos[t[i]-'a'].begin(),pos[t[i]-'a'].end(),nowpos); if(y==pos[t[i]-'a'].end()) return false; nowpos=*y; } return true; } void solve() { scanf("%s",s); scanf("%d",&q); int len=strlen(s); for(int i=0;i 如果要支持带修,把 $vector$ 换成平衡树即可(修改变成 $O(logn)$ 的)。 #include #include using namespace std; const int N=1e5+10; struct fhq_treap{ int lson,rson; int val,key; int size; }; fhq_treap fhq[N]; int root[N],a[N]; int tot; inline int newnode(int val){ tot++; fhq[tot].key=rand(); fhq[tot].val=val; fhq[tot].size=1; return tot; } inline void pushup(int pos){ fhq[pos].size=fhq[fhq[pos].lson].size+fhq[fhq[pos].rson].size+1; } void split(int pos,int val,int &x,int &y){ if(!pos){ x=y=0; return ; } if(fhq[pos].val<=val){ x=pos; split(fhq[pos].rson,val,fhq[x].rson,y); } else{ y=pos; split(fhq[pos].lson,val,x,fhq[y].lson); } pushup(pos); } int merge(int x,int y){ if(!x||!y) return x+y; if(fhq[x].key>t>>n>>m>>k; for(int i=1;i<=n;i++){ cin>>a[i]; root[a[i]]=merge(root[a[i]],newnode(i)); } int opt; int x,y; int pos=0; bool flag; for(int i=1;i<=m;i++){ cin>>opt; if(opt){ cin>>x; pos=0; flag=true; for(int j=1;j<=x;j++){ cin>>y; pos=get_suc(root[y],pos); if(!pos) flag=false; } if(flag) cout<<"Yes\n"; else cout<<"No\n"; } else{ cin>>x>>y; del(root[a[x]],x);//删除 a[x]=y;//修改 ins(root[a[x]],x);//插入 } } } 1.[[https://www.luogu.com.cn/problem/P4112#submit]] ==== 题意 ==== 给定两个长度不超过 $2000$ 的字符串,分别设为 $s_{1},s_{2}$ 分为四个问题: - 求 $s_{1}$ 的最短的子串且不是 $s_{2}$ 的子串 - 求 $s_{1}$ 的最短的子串且不是 $s_{2}$ 的子序列 - 求 $s_{1}$ 的最短的子序列且不是 $s_{2}$ 的子串 - 求 $s_{1}$ 的最短的子序列且不是 $s_{2}$ 的子序列 如果没找到输出 $-1$ 。 ==== 题解 ==== 显然要分别对两个串建立后缀自动机和序列自动机 因为要找最短的,所以 $BFS$ 开始搜,不管是什么自动机,从根节点往下开始搜,搜到 $s_{1}$ 有结点且 $s_{2}$ 没有的结点就可以了,如果到最后都没找到输出 $-1$ 即可。 #include using namespace std; typedef long long ll; const int MAXL=2021; const int MAXM=46; const int MAXN=2021; struct SAM { int las,tot; struct NODE { int ch[MAXM]; int len,fa; NODE() { memset(ch,0,sizeof(ch)); len=0; fa=0; } } dian[MAXN<<1]; void init() { las=tot=1; } void add(int c) { int p=las; int np=las=++tot; dian[np].len=dian[p].len+1; for(; p&&!dian[p].ch[c]; p=dian[p].fa)dian[p].ch[c]=np; if(!p)dian[np].fa=1;//以上为case 1 else { int q=dian[p].ch[c]; if(dian[q].len==dian[p].len+1)dian[np].fa=q;//以上为case 2 else { int nq=++tot; dian[nq]=dian[q]; dian[nq].len=dian[p].len+1; dian[q].fa=dian[np].fa=nq; for(; p&&dian[p].ch[c]==q; p=dian[p].fa)dian[p].ch[c]=nq; //以上为case 3 } } } }S1,S2; char A[MAXL],B[MAXL]; int la,lb; struct SQAM { int ch[MAXL][MAXM],las[MAXM]; int root; void init() { root=1;//构造根节点 } void work(char s[]) { int len=strlen(s); for(int i=len-1;i>=0;i--) { for(int j=0;jq; vis[1][1]=v; Node ntmp; ntmp.a=1,ntmp.b=1,ntmp.s=0; q.push(ntmp); while(!q.empty()) { ntmp=q.front(); int a=ntmp.a,b=ntmp.b,s=ntmp.s; //printf("%d %d %d %d\n",a,b,s,v); q.pop(); for(int i=0,da,db;i