成都集训-字符串篇

[NOI2014]动物园

题目描述

我们给定一个字符串 \(S\) ,定义 \(num[i]\) 表示 \(S\) 的前 \(i\) 个字符组成的字符串中,长度小于等于 \(\lfloor\dfrac{i}{2} \rfloor\)\(border\) 数量。求 $\sum (num[i]+1) $ 。

\(|S| \leqslant 1e6\)

思路点拨

我们考虑一个暴力,我们可以使用 \(\text{KMP}\) 算法先求出 \(fail\) 数组和一个 \(dp\) 数组。其中 \(dp_i\) 表示字符串 \(S\) 的前 \(i\) 个字符组成的字符串中,全部的 \(border\) 的数量。这两点显然是可以线性求出的。那么,每一次我们要求出 \(num[i]\) ,我们就使用一个指针 \(j\) ,不断地跳 \(fail_j\) 直到 \(j \leqslant \lfloor \dfrac{i}{2}\rfloor\) 位置,最后 \(num[i]=dp_j\) 。但是这个做法在全部都是 \(a\) 的字符串中会退化成 \(O(n^2)\) 。我们考虑两种优化。

  • 倍增

因为全部的 \(fail\) 数组实际上会组成一颗 \(fail\) 树。我们要一直跳 \(fail\) 的过程其实十分的重复,所以我们使用树上倍增的方法将其压缩至 \(O(n \log n)\) 。这个做法在本题不够优秀,但是具有启发性。

  • 基于树上倍增的进一步优化

实际上,在一般的树上上述的倍增方法已经是足够优秀了,但是这个 \(fail\) 树上有更为优秀的性质。 \(fail_i < i\) 。也就是说对于树上的一条链,从根到叶子的过程中,我们倍增找到的那个节点的深度是单调的。我们可以维护一个指针扫一遍。时间复杂度 \(O(n)\) 。具体实现并不用那么复杂,具体看代码:

  • \(code\)
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
	int x=0,f=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-') f=-f;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9'){
		x=x*10+ch-'0';
		ch=getchar();
	}
	return x*f;
}
const int MAXN=1e6+10,mod=1e9+7;
int T,n,p[MAXN],f[MAXN];
string s;
signed main(){
	T=read();
	while(T--){
		cin>>s;n=s.length();
		memset(p,0,sizeof(p));
		memset(f,0,sizeof(f));
		for(int i=1;i<n;i++){
			int j=p[i];
			while(j&&s[i]!=s[j]) j=p[j];
			j+=(s[i]==s[j]);
			p[i+1]=j;
		}
		for(int i=1;i<=n;i++)
			f[i]=f[p[i]]+1;
		int ans=1,j=0;
		for(int i=1;i<n;i++){
			while(j&&s[i]!=s[j]) j=p[j];
			j+=(s[i]==s[j]);
			while(j>(i+1)/2) j=p[j];
			ans=ans*(f[j]+1)%mod; 
		}
		cout<<ans<<endl;
	}
	return 0;
} 

BZOJ 1461 (Luogu Cow Patterns G)

题目描述

我们有两个字符串,一个是文本串,另一个是模式串。我们认为两个文本串相等当且仅当两个字符串离散化之后相等。希望直到模式串在文本串中出现的位置。

思路点拨

考虑 \(\text{KMP}\) ,我们可以重新定义两个数相对位置相等。我们在模式串中,对于每一个元素 \(b_i\) 找到一个最大的下标 \(j\) 满足 \(b_j \leqslant b_i\) 和一个最大的小标 \(k\) 满足 \(b_i \leqslant b_i\) \((j<i,k<i)\) 。也就是前驱后继。那么我们比较 \(a_i\)\(b_j\) 是否相等的时候就可以通过判断前驱后继相对位置上的大小关系来比较是否相等。具体匹配的时候可以使用我们上述定义的相等来代替 \(\text{KMP}\) 模板中的相等。

具体的正确性证明也十分简单,因为我们的前驱后继是在模式串的基础上定义的而不是在文本串上定义的,所以匹配的时候不会有问题。

\(code\)

