字符串String

字符串String

字符串学习笔记——主要来自zhicheng和🐨的讲课。

字符串哈希

之前一直对哈希没有感觉,但自从 9 月学了一些就好多了。

很多的字符串问题都可以用哈希去完成,

在之前的学习中我也做了一些用平衡树/线段树去维护哈希值的题目,

比如 P5537 【XR-3】系统设计,还配题解

还有 P4036 [JSOI2008] 火星人

而哈希主要就是去解决字符串匹配的问题,经常可以配上二分。

最长回文子串问题也可以用哈希 \(O(n)\) 解决,

具体就是疑问下一位的 \(ans_i \le ans_{i-1}+2\) ,其他的情况就暴力枚举就可以了,

可以证明最多暴力枚举 \(2n\) 次。

这是字符串最基础也是最有用的算法,写一些例题。

P4824 栈+哈希

P4824 [USACO15FEB] Censoring S

栈+哈希,每次判断栈顶的哈希值就行了。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pll pair<ll,ll>
#define fi first
#define se second 
#define mk make_pair

const int N=1e6+5;
int n,tp,m;
pll st[N],base={229,229},mod={1e9+9,998244853},p[N],val;
pll operator *(pll x,pll y){return {x.fi*y.fi%mod.fi,x.se*y.se%mod.se};}
pll operator -(pll x,pll y){return {(x.fi-y.fi+mod.fi)%mod.fi,(x.se-y.se+mod.se)%mod.se};}
pll operator +(pll x,pll y){return {(x.fi+y.fi)%mod.fi,(x.se+y.se)%mod.se};}
char s[N],t[N],stk[N];

void init(){
  p[0]={1,1};
  for(int i=1;i<N;i++) p[i]=p[i-1]*base;
}

int main(){
  /*2023.11.7 H_W_Y P4824 [USACO15FEB] Censoring S String Hash*/ 
  scanf("%s%s",s,t);init();
  n=strlen(s),m=strlen(t);
  for(int i=0;i<m;i++) val=val+mk(t[i]-'a',t[i]-'a')*p[i+1];
  for(int i=0;i<n;i++){
  	tp++;
  	st[tp]=st[tp-1]+p[tp]*mk(s[i]-'a',s[i]-'a');stk[tp]=s[i];
  	if(tp>=m&&st[tp]-st[tp-m]==val*p[tp-m]) tp-=m;
  }
  for(int i=1;i<=tp;i++) putchar(stk[i]);
  return 0;
}

P3498 最多的本质不同的串

P3498 [POI2010] KOR-Beads

直接枚举 \(k\) 就可以了,再维护一个哈希。

\(\sum_{i=1}^{n}\lfloor \frac{n}{k} \rfloor\) 大概是 \(n \ln n\)

我的哈希被卡了一次!base 最好还是不要写一样的。

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define pll pair<ll,ll>
#define fi first
#define se second
#define mk make_pair
#define pb push_back

const int N=2e5+5;
int a[N],n,ans=0;
vector<int> g;
pll base={28,26},mod={1e9+9,998244853},p[N],pre[N],suf[N];
map<pll,int> mp;

int read(){
  int x=0,f=1;char ch=getchar();
  while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
  while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
  return x*f;
}

pll operator *(pll x,pll y){return {x.fi*y.fi%mod.fi,x.se*y.se%mod.se};}
pll operator +(pll x,pll y){return {(x.fi+y.fi)%mod.fi,(x.se+y.se)%mod.se};}
pll operator -(pll x,pll y){return {(x.fi-y.fi+mod.fi)%mod.fi,(x.se-y.se+mod.se)%mod.se};}

void init(){
  p[0]={1,1};
  for(int i=1;i<N;i++) p[i]=p[i-1]*base;
}

int main(){
  /*2023.11.7 H_W_Y P3498 [POI2010] KOR-Beads String Hash*/
  n=read();init();
  for(int i=1;i<=n;i++) a[i]=read(),pre[i]=pre[i-1]+p[i]*mk(a[i],a[i]);
  for(int i=n;i>=1;i--) suf[i]=suf[i+1]+p[n-i+1]*mk(a[i],a[i]);
  for(int k=1;k<=n;k++){
  	int id=1,cnt=0;mp.clear();
  	while(id+k-1<=n){
  	  pll p1=(pre[id+k-1]-pre[id-1])*p[n-id],p2=(suf[id]-suf[id+k])*p[id+k-2];
  	  if(!mp.count(p1)&&!mp.count(p2)) cnt++;
  	  mp[p1]=1;mp[p2]=1;
	  id+=k;	
	}
	if(cnt>ans) ans=cnt,g.resize(0),g.pb(k);
	else if(cnt==ans) g.pb(k);
  }
  printf("%d %d\n",ans,(int)g.size());
  for(int i=0;i<(int)g.size();i++) printf("%d ",g[i]);
  return 0;
}

P3538 找区间最小循环节

P3538 [POI2012] OKR-A Horrible Poem

由于循环节一定是区间长度的因子,如果一个区间有 \(len\) 长度的循环节,

那么最小的循环节也一定是 \(len\) 的因子。

所以我们可以每一次除掉最小的那个质因子去判断新得到的长度是不是最小循环节,

这样是一定可以找到答案的,并且找到的也是最小的答案,这个很容易证明。

而判断循环节也是好判断的,

我们只需要去判断 \(l \sim r-len\)\(l+len \sim r\) 是否相等,这个结论可以画图推一下,

由于哈希值相等,它们的每一位是一一对应的,就可以得到这个循环的结论。

因为我们每一次是去除掉最小的,所以就不必要枚举了,

在线性筛的时候记录一下就可以了,否则会被卡时间。。。

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

const int N=5e5+5;
int n,m,L,R,cnt=0,prm[N],g[N];
bool vis[N];
char s[N];
ll base=26,mod=998244853,p[N],sum[N];

void init(){
  for(int i=2;i<=n;i++){
  	if(!vis[i]) prm[++cnt]=i,g[i]=i;
  	for(int j=1;j<=cnt&&prm[j]*i<=n;j++){
  	  vis[prm[j]*i]=true;g[prm[j]*i]=prm[j];
	  if(i%prm[j]==0) break;	
	}
  }
  p[0]=1;
  for(int i=1;i<N;i++) p[i]=p[i-1]*base%mod;
}

int read(){
  int x=0,f=1;char ch=getchar();
  while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
  while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
  return x*f;
}

void print(int x){
  int P[14],tmp=0;
  if(x==0) putchar('0');
  if(x<0) putchar('-'),x=-x;
  while(x) P[++tmp]=x%10,x/=10;
  for(int i=tmp;i>=1;i--) putchar(P[i]+'0');
  putchar('\n');
}

bool chk(int l,int r,int len){
  if((sum[r]-sum[l+len-1]+mod)%mod==(sum[r-len]-sum[l-1]+mod)%mod*p[len]%mod) return true;
  return false;
}

int sol(int l,int r){
  int len=r-l+1,tmp=len;
  while(tmp>1){
  	if(chk(l,r,len/g[tmp])) len/=g[tmp];
  	tmp/=g[tmp];
  }
  return len;
}

int main(){
  /*2023.11.7 H_W_Y P3538 [POI2012] OKR-A Horrible Poem String Hash*/
  n=read();init();
  scanf("%s",s+1);
  for(int i=1;i<=n;i++) sum[i]=(sum[i-1]+p[i]*(s[i]-'a')%mod)%mod;
  m=read();
  for(int i=1;i<=m;i++){
  	L=read();R=read();
  	print(sol(L,R));
  }
  return 0;
}

P4398 二分+二维哈希

P4398 [JSOI2008] Blue Mary的战役地图

求两个边长均为 \(n\) 的正方形矩阵的最大公共正方形矩阵的边长。

原题数据极其弱,这里请考虑 \(n \le 1000\)

首先是二分答案,很容易想到,那我们如何处理比较呢?

这个时候就需要用到二维哈希。

二维哈希,就类似于二维前缀和与差分,

在实现的时候我们对每一维分别哈希,再合并到一起。

for(int i=1;i<=n;i++) 
  for(int j=1,x;j<=n;j++)
	x=read(),f[i][j]=(f[i][j-1]*base1%mod+x)%mod; 
for(int i=1;i<=n;i++)
  for(int j=1;j<=n;j++)
    f[i][j]=(f[i-1][j]*base2%mod+f[i][j])%mod;  

于是查询的时候就有

\[hash_{x_1,y_1,x_2,y_2}=f{x_2,y_2}-f_{x_2,y_1-1} \times {base_1}^{y_2-y_1+1}-f_{x_1-1,y_2} \times {base_2}^{x_2-x_1+1}+f_{x_1-1,y_1-1} \times {base_1}^{y_2-y_1+1}\times {base_2}^{x_2-x_1+1} \]

其实也就类似于二维前缀和。

于是这道题就可以完成了。

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

const int N=1005;
int n,f[2][N][N],l,r;
ll base1=229,base2=223,mod=998244853,p1[N],p2[N];

int read(){
  int x=0,F=1;char ch=getchar();
  while(!isdigit(ch)){if(ch=='-') F=-1;ch=getchar();}
  while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
  return x*F;
}

void init(){
  p1[0]=p2[0]=1ll;
  for(int i=1;i<N;i++) p1[i]=p1[i-1]*base1%mod,p2[i]=p2[i-1]*base2%mod;
}

ll h(int op,int X1,int Y1,int X2,int Y2){
  return (f[op][X2][Y2]-f[op][X2][Y1-1]*p1[Y2-Y1+1]%mod-f[op][X1-1][Y2]*p2[X2-X1+1]%mod+f[op][X1-1][Y1-1]*p1[Y2-Y1+1]%mod*p2[X2-X1+1]%mod+2*mod)%mod;
}

map<ll,int >mp;

bool chk(int mid){
  mp.clear();
  for(int i=1;i+mid-1<=n;i++)
  	for(int j=1;j+mid-1<=n;j++){
  	  ll ha=h(0,i,j,i+mid-1,j+mid-1);
  	  mp.insert({ha,1}); 
	}
  for(int i=1;i+mid-1<=n;i++)
    for(int j=1;j+mid-1<=n;j++){
      ll ha=h(1,i,j,i+mid-1,j+mid-1);
      if(mp.count(ha)) return true;
	}
  return false;
}

int main(){
  /*2023.11.7 H_W_Y P4398 [JSOI2008] Blue Mary的战役地图 String Hash*/
  n=read();init();
  for(int op=0;op<2;op++){
    for(int i=1;i<=n;i++) 
      for(int j=1,x;j<=n;j++)
	    x=read(),f[op][i][j]=(f[op][i][j-1]*base1%mod+x)%mod; 
    for(int i=1;i<=n;i++)
      for(int j=1;j<=n;j++)
        f[op][i][j]=(f[op][i-1][j]*base2%mod+f[op][i][j])%mod;  	
  }
  l=0,r=n;
  while(l<r){
    int mid=(l+r+1)/2;
    if(chk(mid)) l=mid;
    else r=mid-1;
  }
  printf("%d\n",l);
  return 0;
}

P2601 二分+二维哈希

P2601 [ZJOI2009] 对称的正方形

同样是二分+二维哈希,但是感觉复杂多了。。。

为了不从很多方向做前缀和,我们考虑先对称,于是就简单多了。

所以还是去枚举中心点再二分就可以了。