#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
	int x=0,f=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-') f=-f;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9'){
		x=x*10+ch-'0';
		ch=getchar();
	}
	return x*f;
}
const int MAXN=1e6+10,mod=1e9+7;
int n,k,S,a[MAXN];
int b[MAXN],pre[MAXN],suc[MAXN]; 
struct node{
	int pos,val;
	bool friend operator<(const node &A,const node &B){
		if(A.val==B.val) return A.pos<B.pos;
		return A.val<B.val;
	}
};
set<node> s;
set<node>::iterator it;
bool equal_ofa(int x,int y){//比较 a[x] 和 b[y] 
	if(pre[y]){
		if(b[pre[y]]<b[y]&&a[x-(y-pre[y])]>=a[x]) return 0;
		if(b[pre[y]]==b[y]&&a[x-(y-pre[y])]!=a[x]) return 0;
	} 
	if(suc[y]){
		if(b[suc[y]]==b[y]&&a[x+(suc[y]-y)]!=a[x]) return 0;
		if(b[suc[y]]>b[y]&&a[x+(suc[y]-y)]<=a[x]) return 0;
	}
	return 1;
}
bool equal_ofb(int x,int y){//比较 b[x]和 b[y] 
	if(pre[y]){
		if(b[pre[y]]<b[y]&&b[x-(y-pre[y])]>=b[x]) return 0;
		if(b[pre[y]]==b[y]&&b[x-(y-pre[y])]!=b[x]) return 0;
	} 
	if(suc[y]){
		if(b[suc[y]]==b[y]&&b[x+(suc[y]-y)]!=b[x]) return 0;
		if(b[suc[y]]>b[y]&&b[x+(suc[y]-y)]<=b[x]) return 0;
	}
	return 1;
}
int fail[MAXN];
int id[MAXN],cnt;
signed main(){
	n=read(),k=read(),S=read();
	for(int i=1;i<=n;i++) a[i]=read();
	for(int i=1;i<=k;i++){
		b[i]=read();
		s.insert((node){i,b[i]}); 
		it=s.lower_bound((node){i,b[i]});
		it++;
		if(b[(*it).pos]>=b[i]&&(*it).pos<i) suc[i]=(*it).pos;
		--it;--it;
		if(b[(*it).pos]<=b[i]&&(*it).pos<i) pre[i]=(*it).pos;
	}
	for(int i=2;i<=k;i++){
		int j=fail[i-1];
		while(j&&!equal_ofb(i,j+1)) j=fail[j];
		j+=equal_ofb(i,j+1);
		fail[i]=j;
	}
	for(int i=1,j=0;i<=n;i++){
		while(j&&!equal_ofa(i,j+1)) j=fail[j];
		j+=equal_ofa(i,j+1);
		if(j==k){
			++cnt;
			id[cnt]=i-k+1;
			j=fail[j];
		} 
	}
	cout<<cnt<<endl;
	for(int i=1;i<=cnt;i++) cout<<id[i]<<endl;
	return 0;
} 

[POI2005] SZA-Template

题目描述

你打算在纸上印一串字母。

为了完成这项工作,你决定刻一个印章。印章每使用一次,就会将印章上的所有字母印到纸上。

同一个位置的相同字符可以印多次。例如:用 aba 这个印章可以完成印制 ababa 的工作(中间的 a 被印了两次)。但是,因为印上去的东西不能被抹掉,在同一位置上印不同字符是不允许的。例如:用 aba 这个印章不可以完成印制 abcba 的工作。

因为刻印章是一个不太容易的工作,你希望印章的字符串长度尽可能小。

思路点拨

本题具体有两种做法,失配树和动态规划。这里讲述更好理解的失配树做法,想要了解动态规划做法可以看 这里

我们考虑建出失配树,然后寻找一些性质。对于一个印章,我们肯定需要在 \(1\) 开头的位置印刷一次,在 \(n\) 结尾的地方印刷一次,那么这个印章是 \(1,2,...,n-1,n\) 的一个 \(\text{border}\) 。答案返回到失配树上,就是根节点到 \(n\) 的这一条路径上。我们的答案是在这条路径上合法,并且深度最小的点。

我们接着想,一个答案什么时候合法?对于一个失配树上的节点 \(u\) ,我们对其子树内的节点排序。如果存在排序后两个相邻的元素 \(i,j\)\(\text{abs(i-j)}>u\) ,那么这个 \(u\) 肯定不合法。具体大家可以结合失配树的意义自行理解一下。类似于出现了一个长度大于 \(u\) 的区间无法被 \(1\)\(u\) 这个 \(\text{border}\) 印刷出来。我们现在需要解决的就是如何找到这个最大的邻值。

如果我们从 \(n\) 一路走到根节点,这个最大的邻值是单调不递增的,我们不好维护。但是如果我们是从根节点走到 \(n\) ,那么添加节点机会变成添加节点,这个最大邻值也就是单调不递减的。我们可以使用一个双向链表每次 \(O(1)\) 维护。总体时间复杂度 \(O(n)\)

#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
	int x=0,f=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-') f=-f;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9'){
		x=x*10+ch-'0';
		ch=getchar();
	}
	return x*f;
}
const int MAXN=5e5+10,mod=1e9+7;
int n,fail[MAXN];
string s;
int temp[MAXN],top;
int pre[MAXN],suc[MAXN];//双向链表 
int mx=1;//最大的邻值之差
vector<int> e[MAXN];//失配树
void erase(int x){
	if(x<1||x>n) return ;
	mx=max(mx,suc[x]-pre[x]);
	suc[pre[x]]=suc[x];
	pre[suc[x]]=pre[x];
}
void bfs(int f,int v){
	queue<int> q;
	q.push(f);
	while(!q.empty()){
		int x=q.front();
		q.pop();
		if(x==v) continue;
		erase(x);
		for(int i=0;i<e[x].size();i++){
			int to=e[x][i];
			q.push(to);
		}
	}
}
signed main(){
	cin>>s;n=s.length();
	s='0'+s;
	for(int i=2,j=0;i<=n;i++){
		while(j&&s[i]!=s[j+1]) j=fail[j];
		j+=(s[i]==s[j+1]);
		fail[i]=j;
	}//KMP
	for(int i=n;i;i=fail[i]) temp[++top]=i;//此时temp中失是降序的
	for(int i=1;i<=n;i++){
		pre[i]=i-1,suc[i]=i+1;
		e[fail[i]].push_back(i);
	}//树根为0
	for(int i=top;i;i--){
		bfs(temp[i+1],temp[i]);
		if(mx<=temp[i]){
			cout<<temp[i];
			return 0;
		}
	}
	cout<<n;
	return 0;
} 

[BZOJ 2601] Country

题目描述

\(n (n \leqslant 26)\) 个字符串变量。每一个字符串变量可以包含小写字母,也可以包含其他的字符串变量(用大写字母表示)。例如:
\(A=greatglorycorrect,B=xx,C=leadusgo,D=ABC\)
我们保证这些字符串的定义是无环的。现在给定了一个小写字母组成的模式串,问其在某个字符串变量中出现的次数。字符串变量的长度和模式串长度 单个 不超过 \(100\)

思路点拨

当一个字符串变量只有小写字母时,我们只需要做 \(\text{KMP}\) 的板子就可以了。但是当我们出现的字符串的递归定义式,两个字符串之间可能产生新的满足条件的模式串。例如 \(A=ab,B=AA\) ,模式串是 \(ba\)
那么两个 \(A\) 之间就多出现的了一个模式串。怎么将两个字符串变量接起来呢?我发现,在我们处理完第一个字符串变量之后,会留下一个 \(fail\) 指针,那么第二个字符串变量就可以在这个指针的基础上继续匹配就行了。这个题目中的递归关系需要我们多次调用某一个字符串变量的情况,我们考虑使用动态规划:

定义状态 \(f_{i,j}\) 表示在第 \(i\) 个字符串变量, \(fail\) 指针从 \(j\) 开始匹配可以匹配出多少个模式串。

定义状态 \(pos_{i,j}\) 表示在第 \(i\) 个字符串变量, \(fail\) 指针从 \(j\) 开始匹配后留下的 \(fail\) 指针。

状态转移是十分显然的。当我们在字符串变量扫到的字母是大写时,递归求解。反之我们直接 \(\text{KMP}\) 。具体看代码。本题的难度不大,但是需要对 \(\text{KMP}\) 算法有十分透彻的理解。如果还是不会建议重新的,仔细再学一遍 \(\text{KMP}\)

\(code\)