它居然卡我模数?!这个东西只允许自然溢出,否则会 TLE。。。

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

const int N=2e3+3;
int n,m,a[N][N],l,r;
ll f[N][N],base1=229,base2=223,ans=0,p1[N],p2[N];

int read(){
  int x=0,F=1;char ch=getchar();
  while(!isdigit(ch)){if(ch=='-') F=-1;ch=getchar();}
  while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
  return x*F;
}

void init(){
  p1[0]=p2[0]=1ll;
  for(int i=1;i<N;i++) p1[i]=p1[i-1]*base1,p2[i]=p2[i-1]*base2;
}

ll h(int xa,int ya,int xb,int yb){
  return f[xb][yb]-f[xb][ya-1]*p1[yb-ya+1]-f[xa-1][yb]*p2[xb-xa+1]+f[xa-1][ya-1]*p2[xb-xa+1]*p1[yb-ya+1];
}

bool chk(int xa,int ya,int xb,int yb){
  if(xa<1||xb<1||ya<1||yb<1||xa>n||xb>n||ya>m||yb>m) return false;
  if(h(xa,ya,xb,yb)==h((n<<1)-xb+1,ya,(n<<1)-xa+1,yb)&&h(xa,ya,xb,yb)==h(xa,(m<<1)-yb+1,xb,(m<<1)-ya+1)) return true;
  return false;
}

int main(){
  /*2023.11.7 H_W_Y P2601 [ZJOI2009] 对称的正方形 String Hash*/
  n=read();m=read();init();
  for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++){
      a[i][j]=read();
      a[(n<<1)-i+1][j]=a[i][(m<<1)-j+1]=a[i][j];
	}
  for(int i=1;i<=(n<<1);i++)
    for(int j=1;j<=(m<<1);j++)
      f[i][j]=f[i][j-1]*base1+a[i][j];
  for(int i=1;i<=(n<<1);i++)
    for(int j=1;j<=(m<<1);j++)
      f[i][j]+=f[i-1][j]*base2;
  for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++){
      l=0,r=min(min(i,j),min(n-i,m-j));
      while(l<r){
      	int mid=((l+r+1)>>1);
		if(chk(i-mid+1,j-mid+1,i+mid,j+mid)) l=mid;
		else r=mid-1; 
	  }
	  ans+=l;
	  l=0;r=min(min(i-1,j-1),min(n-i,m-j));
	  while(l<r){
	  	int mid=((l+r+1)>>1);
		if(chk(i-mid,j-mid,i+mid,j+mid)) l=mid;
		else r=mid-1; 
	  }
	  ans+=l+1;
	}
  printf("%lld\n",ans);
  return 0;
}

SP4103 回文+哈希

SP4103 EPALIN - Extend to Palindrome

发现回文是要求从最后一个字符往前一定是回文的,

你就去枚举回文的长度,判断一下就可以了。

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

const int N=1e5+5;
int n;
char s[N];
ll base=223,pre[N],suf[N],p[N];

void init(){
  p[0]=1ll;
  for(int i=1;i<N;i++) p[i]=p[i-1]*base;
}

bool chk(int len){
  int l=n-len+1;len/=2;
  if(pre[l+len-1]-pre[l-1]==suf[n-len+1]*p[l-1]) return true;
  return false;
}

int main(){
  /*2023.11.7 H_W_Y SP4103 EPALIN - Extend to Palindrome String Hash*/
  init();
  while(scanf("%s",s+1)!=EOF){
  	n=strlen(s+1);
  	pre[0]=suf[n+1]=0;
  	for(int i=1;i<=n;i++) pre[i]=pre[i-1]+(s[i]-'a')*p[i];
  	for(int i=n;i>=1;i--) suf[i]=suf[i+1]+(s[i]-'a')*p[n-i+1];
  	int pos=0;
  	for(int i=n;i>=1;i--)
  	  if(chk(i)){pos=i;break;} 	
  	for(int i=1;i<=n;i++) putchar(s[i]);
  	for(int i=n-pos;i>=1;i--) putchar(s[i]);
  	putchar('\n');
  }
  return 0;
}

\(998244853\) 又被卡了。。。

P3167 通配符匹配 dp+哈希

P3167 [CQOI2014] 通配符匹配

是非常有难度的题目!

首先发现整个串是既有通配符又有给定字符——

这样夹杂着非常不好处理!!!

那我们如何处理呢?

我们考虑把整个串分成若干个部分,每一个部分都是以通配符开头,而以给定的字符结尾。

于是这样我们就可以得到一个 dp,

\(f_{i,j}\) 表示分出来的第 \(i\) 个串的结尾能否匹配上当前查询串的前 \(j\) 个字符。

这里令第 \(i\) 个串前面的通配符状态为 \(0,1,2\) 表示没有,\(?,*\) 三种情况。

那么我们就可以得到了以下的转移方程:

\[f_{i,j}= \begin{cases} f_{i-1,j-len_i} (op_i=0)\\ f_{i-1,j-len_i-1} (op_i=1)\\ \sum_{k=0}^{j-len_i} f_{i-1,k}(op_i=2) \end{cases} \]

而能否转移可以通过哈希 \(O(1)\) 实现。

由于题目中的通配符个数最多是 \(10\) 个,所以完全是没有问题的。

要善于把握题目中的特性。

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define pll pair<ll,ll>
#define fi first
#define se second
#define mk make_pair

const int N=1e5+5;
int n,len,cnt=0,m,lst=0;
bool f[12][N],g[12][N];
char s[N];
pll mod={19260817,1011451423},base={28,26},p[N],hs[N];
struct node{
  int op,len;
  pll hs;
}a[N];

pll operator *(pll x,pll y){return {x.fi*y.fi%mod.fi,x.se*y.se%mod.se};}
pll operator +(pll x,pll y){return {(x.fi+y.fi)%mod.fi,(x.se+y.se)%mod.se};}
pll operator -(pll x,pll y){return {(x.fi-y.fi+mod.fi)%mod.fi,(x.se-y.se+mod.se)%mod.se};}

void init(){
  p[0]={1,1};
  for(int i=1;i<N;i++) p[i]=p[i-1]*base;
}

int main(){
  /*2023.11.8 H_W_Y P3167 [CQOI2014] 通配符匹配 String Hash*/
  scanf("%s",s+1);
  init();len=strlen(s+1);lst=0;
  for(int i=1,j=1;i<=len;i++){
  	if(s[i]=='*'||s[i]=='?'){
  	  cnt++;
	  a[cnt].op=(s[i]=='?')?1:2;
	  a[cnt].len=0;
	  j=i;
	  while(j<len&&s[j+1]!='*'&&s[j+1]!='?'){
	  	j++;a[cnt].len++;
	  	a[cnt].hs=a[cnt].hs+mk(s[j]-'a',s[j]-'a')*p[a[cnt].len];
	  }
	  i=j;
	}
	else if(i==1){
	  cnt++;
	  a[cnt].op=0;
	  a[cnt].len=1;a[cnt].hs=mk(s[i]-'a',s[i]-'a')*p[1];
	  j=i;
	  while(j<len&&s[j+1]!='*'&&s[j+1]!='?'){
	  	j++;a[cnt].len++;
	  	a[cnt].hs=a[cnt].hs+mk(s[j]-'a',s[j]-'a')*p[a[cnt].len];
	  }
	  i=j;
	}
  }
  scanf("%d",&n);
  while(n--){
    scanf("%s",s+1);
    len=strlen(s+1);
    hs[0]={0,0};
    for(int i=1;i<=len;i++) hs[i]=hs[i-1]+p[i]*mk(s[i]-'a',s[i]-'a');
    f[0][0]=true;g[0][0]=true;
    for(int i=1;i<=len;i++) g[0][i]=true;
    for(int i=1;i<=cnt;i++)
      for(int j=1;j<=len;j++){
      	f[i][j]=false;g[i][j]=false;
      	if(j<a[i].len){g[i][j]=g[i][j-1];continue;}
      	if(a[i].op==0){
      	  if(f[i-1][j-a[i].len]&&a[i].hs*p[j-a[i].len]==(hs[j]-hs[j-a[i].len])) f[i][j]=true;	
	    }
	    if(a[i].op==1){
	      if(f[i-1][j-a[i].len-1]&&a[i].hs*p[j-a[i].len]==(hs[j]-hs[j-a[i].len])) f[i][j]=true;
		}
		if(a[i].op==2){
		  if(g[i-1][j-a[i].len]&&a[i].hs*p[j-a[i].len]==(hs[j]-hs[j-a[i].len])) f[i][j]=true;
		}
		g[i][j]=g[i][j-1]|f[i][j];
	  }
	puts(f[cnt][len]?"YES":"NO");
  }
  return 0;
}

P3449 Trie树+哈希

P3449 [POI2006] PAL-Palindromes

我们发现把两个串拼起来的时候,

假设长的串在前面,那么最后是回文的当且仅当,后面短的串翻转后是前面的串的前缀,

且前面的串的剩余部分是一个回文串。

但是我们发现题目中给出了原串也是回文串,

所以把这个回文串再翻转以下,我们可以得到只要短的是长的的前缀就可以了。

于是这个东西就是好处理的了,一定要把图画出来!

而对于后面的串比较长的情况我们也是可以以类似的方法处理的,这道题就做完了。

具体实现的时候就是在 Trie 树上走,感觉可以用 map,但是维护起来比较复杂。

写得太丑了,根本调不出来。。。于是重构。

我们并不需要把每一个字符串单独存,而我们其实可以把这些字符串存在一个字符串里面就可以了。

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

const int N=2e6+3;
int n,ch[N][26],val[N],idx=0,sz[N],len,sum=0;
char s[N],t[N];
ll base=28,ans,p[N],hs1[N],hs2[N];

void ins(char *t,int len){
  int nw=0;
  for(int i=1;i<=len;i++){
  	int tmp=t[i]-'a';
  	if(!ch[nw][tmp]) ch[nw][tmp]=++idx;
  	nw=ch[nw][tmp];
  }
  val[nw]++;
}

void find(char *t,int len){
  hs1[0]=hs2[len+1]=0;
  for(int i=1;i<=len;i++) hs1[i]=hs1[i-1]+p[i]*(t[i]-'a');
  for(int i=len;i>=1;i--) hs2[i]=hs2[i+1]+p[len-i+1]*(t[i]-'a');
  int nw=0;
  for(int i=1;i<=len;i++){
  	int tmp=t[i]-'a';
  	nw=ch[nw][tmp];
  	if(val[nw]){
  	  int l=i+1,r=len,mid=(r-l+1)/2;
	  if(r-l+1<2) ans+=val[nw];
	  else if((hs1[l+mid-1]-hs1[l-1])*p[len-r]==(hs2[r-mid+1]-hs2[r+1])*p[l-1]) ans+=val[nw];
	  int L=l,R=r;
  	  l=1,r=len-i,mid=(r-l+1)/2;
	  if(r-l+1<2) ans+=val[nw];
	  else if((hs1[l+mid-1]-hs1[l-1])*p[len-r]==(hs2[r-mid+1]-hs2[r+1])*p[l-1]) ans+=val[nw];	
	}
  }
  ans--;
}

int main(){
  /*2023.11.8 H_W_Y P3449 [POI2006] PAL-Palindromes String Hash*/
  scanf("%d",&n);len=0;
  for(int i=1;i<=n;i++){
  	scanf("%d%s",&sz[i],t);
  	for(int j=0;j<sz[i];j++) s[len+j+1]=t[j];
  	len+=sz[i];
  }
  p[0]=1ll;s[0]=' ';
  for(int i=1;i<=len;i++) p[i]=p[i-1]*base;
  sum=0;
  for(int i=1;i<=n;i++) ins(s+sum,sz[i]),sum+=sz[i];
  sum=0;
  for(int i=1;i<=n;i++) find(s+sum,sz[i]),sum+=sz[i];
  printf("%lld\n",ans);
  return 0;
}

CF504E 树上的哈希问题-好题!

CF504E Misha and LCP on Tree

给定一棵 \(n\) 个节点的树,每个节点有一个小写字母。

\(m\) 组询问,每组询问为树上 \(a \to b\)\(c \to d\) 组成的字符串的最长公共前缀。

\(n \le 3 \times 10^5,m \le 10^6\)

题解给了一个树剖的做法,但是我觉得可以直接用 LCA 完成。

就是直接记录每一个到根路径上的哈希值,用 dfn 序 \(O(1)\) 求 LCA,

查询的时候我们二分答案,可以 \(O(1)\) 求出两个前缀的哈希值,比较即可。

但是发现比较的过程我们需要 \(O(1)\) 找到 \(k\) 级祖先,需要用长链剖分解决。

由于不太会长链剖分,所以我们还是采用树剖。

树剖会把 \(a \to b\)\(c \to d\) 的路径分成 \(\log n\) 个区间,于是我们每次尝试去消掉一个区间。

而如果消不掉,就在里面二分答案找答案即可。

但实现的时候多少有点细节。

呜呜呜——不想写串串了,哈希什么**玩意?

但这道题确实挺好的。

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define pll pair<ll,ll>
#define pii pair<int,int>
#define fi first
#define se second
#define mp make_pair
#define pb push_back

const int N=3e5+5;
int n,m,a,b,c,d,head[N],tot=0,son[N],sz[N],fa[N],top[N],dfn[N],idx=0,rev[N],dep[N];
pll hs1[N],hs2[N],p[N],mod={19260817,1011451423},base={28,26};
char s[N];
struct edge{
  int v,nxt;
}e[N<<1];

int read(){
  int x=0,f=1;char ch=getchar();
  while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
  while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
  return x*f;
}

void print(int x){
  int P[15],tmp=0;
  if(x==0) putchar('0');
  if(x<0) putchar('-'),x=-x;
  while(x) P[++tmp]=x%10,x/=10;
  for(int i=tmp;i>=1;i--) putchar(P[i]+'0');
  putchar('\n');
}

void add(int u,int v){
  e[++tot]=(edge){v,head[u]};head[u]=tot;
  e[++tot]=(edge){u,head[v]};head[v]=tot;
}

pll operator *(pll x,pll y){return {x.fi*y.fi%mod.fi,x.se*y.se%mod.se};}
pll operator +(pll x,pll y){return {(x.fi+y.fi)%mod.fi,(x.se+y.se)%mod.se};}
pll operator -(pll x,pll y){return {(x.fi-y.fi+mod.fi)%mod.fi,(x.se-y.se+mod.se)%mod.se};}

void init(){
  p[0]={1,1};for(int i=1;i<N;i++) p[i]=p[i-1]*base;
}

void dfs1(int u,int pre){
  fa[u]=pre;sz[u]=1;son[u]=-1;dep[u]=dep[pre]+1;
  for(int i=head[u];i;i=e[i].nxt){
  	int v=e[i].v;
  	if(v==pre) continue;
  	dfs1(v,u);
  	sz[u]+=sz[v];
  	if(son[u]==-1||sz[v]>sz[son[u]]) son[u]=v;
  }
}

void dfs2(int u,int pre){
  top[u]=pre;dfn[u]=++idx,rev[idx]=u;
  if(son[u]==-1) return ;
  dfs2(son[u],pre);
  for(int i=head[u];i;i=e[i].nxt){
  	int v=e[i].v;
  	if(v==fa[u]||v==son[u]) continue;
  	dfs2(v,v);
  }
}

int lca(int u,int v){
  while(top[u]!=top[v]){
  	if(dep[top[u]]<dep[top[v]]) swap(u,v);
	u=fa[top[u]]; 
  }
  if(dep[u]>dep[v]) swap(u,v);
  return u;
}

vector<pii> find(int u,int v){
  int gf=lca(u,v);
  vector<pii> fir,sec;
  while(dep[top[u]]>dep[gf]) fir.pb({u,top[u]}),u=fa[top[u]];
  fir.pb({u,gf});
  while(dep[top[v]]>dep[gf]) sec.pb({top[v],v}),v=fa[top[v]];
  if(v!=gf) sec.pb({son[gf],v});
  while(sec.size()) fir.pb(sec.back()),sec.pop_back();
  return fir;
}

pll hs(bool op,int x,int len){
  if(op) return hs2[x-len+1]-hs2[x+1]*p[len];
  return hs1[x+len-1]-hs1[x-1]*p[len];
}

int main(){
  /*2023.11.8 H_W_Y CF504E Misha and LCP on Tree String Hash*/
  n=read();init();
  for(int i=1;i<=n;i++){
  	s[i]=getchar();
  	while(s[i]<'a'||s[i]>'z') s[i]=getchar();
  }
  for(int i=1,u,v;i<n;i++) u=read(),v=read(),add(u,v);
  dfs1(1,0);dfs2(1,1);
  for(int i=1;i<=n;i++) hs1[i]=hs1[i-1]*base+mp(s[rev[i]]-'a',s[rev[i]]-'a');
  for(int i=n;i>=1;i--) hs2[i]=hs2[i+1]*base+mp(s[rev[i]]-'a',s[rev[i]]-'a');
  m=read();
  while(m--){
  	a=read(),b=read(),c=read(),d=read();int ans=0;
  	vector<pii> f=find(a,b),g=find(c,d);
  	int itf=0,itg=0;
  	while(itf<(int)f.size()&&itg<(int)g.size()){
  	  int st1=dfn[f[itf].fi],ed1=dfn[f[itf].se];
	  int st2=dfn[g[itg].fi],ed2=dfn[g[itg].se];
	  bool op1=st1>ed1,op2=st2>ed2;
	  int l1=(op1?st1-ed1:ed1-st1)+1,l2=(op2?st2-ed2:ed2-st2)+1;
	  int len=min(l1,l2);
	  pll hf=hs(op1,st1,len);
	  pll hg=hs(op2,st2,len);
	  if(hf==hg){
	    if(len==l1) itf++;
	    else f[itf].fi=rev[st1+(op1?-1:1)*len];
	    if(len==l2) itg++;
	    else g[itg].fi=rev[st2+(op2?-1:1)*len];
	    ans+=len;
	  }else{
	  	int l=0,r=len;
	  	while(l<r){
	  	  int mid=(l+r+1)>>1;
		  hf=hs(op1,st1,mid);
		  hg=hs(op2,st2,mid);
		  if(hf==hg) l=mid;
		  else r=mid-1;	
		}
		ans+=l;break;
	  }
    }
    print(ans);
  }
  return 0;
}

P9576 二分+哈希+树状数组

P9576 「TAOI-2」Ciallo

感觉非常困难。。。

首先我们可以处理掉不删除的情况,

接下来我们就只需要处理要删除的情况。

很容易想到要去维护一个前缀和后缀,设 \(f_i,g_i\) 分别表示从 \(i\) 开始往后于 \(t\) 的最大前缀和往前与 \(t\) 的最大后缀,这个可以用二分+哈希预处理出来。

注意这里的值都要和 \(|t|-1\)\(\min\) ,因为我们钦定了它要删除。

于是现在我们就去枚举第一次选的区间 \([i,j]\) ,对答案的贡献,

首先这个区间需要满足 \(i+|t|-1 \lt j,f_i+g_j \ge |t|\)

那么贡献就是 \(f_i+g_j-|t|+1\) ,这个可能需要画图分析以下,中间删除的区间是可以随意选的,而长度是一定的。

所以总的式子就是:

\[\sum_{i=1}^n\sum_{j=i+|t|}^n [f_i+g_j \le |t|](f_i+g_j-|t|+1) \]

于是这个东西可以用树状数组维护,这道题就做完了。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pll pair<ll,ll>
#define fi first
#define se second

const int N=4e5+5;
int n,m,f[N],g[N];
ll ans=0;
pll tr[N];
char s[N],t[N];
ll mod=1011451423,base=28,p[N],hs1[N],hs2[N];

void init(){
  p[0]=1ll;
  for(int i=1;i<N;i++) p[i]=p[i-1]*base%mod;
}

ll hs(int op,int l,int r){
  if(op==1) return (hs1[r]-hs1[l-1]*p[r-l+1]%mod+mod)%mod;
  return (hs2[r]-hs2[l-1]*p[r-l+1]%mod+mod)%mod;
}

int lowbit(int i){return i&(-i);}
void upd(int x,int val){for(int i=x;i<=m;i+=lowbit(i)) tr[i].fi+=1ll*val,tr[i].se++;}
pll qry(int x){
  ll res=0,cnt=0;
  for(int i=x;i>=1;i-=lowbit(i)) res+=tr[i].fi,cnt+=tr[i].se;
  return {res,cnt};
}

int main(){
  /*2023.11.8 H_W_Y P9576 「TAOI-2」Ciallo String Hash*/
  scanf("%s%s",s+1,t+1);init();
  n=strlen(s+1),m=strlen(t+1);
  for(int i=1;i<=n;i++) hs1[i]=(hs1[i-1]*base%mod+(s[i]-'a'))%mod;
  for(int i=1;i<=m;i++) hs2[i]=(hs2[i-1]*base%mod+(t[i]-'a'))%mod;
  for(int i=1;i<=n;i++){
  	if(i+m-1<=n&&hs(1,i,i+m-1)==hs2[m]) ans+=1ll*(i-1)*(i-2)/2+(i-1)+1ll*(n-i-m+1)*(n-i-m)/2+(n-i-m+1);
	int l=0,r=min(m-1,i);
	while(l<r){
	  int mid=(l+r+1)/2;
	  if(hs(1,i-mid+1,i)==hs(2,m-mid+1,m)) l=mid;
	  else r=mid-1;
	} 
	g[i]=l;
	l=0,r=min(m-1,n-i+1);
	while(l<r){
	  int mid=(l+r+1)/2;
	  if(hs(1,i,i+mid-1)==hs(2,1,mid)) l=mid;
	  else r=mid-1;
	}
	f[i]=l;
  } 
  for(int i=n;i>=1;i--){
  	if(i+m<=n&&g[i+m]) upd(g[i+m],g[i+m]);
	pll r=qry(m),l=qry(m-f[i]-1);
	r.fi=r.fi-l.fi,r.se=r.se-l.se;
	ans+=(f[i]-m+1)*r.se+r.fi; 
	
  }
  printf("%lld\n",ans);
  return 0;
}

哈希——终于写完了。。。

KMP

CSP 之前背下来 KMP 模板的代码,原理还不是特别理解。。。

首先定义字符串 \(s\) 的 border 为串 \(t\) 满足 \(t\) 既是 \(s\) 的真前缀又是真后缀。

于是 KMP 算法可以在线性时间内求出 \(s\) 任意前缀的最长 border,即 \(nxt\) 数组。