#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
	int x=0,f=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-') f=-f;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9'){
		x=x*10+ch-'0';
		ch=getchar();
	}
	return x*f;
}
const int MAXN=1e2+10,S=30;
const int mod=10000;
int n,len[S],fail[MAXN],m;
char s[MAXN];//s1是文本串,s2是待匹配串
char c[S][MAXN],txt[MAXN];
void init(){
	scanf("%s",s+1);
	m=strlen(s+1);
	for(int i=2;i<=m;i++){
		int j=fail[i-1];
		while(j&&s[i]!=s[j+1]) j=fail[j];
		j+=(s[i]==s[j+1]);
		fail[i]=j;
	} 
}
int f[S][MAXN],pos[S][MAXN];
//f[i][j]表示在字符串 i 开始的 j 位匹配模式串的结果
//nxt[i][j]表示在字符串 i 开始的 j 位匹配模式串后的fail指针 
void dp(int i,int j){
	if(f[i][j]!=-1) return ;
	f[i][j]=0;
	int id=j;
	for(int k=1;k<=len[i];k++){
		if('A'<=c[i][k]&&c[i][k]<='Z'){//遇到大写字母,递归求解
			dp(c[i][k]-'A',id);
			f[i][j]=(f[i][j]+f[c[i][k]-'A'][id])%mod;
			id=pos[c[i][k]-'A'][id];
		}
		else{
			while(id&&c[i][k]!=s[id+1])
				id=fail[id];
			id+=(c[i][k]==s[id+1]);
			if(id==m){
				f[i][j]=(f[i][j]+1)%mod;
				id=fail[id];
			}
		}
	}
	pos[i][j]=id;
}
signed main(){
	scanf("%lld",&n);
	scanf("%s",txt);
	for(int i=0;i<n;i++){
		scanf("%s",c[i]);
		len[i]=strlen(c[i]);
		for(int j=2;j<len[i];j++)
			c[i][j-1]=c[i][j];
		len[i]-=2;
	}
	init();//预处理fail指针
	memset(f,-1,sizeof(f));
	dp(txt[0]-'A',0);
	cout<<f[txt[0]-'A'][0];
	return 0;
}

[NOIP2020] 字符串匹配

题意描述

对于一个字符串 \(S\),题目要求他找到 \(S\) 的所有具有下列形式的拆分方案数:

\(S = ABC\)\(S = ABABC\)\(S = ABAB \ldots ABC\),其中 \(A\)\(B\)\(C\) 均是非空字符串,且 \(A\) 中出现奇数次的字符数量不超过 \(C\) 中出现奇数次的字符数量。

更具体地,我们可以定义 \(AB\) 表示两个字符串 \(A\)\(B\) 相连接,例如 \(A = \texttt{aab}\)\(B = \texttt{ab}\),则 \(AB = \texttt{aabab}\)

并递归地定义 \(A^1=A\)\(A^n = A^{n - 1} A\)\(n \ge 2\) 且为正整数)。例如 \(A = \texttt{abb}\),则 \(A^3=\texttt{abbabbabb}\)

则小 C 的习题是求 \(S = {(AB)}^iC\) 的方案数,其中 \(F(A) \le F(C)\)\(F(S)\) 表示字符串 \(S\) 中出现奇数次的字符的数量。两种方案不同当且仅当拆分出的 \(A\)\(B\)\(C\) 中有至少一个字符串不同。

思路点拨

可以发现 \(AB\) 是字符串 \(S\) 的一个前缀,我们枚举一个 \(AB\) ,紧接着枚举一个 \(i\) ,这样我们就可以知道字符串 \(C\)\(AB,i,C\) 均指题目所求得 \((AB)^i C\) 。我们就需要知道,在长度为 \(|AB|\) 的前缀中,存在多少个 \(A\) 满足 \(f(A) \leqslant f(C)\) 。因为 \(A,C\) 均是字符串的后缀,所以 \(f(A)\)\(f(C)\) 可以预处理。那么已经知道了 \(f(C)\) ,存在的 \(f(A)\) 可以使用树状数组求出。

时间复杂度 \(O(Tn\log n\log |S|)\) ,其中 \(S\) 是字符集大小。本题还是十分简单的,可以作为 \(\text{KMP}\) 或者 \(\text{Hash}\) 的练手题。对于这种题目,就是要选对该枚举的字符串,问题就会迎刃而解。这里给出一份使用字符串哈希实现的代码

\(code\)