很多时候我们不是去求字符串匹配,而是去求 \(nxt\) 数组解题。

而然 KMP 求匹配的时候我们就不断地跳 border 就可以了。

至于 \(nxt\) 数组的求法,也就是不断往前面跳就可以了。

放一个 KMP 的代码:

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

const int N=1e6+5;
int la,lb,kmp[N],j;
char a[N],b[N];

int main(){
  scanf("%s%s",a+1,b+1);
  la=strlen(a+1);lb=strlen(b+1);
  for(int i=2;i<=lb;i++){
  	while(j&&b[j+1]!=b[i]) j=kmp[j];
  	if(b[j+1]==b[i]) j++;
  	kmp[i]=j;
  } 
  j=0;
  for(int i=1;i<=la;i++){
  	while(j&&a[i]!=b[j+1]) j=kmp[j];
  	if(b[j+1]==a[i]) j++;
  	if(j==lb) cout<<(i-lb+1)<<'\n';
  }
  for(int i=1;i<=lb;i++) cout<<kmp[i]<<' ';
  cout<<'\n';
  return 0;
}

P5829 失配树

P5829 【模板】失配树

KMP 的应用有很多首先就是失配树。

因为 \(nxt_i \lt i\),那么我们从 \(nxt_i\)\(i\) 连边,最后会连出一棵有根树(\(0\) 为根)。

于是我们可以发现对于失配树上的每一个 \(i\) ,它的每一个祖先 \(j\) 都满足 \(s[1,j]\)\(s[1,i]\) 的 border。

而这道题就直接在失配树上面找 LCA 就可以了。

注意每一次是找 \(nxt[i]\) 的 LCA。

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

const int N=1e6+5;
int head[N],tot=0,n,m,nxt[N],dfn[N],st[22][N],lg[N],idx=0;
char s[N];
struct edge{
  int v,nxt;
}e[N<<1];

int read(){
  int x=0,f=1;char ch=getchar();
  while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
  while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}\
  return x*f; 
}

void add(int u,int v){
  e[++tot]=(edge){v,head[u]};head[u]=tot;
  e[++tot]=(edge){u,head[v]};head[v]=tot;
}

void dfs(int u,int fa){
  dfn[u]=++idx;
  st[0][idx]=fa;
  for(int i=head[u];i;i=e[i].nxt){
  	int v=e[i].v;
  	if(v==fa) continue;
  	dfs(v,u);
  }
}

int Min(int x,int y){return (dfn[x]<dfn[y]?x:y);}

void init(){
  lg[1]=0;lg[2]=1;
  for(int i=3;i<=n+1;i++) lg[i]=lg[i/2]+1;
  dfs(1,0);
  for(int i=1;i<=lg[n+1]+1;i++)
    for(int j=1;j+(1<<i)-1<=n+1;j++)
      st[i][j]=Min(st[i-1][j],st[i-1][j+(1<<(i-1))]);
}

int lca(int u,int v){
  if(u==v) return u;
  u=dfn[u];v=dfn[v];
  if(u>v) swap(u,v);
  int t=lg[v-u];
  return Min(st[t][u+1],st[t][v-(1<<t)+1]);
}

int main(){
  /*2023.11.8 H_W_Y P5829 【模板】失配树 KMP*/
  scanf("%s",s+1);n=strlen(s+1);
  int j=0;
  for(int i=2;i<=n;i++){
  	while(j&&s[j+1]!=s[i]) j=nxt[j];
  	if(s[j+1]==s[i]) j++;
  	nxt[i]=j;
  }
  for(int i=1;i<=n;i++) add(i+1,nxt[i]+1);
  init();
  m=read();
  for(int i=1;i<=m;i++){
  	int l=read(),r=read();
  	printf("%d\n",lca(nxt[l]+1,nxt[r]+1)-1);
  }
  return 0;
}

P2375 [NOI2014] 动物园

P2375 [NOI2014] 动物园

不知道看错了多少遍题目。

就是对于每一个 \(i\) ,我们求前 \(i\) 个字符的不重叠 border 的数量。

发现我们求 border 的时候每一次是去跳 nxt 而每一次跳到的差距也就是加一。

所以我们就可以预处理出每一个串的最长 border 的长度,

于是再进行一次 kmp 的匹配,

如果长度大于了 \(2/i\) 就继续跳下去,直到跳到了该到的地方加上 ans 就行了。

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

const int N=1e6+5;
int T,n,nxt[N],num[N];
ll ans=0;
const ll mod=1e9+7;
char s[N];

void sol(){
  scanf("%s",s+1);n=strlen(s+1);
  num[1]=1;
  for(int i=2,j=0;i<=n;i++){
  	while(j&&s[i]!=s[j+1]) j=nxt[j];
  	if(s[i]==s[j+1]) j++;
  	nxt[i]=j;num[i]=num[j]+1;
  }ans=1ll;
  for(int i=2,j=0;i<=n;i++){
  	while(j&&s[i]!=s[j+1]) j=nxt[j];
  	if(s[i]==s[j+1]) j++;
  	if((j<<1)>i) j=nxt[j];
  	ans=ans*(num[j]+1)%mod;
  }
  printf("%lld\n",ans);
}

int main(){
  /*2023.11.11 H_W_Y P2375 [NOI2014] 鍔ㄧ墿鍥?KMP*/
  scanf("%d",&T);
  while(T--) sol();
  return 0;
}

CF432D KMP+前缀出现次数(奇妙dp)

CF432D Prefixes and Suffixes

给你一个长度为 \(n\) 的长字符串,求它的 border 的个数且统计这些 border 作为子串在字符串中出现的次数。

\(1 \le n \le 10^5\)

很明显,border 直接用 KMP 去求就可以了。

于是答案输出的时候长度就是不断跳 border 的值,

现在考虑如何算子串的出现次数。

发现每一个串,它会在它的 border 里面多出现一次,

所以我们设 \(f_i\) 表示长为 \(i\) 的前缀的出现次数,

那么 \(f_{nxt[i]}+=f_i\),这也是很好感受到的,初始值都是 \(1\) 就可以了。

这种都 dp 就可以直接完成啦。于是这道题就做完了。

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

const int N=1e6+5;
int n,nxt[N],f[N],ans[N],cnt;
char s[N];

void print(int x,char ch){
  int p[15],tmp=0;
  if(x==0) putchar('0');
  if(x<0) putchar('-'),x=-x;
  while(x) p[++tmp]=x%10,x/=10;
  for(int i=tmp;i>=1;i--) putchar(p[i]+'0');
  putchar(ch);
}

int main(){
  /*2023.11.12 H_W_Y CF432D Prefixes and Suffixes KMP*/ 
  scanf("%s",s+1);n=strlen(s+1);
  for(int i=2,j=0;i<=n;i++){
  	while(j&&s[i]!=s[j+1]) j=nxt[j];
  	if(s[i]==s[j+1]) j++;
  	nxt[i]=j;
  }
  for(int i=n;i>=1;i--)
  	f[i]++,f[nxt[i]]+=f[i];
  int j=n;cnt=0;
  while(nxt[j]) ans[++cnt]=nxt[j],j=nxt[j];
  ans[++cnt]=n;sort(ans+1,ans+cnt+1);
  print(cnt,'\n');
  for(int i=1;i<=cnt;i++) print(ans[i],' '),print(f[ans[i]],'\n');
  return 0;
}

P3435 最小 border+KMP

P3435 [POI2006] OKR-Periods of Words

就是求最小 border 之和,你在算 KMP 的时候记录一下是不是第一次就可以了。

KMP 板子写错了——不愧是我,搞了好一会儿。

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

const int N=1e6+5;
int n,nxt[N],ans[N];
char s[N];
ll res=0;

int main(){
  /*2023.11.12 H_W_Y P3435 [POI2006] OKR-Periods of Words KMP*/
  scanf("%d%s",&n,s+1);ans[1]=1;
  for(int i=2,j=0;i<=n;i++){
  	while(j&&s[j+1]!=s[i]) j=nxt[j];
	if(s[j+1]==s[i]) j++;
	nxt[i]=j;
	if(j) ans[i]=ans[j];
    else ans[i]=i;
  }
  for(int i=1;i<=n;i++) res+=(i-ans[i]);
  printf("%lld\n",res);
  return 0;
} 

P3193 KMP+矩阵快速幂+dp 好题!

P3193 [HNOI2008] GT考试

求有多少个 \(n\) 位的数字串 \(s\) 不包含给定的 \(m\) 位的字符串 \(t\)

\(n \le 10^9 , m \le 20\)

一来就没看 \(n\) 的数据范围,感觉是个简单 dp,就直接设 \(f_{i,j}\) 表示 \(s\) 匹配到 \(i\) 位,\(t\) 匹配到 \(j\) 位的方案数。

那么很明显最后的答案就是 \(\sum_{i=1}^{m-1} f_{n,i}\)

于是 \(f_{i,j}\) 就是从 \(f_{i-1,j-1}\) 转移过来就行了。

但是发现没有意识到一个严重的问题:

为什么 \(f_{i,j}\) 必须是 \(f_{i-1,j-1}\) 转移过来呢?

有没有可能是什么东西失配了又转移到这里来的。

这下复杂了。。。

我们设 \(g_{k,j}\) 表示长为 \(k\) 的前缀转移到长为 \(j\) 的前缀的方案数,

这是比较好处理的,就是 KMP 中它的某一个 border 再加上一个数。

于是转移方程就是:

\[f_{i,j}=\sum_{k=0}^{m-1} f_{i-1,k}\times g_{k,j} \]

就得到了一个 \(O(nm^2)\) 的做法。

发现 \(n\) 的范围高达 \(1e9\) 很明显是无法完成的。

但是我们发现 dp 的式子很像矩阵乘法,于是用矩阵快速幂去优化一下就可以了。

实现的时候我们直接不考虑不合法的情况。

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

const int N=1e3+5;
int n,m,mod,nxt[N],ans;
char s[N];

struct matrix{
  int g[22][22];
  matrix operator *(const matrix &a) const{
    matrix res;memset(res.g,0,sizeof(res.g));
    for(int i=0;i<m;i++)
      for(int j=0;j<m;j++)
        for(int k=0;k<m;k++) 
          res.g[i][j]=(res.g[i][j]+g[i][k]*a.g[k][j]%mod)%mod;
    return res;
  }
}g;

matrix qpow(matrix x,int n){
  matrix res;
  memset(res.g,0,sizeof(res.g));
  for(int i=0;i<m;i++) res.g[i][i]=1;
  while(n){if(n&1) res=res*x;x=x*x;n>>=1;}
  return res;
}

int main(){
  /*2023.11.12 H_W_Y P3193 [HNOI2008] GT考试 KMP*/
  scanf("%d%d%d",&n,&m,&mod);
  scanf("%s",s+1);
  for(int i=2,j=0;i<=m;i++){
  	while(j&&s[j+1]!=s[i]) j=nxt[j];
  	if(s[j+1]==s[i]) j++;
  	nxt[i]=j;
  }
  memset(g.g,0,sizeof(g.g));
  for(int i=0;i<m;i++){
  	for(char ch='0';ch<='9';ch++){
  	  int j=i;
	  while(j&&s[j+1]!=ch) j=nxt[j];
	  if(s[j+1]==ch) ++j;
	  g.g[i][j]++;
	}
  }
  g=qpow(g,n);
  for(int i=0;i<m;i++) ans=(ans+g.g[0][i])%mod;
  printf("%d\n",ans);
  return 0;
}

P3426 dp+KMP

P3426 [POI2005] SZA-Template

你打算用印章在纸上印一串字母。印章每使用一次,就会将印章上的所有字母印到纸上。同一个位置的相同字符可以印多次。

例如:用 aba 这个印章可以完成印制 ababa 的工作(中间的 a 被 印了两次)。但是,在同一位置上印不同字符是不允许的。

求出印章的最小长度。

\(n \le 5 \times 10^5\)

感觉很妙的题目。

首先设 \(f_i\) 表示覆盖到 \(i\) 位的最小印章长度,

那么既然覆盖到了 \(i\) ,那么 \(i\) 的 border 一定是被覆盖过了的。

于是 \(f_i\) 的答案一定是 \(f_{nxt_i}\) 或者 \(i\)

而当答案取在 \(f_{nxt_i}\) 时,说明 \(f_{i-nxt_i \sim i-1}\) 中间有一个是 \(f_{nxt_i}\) 这也很好证明,画图推一下即可。

这个的实现也是非常简单的,直接用桶维护一下就可以了。

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

const int N=1e6+5;
int n,f[N],nxt[N],t[N];
char s[N];

int main(){
  /*2023.11.13 H_W_Y P3426 [POI2005] SZA-Template KMP+dp*/
  scanf("%s",s+1);
  n=strlen(s+1);
  for(int i=2,j=0;i<=n;i++){
  	while(j&&s[j+1]!=s[i]) j=nxt[j];
  	if(s[j+1]==s[i]) j++;
  	nxt[i]=j;
  }
  f[1]=1;t[1]=1;
  for(int i=2;i<=n;i++){
  	if(t[f[nxt[i]]]>=i-nxt[i]) f[i]=f[nxt[i]];
  	else f[i]=i;
  	t[f[i]]=i;
  }
  printf("%d\n",f[n]);
  return 0;
}

P3546 前后缀循环同构问题 KMP+哈希

P3546 [POI2012] PRE-Prefixuffix

给出一个长度为 \(n\) 的串 \(S\),求满足下面条件的最大的 \(L(L \le \frac{n}{2} )\)\(S\) 长为 \(L\) 的前缀和长为 \(L\) 的后缀是循环同构的。

\(n \le 10^6\)

用 Z 函数和 KMP 都胡了一下,发现都不太对。

感觉不好去处理中间某一串的 border。

把字符串换一种表示方式表示出来,发现就是 \(ABCBA\)

那我们其实可以从中间一种往外拓展找 border。

加设再 \(i\) 位置的 border 为 \(len\) ,发现在 \(i+1\) 处的 border 一定 \(\le len+2\)

因为画图分析一下,如果大于了 \(len+2\)

那么 \(len\) 就不是 \(i\) 位置最长的了。

所以我们拓展的时候直接用哈希就可以了,

时间复杂度是 \(O(n)\) 的。

注意枚举开始的位置!其实也没有用到 KMP 。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pll pair<ll,ll>
#define fi first
#define se second
#define mp make_pair

const int N=1e6+5;
int n,res=0,ans=0,st=0;
char s[N];
pll hs[N],mod={1011451423,19260817},base={223,229},pre[N];

pll operator *(pll x,pll y){return {x.fi*y.fi%mod.fi,x.se*y.se%mod.se};}
pll operator -(pll x,pll y){return {(x.fi-y.fi+mod.fi)%mod.fi,(x.se-y.se+mod.se)%mod.se};}
pll operator +(pll x,pll y){return {(x.fi+y.fi)%mod.fi,(x.se+y.se)%mod.se};}

void init(){
  pre[0]={1,1};
  for(int i=1;i<=n;i++) pre[i]=pre[i-1]*base;
}

pll find(int l,int r){return (hs[r]-hs[l-1]*pre[r-l+1]);}

int main(){
  /*2023.11.13 H_W_Y P3546 [POI2012] PRE-Prefixuffix KMP+哈希*/
  scanf("%d%s",&n,s+1);
  init();ans=0;
  for(int i=1;i<=n;i++) hs[i]=hs[i-1]*base+mp(s[i]-'a',s[i]-'a');
  if(n%2==0) st=n/2+1;
  else st=n/2+2;
  for(int i=st;i<=n;i++){
  	for(int j=min(res+2,(i-st+1));j>=0;j--)
  	  if(find(i-j+1,i)==find(n-i+1,n-i+j)){res=j;break;}
    if(find(i+1,n)==find(1,n-i)) ans=max(ans,n-i+res);
  }
  printf("%d\n",ans);
  return 0;
}

CF961F 双倍经验

CF961F k-substrings

和上一道题基本上差不多。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pll pair<ll,ll>
#define fi first
#define se second
#define mp make_pair

const int N=1e6+5;
int n,st=0,f[N];
char s[N];
pll hs[N],mod={1011451423,19260817},base={223,229},pre[N];

pll operator *(pll x,pll y){return {x.fi*y.fi%mod.fi,x.se*y.se%mod.se};}
pll operator -(pll x,pll y){return {(x.fi-y.fi+mod.fi)%mod.fi,(x.se-y.se+mod.se)%mod.se};}
pll operator +(pll x,pll y){return {(x.fi+y.fi)%mod.fi,(x.se+y.se)%mod.se};}

void init(){
  pre[0]={1,1};
  for(int i=1;i<=n;i++) pre[i]=pre[i-1]*base;
}

pll find(int l,int r){return (hs[r]-hs[l-1]*pre[r-l+1]);}

int main(){
  /*2023.11.13 H_W_Y P3546 [POI2012] PRE-Prefixuffix KMP+哈希*/
  scanf("%d%s",&n,s+1);
  init();
  for(int i=1;i<=n;i++) hs[i]=hs[i-1]*base+mp(s[i]-'a',s[i]-'a');
  st=(n-1)/2;f[st+1]=-1;
  for(int i=st;~i;i--){
    f[i]=f[i+1]+2;
    while(~f[i]&&i*2+f[i]>=n) f[i]-=2;
    while(~f[i]&&find(i+1,i+f[i])!=find(n-i-f[i]+1,n-i)) f[i]-=2;
  }
  for(int i=0;i<=st;i++) printf("%d ",f[i]);
  return 0;
}

CF808G 最多匹配次数 KMP

CF808G Anthem of Berland

给定 \(s\)\(t\) ,其中 \(s\) 串某些位置包含问号。你需要给把每个问号变成一个小写字母。

\(t\) 匹配 \(s\) 的次数为 \(f\),请输出 \(max(f)\)

\(|s|, |t| \le 10^5 , |s| \times |t| \le 10^7\)

没有注意到 \(|s| \times |t| \le 10^7\) 的条件,虚空想了好久。。。

而有了这个条件就很简单了。

我们先把 \(t\) 的所有 border 求出来,

匹配的时候不断跳 border 就行了。

具体来说就是维护 \(g_i\) 表示到第 \(i\) 位要选最后的答案,

\(f_i\) 就是到第 \(i\) 为的最大答案,分类讨论一下就可以了。

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

const int N=1e5+5;
int n,m,f[N],g[N],nxt[N];
char s[N],t[N];

int main(){
  /*2023.11.13 H_W_Y CF808G Anthem of Berland KMP*/
  scanf("%s%s",s+1,t+1);
  n=strlen(s+1);m=strlen(t+1);
  for(int i=2,j=0;i<=m;i++){
  	while(j&&t[j+1]!=t[i]) j=nxt[j];
  	if(t[j+1]==t[i]) j++;
  	nxt[i]=j;
  }
  for(int i=1;i<=n;i++){
  	f[i]=f[i-1];
  	if(i<m) continue;
  	bool flag=true;
  	for(int j=1;j<=m;j++)
  	  if(s[i-m+j]!='?'&&s[i-m+j]!=t[j]){flag=false;break;}
  	if(!flag) continue;
  	int j=m;
  	g[i]=max(g[i],f[i-m]+1);
  	while(j){
  	  g[i]=max(g[i],g[i-m+nxt[j]]+1);
  	  j=nxt[j];
  	}
  	f[i]=max(f[i],g[i]);
  }
  printf("%d\n",f[n]);
  return 0;
}

exKMP Z函数

P5410 扩展 KMP(Z 函数)

P5410 【模板】扩展 KMP/exKMP(Z 函数)

Z 函数就是用来求每一个后缀与全串的 LCP,

这个也是可以递归求到的,题解讲的挺清楚的。

于是这里就不写了吧。

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

const int N=2e7+5;
ll nxt[N],ext[N];
int n,m;
char a[N],b[N];
ll ans=0;

void gnxt(char *s){
  int len=strlen(s);
  nxt[0]=len;
  int k=1,p=0,l;
  while(p+1<len&&s[p]==s[p+1]) p++;
  nxt[1]=p;
  for(int i=2;i<len;i++){
  	p=k+nxt[k]-1,l=nxt[i-k];
  	if(i+l<=p) nxt[i]=l;
	else{
	  int j=max(0,p-i+1);
	  while(i+j<len&&s[j]==s[i+j]) j++;
	  nxt[i]=j;k=i;
	} 
  }
}

void gext(char *a,char *b){
  int la=strlen(a),lb=strlen(b);
  int p=0,k=0,l;
  while(p<la&&p<lb&&a[p]==b[p]) p++;
  ext[0]=p;
  for(int i=1;i<la;i++){
  	p=k+ext[k]-1,l=nxt[i-k];
  	if(i+l<=p) ext[i]=l;
  	else{
  	  int j=max(0,p-i+1); 
	  while(i+j<la&&j<lb&&a[i+j]==b[j]) j++;
	  ext[i]=j;k=i;	
	}
  }
}

int main(){
  /*2023.11.8 H_W_Y P5410 【模板】扩展 KMP/exKMP(Z 函数) exKMP*/
  scanf("%s%s",a,b);
  
  gnxt(b);gext(a,b);
  n=strlen(a);m=strlen(b);
  for(int i=0;i<m;i++) ans^=((i+1)*(nxt[i]+1));
  printf("%lld\n",ans);ans=0;
  for(int i=0;i<n;i++) ans^=((i+1)*(ext[i]+1));
  printf("%lld\n",ans);
  return 0;
}

P7114 字符串匹配 Z函数

P7114 [NOIP2020] 字符串匹配

求把一个串分成 \(ABABAB \dots ABC\) 的方案数,\(A,B,C\) 非空。

要求 \(C\) 中出现奇数次的字符数 \(\ge\) \(A\) 中出现奇数次的字符数。

\(1 \le |S| \le 2^{20}\)

直接对于每一个后缀求出它的 Z 函数,

于是我们从前往后枚举循环节是什么,根据上面那道找最小周期的题的结论,

我们可以算出这个循环节的出现次数,于是再用树状数组维护出现奇数次的字符个数就可以了。

写还是比较好些的,注意 Z 函数板子不要写错了!

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

const int N=(1<<20)+5;
int n,nxt[N],c[N],tr[30],v[30];
ll ans=0;
char s[N];

int lowbit(int i){return i&(-i);}
void upd(int x,int val){for(int i=x+1;i<=27;i+=lowbit(i)) tr[i]+=val;}
int qry(int x){
  int res=0;
  for(int i=x+1;i>=1;i-=lowbit(i)) res+=tr[i];
  return res;
}