#include<bits/stdc++.h>
#define int unsigned long long
using namespace std;
inline int read(){
	int x=0,f=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-') f=-f;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9'){
		x=x*10+ch-'0';
		ch=getchar();
	}
	return x*f;
}
const int base=123;
const int MAXN=2e6+10,N=2e6;
int T,n,pw[MAXN]={1},h[MAXN];
char c[MAXN];
int hsh(int l,int r){
	int len=r-l+1;
	return h[r]-h[l-1]*pw[len];
}
int cnt[30],pre[MAXN],suc[MAXN];
void init(){
	memset(cnt,0,sizeof(cnt));
	int sum=0;
	for(int i=1;i<=n;i++){
		if(cnt[c[i]-'a']&1) sum--;
		else sum++;
		cnt[c[i]-'a']++;
		pre[i]=sum;
	}
	sum=0;
	memset(cnt,0,sizeof(cnt));
	for(int i=n;i;i--){
		if(cnt[c[i]-'a']&1) sum--;
		else sum++;
		cnt[c[i]-'a']++;
		suc[i]=sum;
	}
}
int t[30],ans;
int lowbit(int x){
	return x&(-x);
}
void add(int x,int y){
	for(int i=x+1;i<=27;i+=lowbit(i))
		t[i]+=y;
}
int query(int x){
	int sum=0;
	for(int i=x+1;i;i-=lowbit(i))
		sum+=t[i];
	return sum;
}
signed main(){
	T=read();
	for(int i=1;i<=N;i++) pw[i]=pw[i-1]*base;
	while(T--){
		scanf("%s",c+1);
		n=strlen(c+1);
		for(int i=1;i<=n;i++)
			h[i]=h[i-1]*base+c[i];
		init();
		memset(t,0,sizeof(t));
		ans=0;
		for(int len=1;len<=n;len++){
			if(len-1) add(pre[len-1],1);
			for(int i=1;i+len-1<=n;i+=len){
				if(hsh(i,i+len-1)!=h[len]) break;
				if(i+len-1<n) ans+=query(suc[i+len]);
			}
		}
		cout<<ans<<endl;
	}
	return 0;
}

[NOI2011] 阿狸的打字机

题目描述

阿狸喜欢收藏各种稀奇古怪的东西,最近他淘到一台老式的打字机。打字机上只有 \(28\) 个按键,分别印有 \(26\) 个小写英文字母和 BP 两个字母。经阿狸研究发现,这个打字机是这样工作的:

  • 输入小写字母,打字机的一个凹槽中会加入这个字母(这个字母加在凹槽的最后)。
  • 按一下印有 B 的按键,打字机凹槽中最后一个字母会消失。
  • 按一下印有 P 的按键,打字机会在纸上打印出凹槽中现有的所有字母并换行,但凹槽中的字母不会消失。

例如,阿狸输入 aPaPBbP,纸上被打印的字符如下:

a
aa
ab

我们把纸上打印出来的字符串从 \(1\) 开始顺序编号,一直到 \(n\)。打字机有一个非常有趣的功能,在打字机中暗藏一个带数字的小键盘,在小键盘上输入两个数 \((x,y)\)(其中 \(1\leq x,y\leq n\)),打字机会显示第 \(x\) 个打印的字符串在第 \(y\) 个打印的字符串中出现了多少次。

思路点拨

\(\text{ACAM}\) 板题。

多模式串,多文本串的字符串匹配问题,考虑 \(\text{ACAM}\) 。并且,我们不难发现,题目输入的第一个字符串就是在给我们提供一个字典树,这个字典树上的节点个数是 \(O(n)\) 级别的,那么我们就可以在线性的时间内建立出 \(\text{ACAM}\) 的字典树和 \(\text{fail}\) 树。(一定要区分,不然下面不好做)

在线做是比较难的,考虑将询问离线下来。对于一组询问 \((u,v)\) ,如果要查询 \(u\)\(v\) 中出现的次数,我们可以将 \(v\) 的所有节点染色,查询 \(u\)\(\text{fail}\) 树上的子树染色节点数量。对于染色这一点,我们可以在字典树上进行 \(\text{dfs}\) ,对于每一个遍历的节点,将 \(\text{fail}\) 树上的节点染色即可。可以使用树状数组实现。

代买实现比较简单,总体时间复杂度是 \(O(n \log n)\)