void gnxt(char *s){
  int len=strlen(s);
  nxt[0]=len;
  int k=1,p=0,l;
  while(p+1<len&&s[p]==s[p+1]) p++;
  nxt[1]=p;
  for(int i=2;i<len;i++){
  	p=k+nxt[k]-1,l=nxt[i-k];
  	if(i+l<=p) nxt[i]=l;
	else{
	  int j=max(0,p-i+1);
	  while(i+j<len&&s[j]==s[i+j]) j++;
	  nxt[i]=j;k=i;
	} 
  }
}

void sol(){
  scanf("%s",s);n=strlen(s);
  memset(tr,0,sizeof(tr));
  memset(nxt,0,sizeof(nxt));
  memset(c,0,sizeof(c));
  memset(v,0,sizeof(v));
  gnxt(s);
  for(int i=n-1;i>=0;i--){
  	v[s[i]-'a']^=1;
  	if(v[s[i]-'a']==1) c[i]=c[i+1]+1;
	else c[i]=c[i+1]-1; 
  }
  int nw=0;memset(v,0,sizeof(v));ans=0;
  for(int i=1;i<n;i++){
  	if(i>1&&nxt[i]>=i){
  	  int r=i+nxt[i],cnt=r/i;r=cnt*i;
  	  if(r==n) r-=i,cnt-=1;
  	  if(cnt&1) ans+=qry(c[r])*(cnt/2+1)+qry(c[r-i])*(cnt/2);
  	  else ans+=qry(c[r])*(cnt/2)+qry(c[r-i])*(cnt/2);
	}
	else ans+=qry(c[i]);
	v[s[i-1]-'a']^=1;
	if(v[s[i-1]-'a']) nw++;
	else nw--;
	upd(nw,1);
  }
  printf("%lld\n",ans);
}

int main(){
  int T;scanf("%d",&T);
  while(T--) sol();
  return 0;
}

CF526D 周期问题+Z 函数

CF526D Om Nom and Necklace

已知长度为 \(n\) 的字符串 \(s\),给定 \(k\),对于 \(s\) 的每一个前缀子串, 判断是否满足 \(ABABA\dots BA\) 的形式(\(A、B\) 可以为空,也可以是一个字符串,\(A\)\(k + 1\) 个,\(B\)\(k\) 个)。

\(n \le 10^6\)

转化一下条件,变成 \(AAAAA \dots AB\),其中 \(B\)\(A\) 的前缀,

就和上一道题非常类似了。

于是我们直接用 Z 函数完成,同样也是去枚举周期的长度,

最后再用上一个 nxt 的值得到最长的前缀即可,

对于答案我们通过差分数组去维护。

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

const int N=1e6+5;
int n,k,nxt[N],d[N];
char s[N];

void gnxt(){
  int k=1,p=0,l=0;nxt[0]=n;
  while(p+1<n&&s[p]==s[p+1]) p++;nxt[1]=p;
  for(int i=2;i<n;i++){
  	l=nxt[i-k],p=k+nxt[k]-1;
  	if(i+l<=p) nxt[i]=l;
  	else {
  	  int j=max(p-i+1,0);
	  while(i+j<n&&s[i+j]==s[j]) j++;
	  nxt[i]=j;k=i;	
	}
  }
}

int main(){
  /*2023.11.12 H_W_Y CF526D Om Nom and Necklace exKMP*/
  scanf("%d%d",&n,&k);
  scanf("%s",s);gnxt();
  if(k==1){
  	for(int i=1;i<=n;i++) putchar('1');
  	return 0;
  }
  for(int i=1;i<=n;i++){
  	if(nxt[i]>=i&&(nxt[i]+i)/i>=k){
  	  int r=i*k;
	  d[r-1]++;d[min(r+nxt[r],r+i)]--;
	}
  }
  for(int i=0;i<n;i++) d[i]+=d[i-1],putchar(d[i]>0?'1':'0');
  return 0; 
}

AC 自动机

还是一个很薄弱的板块,虽然思路都知道,而且之前学过,

但是好久没写了,直接不会写了。——况且我之前的板子还没过二次加强版。

自动机的一些定义感觉还有些复杂,在联赛之前先不去研究了。

ACAM 其实解决的就是给出 \(n\) 个模式串和一个文本串,问你有多少个模式串在文本串中出现过。

具体做法即通过失配指针建出一棵 \(fail\) 树,

查询的时候直接在上面跳就可以了。

P5357 【模板】AC 自动机

P5357 【模板】AC 自动机(二次加强版)

板子题,但是还是要放上。

有一个拓扑排序的优化。

感觉还挺好写的。

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

const int N=2e6+5;
int n,vis[N],num[N],idx=0,in[N];
char s[N],t[N];
struct node{
  int fail,s[26],flag,ans;
  void init(){memset(s,0,sizeof(s));ans=flag=fail=0;}
}tr[N];

void ins(char *s,int id){
  int p=1,len=strlen(s);
  for(int i=0;i<len;i++){
  	int v=s[i]-'a';
  	if(!tr[p].s[v]) tr[p].s[v]=++idx;
  	p=tr[p].s[v];
  }
  if(!tr[p].flag) tr[p].flag=id;
  num[id]=tr[p].flag;
}

void getfail(){
  for(int i=0;i<26;i++) tr[0].s[i]=1;
  queue<int>q;
  q.push(1);
  while(!q.empty()){
  	int u=q.front();q.pop();
  	int f=tr[u].fail;
  	for(int i=0;i<26;i++){
  	  int v=tr[u].s[i];
  	  if(!v){tr[u].s[i]=tr[f].s[i];continue;}
  	  tr[v].fail=tr[f].s[i];in[tr[v].fail]++;
  	  q.push(v);
  	}
  }
}

void dfs(){
  queue<int> q;
  for(int i=1;i<=idx;i++) if(!in[i]) q.push(i);
  while(!q.empty()){
  	int u=q.front();q.pop();
  	vis[tr[u].flag]=tr[u].ans;
  	int v=tr[u].fail;in[v]--;
  	tr[v].ans+=tr[u].ans;
  	if(!in[v]) q.push(v);
  }
}

void qry(char *s){
  int u=1,len=strlen(s);
  for(int i=0;i<len;i++) u=tr[u].s[s[i]-'a'],tr[u].ans++;
}

int main(){
  /*2023.11.14 H_W_Y P5357 【模板】AC 自动机(二次加强版) ACAM*/
  scanf("%d",&n);idx=1;
  for(int i=1;i<=n;i++) scanf("%s",s),ins(s,i);
  getfail();scanf("%s",t);
  qry(t);dfs();
  for(int i=1;i<=n;i++) printf("%d\n",vis[num[i]]);
  return 0;
}

P3121 栈+ACAM

P3121 [USACO15FEB] Censoring G

给定一个字符串 \(S\),和 \(n\) 个违禁串 \(t_i\),每次删除原串中最靠前的违禁子串,直到没有为止,输出最终的串。注意删掉一个违禁串后可能形成新的违禁串。 \(|S| \le 10^5\)

之前做过的一道题。

我们用栈维护每一个匹配的节点,删除就直接跳转到之前的那个点就行了。

感觉代码非常不可看。

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

const int maxn=1e5+10;
int n,val[maxn*10],ch[maxn*10][30],fail[maxn*10],f[maxn],sz=0,top=0,len=0,mx[maxn*10];
char st[maxn],s[maxn],t[maxn];

void add(char *t){
  int u=0,m=strlen(t);
  for(int i=0;i<m;i++){
  	int c=t[i]-'a';
  	if(!ch[u][c]) ch[u][c]=++sz;
  	u=ch[u][c];val[u]=0;
  }
  val[u]=m;
}

void getfail(){
  queue<int> q;
  int u=0;fail[0]=0;
  for(int i=0;i<26;i++)
    if(ch[0][i]) q.push(ch[0][i]);
  while(!q.empty()){
  	int r=q.front();q.pop();
  	for(int i=0;i<26;i++){
  	  u=ch[r][i];
	  if(!u){ch[r][i]=ch[fail[r]][i];continue;}
	  int v=fail[r];
	  while(v&&!ch[v][i]) v=fail[v];
	  fail[u]=ch[v][i];
	  q.push(u);
	}
	mx[r]=max(val[r],val[fail[r]]);
  }
}

int main(){
  scanf("%s",s);len=strlen(s);
  scanf("%d",&n);
  for(int i=1;i<=n;i++) scanf("%s",t),add(t);
  getfail();
  for(int i=0;i<len;i++){
  	st[++top]=s[i];
  	int c=s[i]-'a',rt;
  	rt=f[top-1];
  	rt=ch[rt][c];
  	f[top]=rt;
  	if(mx[rt]) top-=mx[rt];
  }
  for(int i=1;i<=top;i++) printf("%c",st[i]);
  printf("\n");
  return 0;
}

P4052 文本生成器 ACAM+dp 好题!

P4052 [JSOI2007] 文本生成器

给定 \(n\) 个单词,求出长为 \(m\) 的所有字符串中,至少包含一个单词作为子串的数量。

\(n \le 60, m \le 100\),单词长度不超过 \(100\)

考虑我们只要计算出一次都没有出现过的,最后用总方案数减去答案即可。

在 ACAM 上面进行 dp,是一个比较套路的过程:

\(f_{i,j}\) 表示走到字符串的第 \(i\) 位,在 ACAM 上面的第 \(j\) 个节点的方案数。

我们在每一个节点是否是结尾节点的地方打上标记,

如果不是,就可以继续往下走。

注意如果 \(fail\) 是一个结尾的节点,这个节点也要打标记。

于是直接这样做 dp 就可以了,代码是比较好理解的。

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

const int N=1e4+5,M=105,mod=1e4+7;
int n,m,f[M][N],sum=0,ans=0,idx=0;
int vis[N],son[N][26],fail[N];
char s[N];

void ins(char *s){
  int p=0,len=strlen(s);
  for(int i=0;i<len;i++){
  	int tmp=s[i]-'A';
  	if(!son[p][tmp]) son[p][tmp]=++idx;
  	p=son[p][tmp];
  }
  vis[p]|=1;
}

void getfail(){
  queue<int>q;
  for(int i=0;i<26;i++) if(son[0][i]) q.push(son[0][i]);
  while(!q.empty()){
  	int u=q.front();q.pop();
  	for(int i=0;i<26;i++){
  	  int v=son[u][i];
  	  if(!v){son[u][i]=son[fail[u]][i];continue;}
  	  fail[son[u][i]]=son[fail[u]][i];
  	  vis[v]|=vis[son[fail[u]][i]];
  	  q.push(son[u][i]);
  	}
  }
}

int qpow(int a,int b){
  int res=1;
  while(b){if(b&1) res=res*a%mod;a=a*a%mod;b>>=1;}
  return res;
}

int main(){
  /*2023.11.14 H_W_Y P4052 [JSOI2007] 文本生成器 ACAM*/
  scanf("%d%d",&n,&m);
  for(int i=1;i<=n;i++) scanf("%s",s),ins(s);
  getfail();sum=qpow(26,m);f[0][0]=1;
  for(int i=1;i<=m;i++)
    for(int j=0;j<=idx;j++)
      for(int k=0;k<26;k++)
        if(!vis[son[j][k]])
          f[i][son[j][k]]=(f[i][son[j][k]]+f[i-1][j])%mod;
  for(int i=0;i<=idx;i++) ans=(ans+f[m][i])%mod;
  printf("%d\n",(sum-ans+mod)%mod);
  return 0;
}