#include<bits/stdc++.h>
using namespace std;
inline int read(){
	int x=0,f=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-') f=-f;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9'){
		x=x*10+ch-'0';
		ch=getchar();
	}
	return x*f;
}
const int MAXN=1e5+10;
string s;
int n;
int pos[MAXN],trie[MAXN][26],tot;
vector<int> G[MAXN];//字典树 
struct node{
	int fail,sum,dad;
}t[MAXN];
struct problem{
	int u,v,id;//u在v中出现了多少次 
	//id是问题编号 
};
int ans[MAXN];
vector<problem> pb[MAXN];//存储的问题
vector<int> e[MAXN];//fail树
void init(){
	int rot=0,cnt=0;
	for(int i=0;i<s.length();i++){
		if('a'<=s[i]&&s[i]<='z'){
			char c=s[i]-'a';
			if(!trie[rot][c]){
				trie[rot][c]=++tot;
				G[rot].push_back(tot);
			}
			t[trie[rot][c]].dad=rot;
			rot=trie[rot][c];
		}
		else if(s[i]=='P'){
			t[rot].sum++;
			pos[++cnt]=rot;
		} 
		else rot=t[rot].dad;
	}
}
queue<int> q;
void build(){
	for(int i=0;i<26;i++)
		if(trie[0][i])
			q.push(trie[0][i]);
	while(!q.empty()){
		int u=q.front();
		q.pop();
		for(int i=0;i<26;i++){
			int v=trie[u][i];
			if(v){
				t[v].fail=trie[t[u].fail][i];
				q.push(v);
			}
			else trie[u][i]=trie[t[u].fail][i];
		}
	}
	for(int i=1;i<=tot;i++)
		e[t[i].fail].push_back(i);
}
int res,dfn[MAXN],siz[MAXN];
void dfs1(int x){
	dfn[x]=++res;
	siz[x]=1;
	for(int i=0;i<e[x].size();i++){
		int to=e[x][i];
		dfs1(to);
		siz[x]+=siz[to];
	}
}
int bit[MAXN];
int lowbit(int x){
	return x&(-x);
}
void add(int x,int y){
	for(int i=x;i<=res;i+=lowbit(i))
		bit[i]+=y;
}
int query(int x){
	int cnt=0;
	for(int i=x;i;i-=lowbit(i))
		cnt+=bit[i];
	return cnt;
}
void dfs2(int x){
	add(dfn[x],1);
	for(int i=0;i<pb[x].size();i++){
		int v=pos[pb[x][i].u];
		ans[pb[x][i].id]=query(dfn[v]+siz[v]-1)-query(dfn[v]-1);
	}
	for(int i=0;i<G[x].size();i++){
		int to=G[x][i];
		dfs2(to);
	}
	add(dfn[x],-1);
}
signed main(){
	cin>>s;
	init();//建立字典树
	build();
	dfs1(0);
	n=read();
	for(int i=1;i<=n;i++){
		int u=read(),v=read();
		pb[pos[v]].push_back((problem){u,v,i});
	}
	dfs2(0);//这是在字典树上
	for(int i=1;i<=n;i++) cout<<ans[i]<<endl;
	return 0;
}

Substrings in a String

题目描述

你需要维护一个文本串,支持如下操作:

  • 将文本串的第 \(i\) 个字符变成 \(ch\)
  • 给定一个模式串,查询其在文本串的 \([l,r]\) 中出现的次数。

\(n,\sum |S| \leqslant 1e5\)

思路点拨

我们直接bitset。

#include<bits/stdc++.h>
using namespace std;
inline int read(){
	int x=0,f=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-') f=-f;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9'){
		x=x*10+ch-'0';
		ch=getchar();
	}
	return x*f;
}
const int MAXN=1e5+10;
char c[MAXN];
int n,q;
bitset<MAXN> temp;
bitset<MAXN> pos[26];
signed main(){
	scanf("%s",c+1);
	n=strlen(c+1);
	for(int i=1;i<=n;i++)
		pos[c[i]-'a'][i]=1;
	q=read();
	while(q--){
		int opt=read();
		if(opt==1){
			int p=read();
			char ch;cin>>ch;
			pos[c[p]-'a'][p]=0;
			c[p]=ch;
			pos[c[p]-'a'][p]=1;
		}
		else{
			int l=read(),r=read();
			string s;cin>>s;
			int len=s.length();
			temp.set();
			for(int i=0;i<len;i++)
				temp=temp&(pos[s[i]-'a']>>i);
			printf("%d\n",(s.length()>r-l+1)?0:(temp>>l).count()-(temp>>(r-s.length()+2)).count());
			//[l,r] 是我需要的区间,但是我们最终维护的temp数组只记录的字符串匹配的起点 
			//所以我们令len为字符串长度,那么我们在最终的temp中只需要考虑[l,r-len+1]
			//我们先将bitset右移l,提取出[l,n],接着右移 r-len+2位,提取出我们需要的区间
			//但是当字符串的长度大于询问区间的时候,bitset中任然可能有存留(l>r-len+1) ,我们需要特判,不然WA on 25 
		}
	}
	return 0;
}
posted @ 2023-07-16 10:00  Diavolo-Kuang  阅读(15)  评论(0编辑  收藏  举报