P2292 L 语言 ACAM+dp+(状压)

P2292 [HNOI2004] L 语言

我们称一段文章 \(T\) 在某个字典 \(D\) 下是可以被理解的,是指如果文章 \(T\) 可以被分成若干部分,且每一个部分都是字典 \(D\) 中的单词。给定一个字典 \(D\),你的程序需要判断 \(m\) 段文章 \(t\) 在有 \(n\) 个单词的字典 \(D\) 下是否能够被理解。并给出其在字典 \(D\) 下能够被理解的最长前缀的位置。

\(n \le 20, m \le 50, |t| \le 2 \times 10^6\),单词长度不超过 \(20\)

没有说需要在第一个相同的地方就匹配,所以我们需要用 dp。

dp 的过程也很好想,就是一个不断跳 \(fail\) 的过程。

加一点小小的优化可以卡过去,如果想严格 \(O(len)\) 需要把过程直接预处理出来。

也是很好像的,就直接再跳 \(fail\) 的时候处理出来就可以了,用状压维护每一个 \(fail\) 的节点是不是一个单词的结束。

下面的代码没有加优化。

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

const int N=1e3+5,M=2e6+5;
int son[N][26],vis[N],n,m,idx=0,fail[N],dep[N];
char S[N],T[M];
bool f[N];

void ins(char* s){
  int p=0,len=strlen(s);
  for(int i=0;i<len;i++){
  	int tmp=s[i]-'a';
  	if(!son[p][tmp]) son[p][tmp]=++idx;
  	p=son[p][tmp];
  }
  vis[p]|=1;
}

void getfail(){
  queue<int > q;dep[0]=0;
  for(int i=0;i<26;i++) if(son[0][i]) q.push(son[0][i]),dep[son[0][i]]=1;
  while(!q.empty()){
    int u=q.front();q.pop();
    for(int i=0;i<26;i++){
      int v=son[u][i];
      if(!v){son[u][i]=son[fail[u]][i];continue;}
      fail[v]=son[fail[u]][i];
      q.push(v);dep[v]=dep[u]+1;
    }
  }
}

int qry(char* s){
  int len=strlen(s+1),p=0,ans=0;
  f[0]=true;
  for(int i=1;i<=len;i++){
    if(ans+20<i) break;
  	int tmp=s[i]-'a';f[i]=false;
    int nw=son[p][tmp];
    while(nw){
      f[i]|=(f[i-dep[nw]]&vis[nw]);
      nw=fail[nw];
      if(f[i]) break;
    }
    p=son[p][tmp];
    if(f[i]) ans=i;
  }
  return ans;
}

int main(){
  /*2023.11.14 H_W_Y P2292 [HNOI2004] L 语言 Trie*/
  scanf("%d%d",&n,&m);
  for(int i=1;i<=n;i++) scanf("%s",S),ins(S);
  getfail();
  for(int i=1;i<=m;i++){
  	scanf("%s",T+1);
  	printf("%d\n",qry(T));
  }
  return 0;
}

P2414 阿狸的打字机 ACAM+fail树+BIT 好题

P2414 [NOI2011] 阿狸的打字机

给你一棵字典树。求单词 \(x\)\(y\) 里出现了多少次。询问数不大于 \(10^5\)

第一次写了个线段树合并,感觉不太能调于是重构,这道题至少搞了 \(2h\)

考虑建出一棵 \(fail\) 树,我们发现 \(x\)\(y\) 里面出现当且仅当 \(x\)\(y\) 的祖先。

这个是挺容易发现的。

于是我们如何求呢?

回到原来的 Trie 树上面,我们把根到它的路径上面的所有点都 \(+1\)

后面直接查询就可以了。

至于之前就先在 \(fail\) 树上面跑一次 dfs 即可,

所以得到的子树的区间一定是连续的——用树状数组维护区间查询和单点修改即可。

#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define pii pair<int,int>
#define fi first
#define se second

const int N=1e5+5;
int n,m,ans[N],son[N][26],s[N][26],fail[N],dfn[N],idx=0,cnt=0,fa[N],tot=0,l[N],r[N],id[N];
vector<int> g[N],e[N];
char t[N];
vector<pii> fsy[N];

int read(){
  int x=0,f=1;char ch=getchar();
  while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
  while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
  return x*f;
}

void print(int x){
  int p[15],tmp=0;
  if(x==0) putchar('0');
  if(x<0) putchar('-'),x=-x;
  while(x) p[++tmp]=x%10,x/=10;
  for(int i=tmp;i>=1;i--) putchar(p[i]+'0');
  putchar('\n');
}

namespace BIT{
  int tr[N];
  int lowbit(int i){return i&(-i);}
  void upd(int x,int val){for(int i=x;i<=tot;i+=lowbit(i)) tr[i]+=val;}
  int qry(int x){int res=0;for(int i=x;i>=1;i-=lowbit(i)) res+=tr[i];return res;}
}

void ins(char *S){
  int len=strlen(S),p=0;
  for(int i=0;i<len;i++){
  	if(S[i]!='B'&&S[i]!='P'){
  	  int tmp=S[i]-'a';
  	  if(!son[p][tmp]) son[p][tmp]=++idx,fa[idx]=p;
  	  p=son[p][tmp];
  	}
  	else if(S[i]=='P') g[p].pb(++cnt),id[cnt]=p;
    else p=fa[p];
  }
  for(int i=0;i<=idx;i++)
    for(int j=0;j<26;j++)
      s[i][j]=son[i][j];
}

void getfail(){
  queue<int> q;
  for(int i=0;i<26;i++) if(son[0][i]) q.push(son[0][i]);
  while(!q.empty()){
  	int u=q.front();q.pop();
  	for(int i=0;i<26;i++){
  	  int v=son[u][i];
  	  if(!v){son[u][i]=son[fail[u]][i];continue;}
  	  fail[v]=son[fail[u]][i];q.push(v);
  	}
  }
  for(int i=1;i<=idx;i++) e[fail[i]].pb(i);
}

void dfs(int u){
  dfn[u]=++tot;l[u]=dfn[u];
  for(auto v:e[u]) dfs(v);
  r[u]=tot;
}

void sol(int p){
  BIT::upd(dfn[p],1);
  for(auto i:g[p]){
  	for(auto q:fsy[i])
  	  ans[q.se]=BIT::qry(r[id[q.fi]])-BIT::qry(l[id[q.fi]]-1);
  }
  for(int i=0;i<26;i++) if(s[p][i]) sol(s[p][i]);
  BIT::upd(dfn[p],-1);
}

void wrk(){
  scanf("%s",t);ins(t);
  m=read();
  for(int i=1,u,v;i<=m;i++) u=read(),v=read(),fsy[v].pb({u,i});
  getfail();
  for(int i=0;i<=idx;i++) if(!dfn[i]) dfs(i);
  sol(0);
  for(int i=1;i<=m;i++) print(ans[i]);
}

int main(){
  /*2023.11.14 H_W_Y P2414 [NOI2011] 闃跨嫺鐨勬墦瀛楁満 ACAM+BIT*/
  freopen("out.out","w",stdout);
  wrk();return 0;
}

P3041 ACAM+dp

P3041 [USACO12JAN] Video Game G

给出 \(n\) 个模式串,求长为 \(k\) 的文本串最多出现多少次模式串中的单词。多次出现算多次。

\(n \le 20, k \le 10^3\),单词长度不超过 \(15\)

还是一道比价典的题目,

于是再 ACAM 上面进行 dp 就可以了。

注意要判断深度是否满足条件。

dp 方式就和前面的那道 dp 基本一样。

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

const int N=1e3+5;
int n,m,f[N][N],son[N][26],fail[N],vis[N],idx=0,ans=0,dep[N];
char S[N];

void ins(char *s){
  int len=strlen(s),p=0;
  for(int i=0;i<len;i++){
  	int tmp=s[i]-'A';
  	if(!son[p][tmp]) son[p][tmp]=++idx,dep[idx]=dep[p]+1;
  	p=son[p][tmp];
  }
  vis[p]++;
}

void getfail(){
  queue<int> q;
  for(int i=0;i<26;i++) if(son[0][i]) q.push(son[0][i]);
  while(!q.empty()){
  	int u=q.front();q.pop();
  	for(int i=0;i<26;i++){
  	  int v=son[u][i];
	  if(!v){son[u][i]=son[fail[u]][i];continue;}
	  fail[v]=son[fail[u]][i];
	  vis[v]+=vis[son[fail[u]][i]];
	  q.push(v);	
	}
  }
}

int main(){
  /*2023.11.14 H_W_Y P3041 [USACO12JAN] Video Game G ACAM*/
  scanf("%d%d",&n,&m);
  for(int i=1;i<=n;i++) scanf("%s",S),ins(S);
  getfail();
  for(int i=1;i<=m;i++)
    for(int j=0;j<=idx;j++){
      if(dep[j]>i-1) continue;
      for(int k=0;k<26;k++)
        if(son[j][k]&&dep[son[j][k]]<=i) f[i][son[j][k]]=max(f[i][son[j][k]],f[i-1][j]+vis[son[j][k]]);
	}
  for(int i=0;i<=idx;i++) ans=max(ans,f[m][i]);
  printf("%d\n",ans);
} 

CF1202E ACAM+简单dp

CF1202E You Are Given Some Strings...

你有一个字符串 \(t\)\(n\) 个字符串 \(s_1, s_2, \dots, s_n\)。令 \(f(t, s)\)\(s\)\(t\) 中的出现次数。请计算 \(\sum_{i=1}\sum_{j=1} f(t,s_i+s_j)\), 其中 \(s + t\) 代表 \(s\)\(t\) 连接起来。

\(|t|, n, \sum|s| \le 2 times 10^5\)

比前面几道题的思维都简单,

就是你枚举每一个点,往前和往后匹配分别能和多少个模式串匹配,

最后相乘算答案就可以了。

我用了两个 ACAM 去跑。

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

const int N=2e5+5;
int son[N][26],fail[N],vis[N],n,pre[N],suf[N],l[N],r[N],idx=0;
char s[N],t[N];
ll ans=0;

void ins(char *S,int len){
  int p=0;
  for(int i=0;i<len;i++){
  	int tmp=S[i]-'a';
  	if(!son[p][tmp]) son[p][tmp]=++idx;
  	p=son[p][tmp];
  }
  vis[p]++;
}

void ins2(char *S,int len){
  int p=0;
  for(int i=len-1;i>=0;i--){
  	int tmp=S[i]-'a';
  	if(!son[p][tmp]) son[p][tmp]=++idx;
  	p=son[p][tmp];
  }
  vis[p]++;
}

void getfail(){
  queue<int>q;
  for(int i=0;i<26;i++) if(son[0][i]) q.push(son[0][i]);
  while(!q.empty()){
  	int u=q.front();q.pop();
  	for(int i=0;i<26;i++){
  	  int v=son[u][i];
  	  if(!v){son[u][i]=son[fail[u]][i];continue;}
  	  fail[v]=son[fail[u]][i];
  	  vis[v]+=vis[fail[v]];
  	  q.push(v);
  	}
  }
}

int main(){
  /*2023.11.15 H_W_Y CF1202E You Are Given Some Strings... ACAM*/
  scanf("%s",t);int m=strlen(t);
  scanf("%d",&n);r[0]=-1;
  for(int i=1;i<=n;i++){
  	l[i]=r[i-1]+1;
  	scanf("%s",s+l[i]);
  	r[i]=l[i]+(int)strlen(s+l[i])-1;
    ins(s+l[i],r[i]-l[i]+1);
  }
  int p=0;getfail();
  for(int i=0;i<m;i++){
  	p=son[p][t[i]-'a'];
  	pre[i]=vis[p];
  }idx=0;
  memset(son,0,sizeof(son));
  memset(fail,0,sizeof(fail));
  memset(vis,0,sizeof(vis));
  for(int i=1;i<=n;i++)
  	ins2(s+l[i],r[i]-l[i]+1);
  getfail();p=0;
  for(int i=m-1;i>=0;i--){
  	p=son[p][t[i]-'a'];
  	suf[i]=vis[p];
  }
  for(int i=0;i<m;i++) ans+=1ll*pre[i]*suf[i+1];
  printf("%lld\n",ans);
  return 0;
}

后缀数组 SA

怎么都 11.15 才启动啊!

lxs 希望我能做几道题,套路就应该掌握了。

感觉板板已经是要忘不忘的地步了,好久没写。

之前写过一篇博客有关 SA 的博客。Link

而关于 SA 求 \(height\) 数组从而求得 LCP 的方法,也是基本套路。

很多题都是把两个串串起来。

P3809 【模板】后缀排序

P3809 【模板】后缀排序

板子。

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

const int N=1e6+5;
int n,m,sa[N],rk[N],y[N],c[N];
char s[N];

void getsa(){
  m=122;
  for(int i=1;i<=n;i++) rk[i]=s[i],c[rk[i]]++;
  for(int i=1;i<=m;i++) c[i]+=c[i-1];
  for(int i=n;i>=1;i--) sa[c[rk[i]]--]=i;
  for(int k=1;;k<<=1){
  	int num=0;
  	for(int i=n-k+1;i<=n;i++) y[++num]=i;
  	for(int i=1;i<=n;i++) if(sa[i]>k) y[++num]=sa[i]-k;
  	for(int i=1;i<=m;i++) c[i]=0;
  	for(int i=1;i<=n;i++) c[rk[i]]++;
  	for(int i=1;i<=m;i++) c[i]+=c[i-1];
  	for(int i=n;i>=1;i--) sa[c[rk[y[i]]]--]=y[i],y[i]=rk[i];
  	rk[sa[1]]=num=1;
  	for(int i=2;i<=n;i++){
  	  if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]) rk[sa[i]]=num;
  	  else rk[sa[i]]=++num;
  	}m=num;
  	if(num==n) break;
  }
}

int main(){
  /*2023.11.16 H_W_Y P3809 【模板】后缀排序 SA*/
  scanf("%s",s+1);n=strlen(s+1);
  getsa();
  for(int i=1;i<=n;i++) printf("%d ",sa[i]);
  return 0;
}

SA 求最长公共子序列

放个板子。

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

const int maxn=5e5+10;
int n,m,rk[maxn],p,sa[maxn],c[maxn],y[maxn],h[maxn],lst[2],ans=0,mn[2];
char s[maxn];

void getsa(){
  m=122;
  for(int i=1;i<=n;i++) rk[i]=s[i],c[rk[i]]++;
  for(int i=1;i<=m;i++) c[i]+=c[i-1];
  for(int i=1;i<=n;i++) sa[c[rk[i]]--]=i;
  for(int k=1;;k<<=1){
  	int num=0;
  	for(int i=n-k+1;i<=n;i++) y[++num]=i;
  	for(int i=1;i<=n;i++) if(sa[i]>k) y[++num]=sa[i]-k;
  	for(int i=1;i<=m;i++) c[i]=0;
  	for(int i=1;i<=n;i++) c[rk[i]]++;
  	for(int i=1;i<=m;i++) c[i]+=c[i-1];
  	for(int i=n;i>=1;i--) sa[c[rk[y[i]]]--]=y[i],y[i]=rk[i];
  	rk[sa[1]]=num=1;
  	for(int i=2;i<=n;i++){
  	  if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]) rk[sa[i]]=num;
	  else rk[sa[i]]=++num;	
	}m=num;
	if(n==num) return;
  }
}

void getheight(){
  for(int i=1;i<=n;i++) rk[sa[i]]=i;
  for(int i=1,k=0;i<=n;i++){
  	if(rk[i]==1) continue;
  	if(k) k--;
  	int j=sa[rk[i]-1];
  	while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) k++;
  	h[rk[i]]=k;
  }
}

void getans(){
  lst[0]=lst[1]=0;mn[0]=mn[1]=0x3f3f3f3f;
  for(int i=1;i<=n;i++){
  	mn[0]=min(mn[0],h[i]);
  	mn[1]=min(mn[1],h[i]);
  	if(sa[i]<=p){
  	  lst[0]=i;mn[0]=0x3f3f3f3f;
  	  if(!lst[1]) continue;
  	  ans=max(ans,mn[1]);
	}
	else if(sa[i]>p+1){
	  lst[1]=i;mn[1]=0x3f3f3f3f;
	  if(!lst[0]) continue;
	  ans=max(ans,mn[0]);
	}
  }
}

int main(){
  /*2023.7.26 H_W_Y SP1811 LCS - Longest Common Substring 后缀数组*/ 
  scanf("%s",s+1);p=strlen(s+1);s[p+1]='$';
  scanf("%s",s+p+2);n=strlen(s+1);
  getsa();getheight();getans();
  printf("%d\n",ans);
  return 0;
}

P4248 [AHOI2013] 差异-公共子串的长度和

P4248 [AHOI2013] 差异

给定一个长度为 \(n\) 的字符串 \(S\),令 \(T_i\) 表示它从第 \(i\) 个字符开始的后缀。求

\[\displaystyle \sum_{1\leqslant i<j\leqslant n}\operatorname{len}(T_i)+\operatorname{len}(T_j)-2\times\operatorname{lcp}(T_i,T_j) \]

其中,\(\text{len}(a)\) 表示字符串 \(a\) 的长度,\(\text{lcp}(a,b)\) 表示字符串 \(a\) 和字符串 \(b\) 的最长公共前缀。

\(2\le n\le 500000\),且 \(S\) 中均为小写字母。

转换一下式子,发现就是求

\[\frac{n(n-1)(n+1)}{2}+2 \times \sum_{1 \le i \lt j \le n} LCP(suf_i,suf_j) \]

于是对于每一个 \(h\) 的区间我们都要求最小值,用单调栈维护即可。

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

const int N=1e6+5,inf=0x3f3f3f3f;
int sa[N],n,m,rk[N],c[N],y[N],h[N],st[N],tp=0,l[N],r[N];
ll ans=0;
char s[N];

void getsa(){
  m=122;
  for(int i=1;i<=n;i++) rk[i]=s[i],c[rk[i]]++;
  for(int i=1;i<=m;i++) c[i]+=c[i-1];
  for(int i=n;i>=1;i--) sa[c[rk[i]]--]=i;
  for(int k=1;;k<<=1){
  	int num=0;
  	for(int i=n-k+1;i<=n;i++) y[++num]=i;
  	for(int i=1;i<=n;i++) if(sa[i]>k) y[++num]=sa[i]-k;
  	for(int i=1;i<=m;i++) c[i]=0;
  	for(int i=1;i<=n;i++) c[rk[i]]++;
  	for(int i=1;i<=m;i++) c[i]+=c[i-1];
  	for(int i=n;i>=1;i--) sa[c[rk[y[i]]]--]=y[i],y[i]=rk[i];
  	rk[sa[1]]=num=1;
  	for(int i=2;i<=n;i++){
  	  if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]) rk[sa[i]]=num;
  	  else rk[sa[i]]=++num;
  	}
  	m=num;
  	if(n==num) break;
  }
}

void geth(){
  for(int i=1;i<=n;i++) rk[sa[i]]=i;
  for(int i=1,k=0;i<=n;i++){
  	if(rk[i]==1) continue;
  	if(k) k--;
  	int j=sa[rk[i]-1];
  	while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) k++;
  	h[rk[i]]=k;
  }
}

int main(){
  /*2023.11.1 H_W_Y P4248 [AHOI2013] 差异 SA*/
  scanf("%s",s+1);n=strlen(s+1);
  getsa();geth();st[++tp]=1;
  for(int i=2;i<=n;i++){
    while(tp&&h[i]<h[st[tp]]) r[st[tp--]]=i;
    l[i]=st[tp];st[++tp]=i;
  }
  for(int i=tp;i>=1;i--) r[st[i]]=n+1;
  ans=1ll*n*(n-1)*(n+1)/2;
  for(int i=2;i<=n;i++) ans-=2ll*h[i]*(i-l[i])*(r[i]-i);
  printf("%lld\n",ans);
  return 0;
}

联赛前就写到这里吧,还去口胡了几个 SA 的题目,明天再打一遍板板就该上战场了。

更多内容赛后来更新——这已经勾起了我对字符串的兴趣。

Conclusion

哈希是个有用有很玄学的东西,考场上可能估不到会不会被卡。。。一定要想清楚再写(这个东西几乎是不太可调的)。

  1. 判断一个子串是否是 \(l \sim r\) 区间串的循环节,就是比较 \(l+len \sim r\)\(l \sim r-len\) 的哈希值是否相等即可。(P3538 [POI2012] OKR-A Horrible Poem)
  2. 很多题在实现的时候都有技巧,我们可以考虑先把原序列对称/翻转。(P2601 [ZJOI2009] 对称的正方形)
  3. 题目中有一些特殊性质的时候,我们考虑把序列重新分段,不一定 dp 非得一位一位比较,有些时候也可以一段一段进行转移。(P3167 [CQOI2014] 通配符匹配)
  4. 有些时候字符串储存可以直接把很多个串拼在一起存,这样会使操作很方便。(P3449 [POI2006] PAL-Palindromes)
  5. 回文串一定要用好左右两边是翻转得到的这个性质,有些时候我们就可以省去不必要的翻转操作。(P3449 [POI2006] PAL-Palindromes)
  6. 字符串失配之后很有可能和其他的前缀配对!(P3193 [HNOI2008] GT考试)
  7. 字符串的题目一般可以先把题目中的串换一种方式表达出来,这样更好理解分析。(P3546 [POI2012] PRE-Prefixuffix)
  8. ACAM+dp 通常的状态都是 \(f_{i,j}\) 表示原字符串走到第 \(i\) 位,ACAM 上走到 \(j\) 节点的答案,转移会比较简单。(P4052 [JSOI2007] 文本生成器)
  9. ACAM 上面打标记时要注意如果 \(fail\) 节点有标记,那么这个节点也有。(P4052 [JSOI2007] 文本生成器)
  10. 是否是子串的问题可以转化到 \(fail\) 树上面判断子树区间查询的问题。(P2414 [NOI2011] 阿狸的打字机)
  11. 很多字符串问题都是两个相同的串拼起来的方案数,这个时候我们可以考虑记录每一个位置往前和往后满足条件的串的个数。
posted @ 2023-11-16 20:48  H_W_Y  阅读(25)  评论(0编辑  收藏  举报