题意:
给定长度为n的排列a,和长度为m的排列b,
问有多少种d,满足对a中的所有元素+d后,a是b的子序列
数据范围:1<=n<=m<=2e5
解法:
考虑枚举d,判断a(i)+d是否是b的子序列
当d=0的时候,就是将b中的1到n提取出来,判断取出的数的相对位置是否和排列a一致
当d=1的时候,就是将b中的2到n-1提取出来,判断取出的数的相对位置是否和排列a(i)+1一致
因为需要快速比较一致性,考虑hash。
先计算出排列a的hash值,本题采用的hash方式:
hash_A=a(1)p0+a(1)p1+…a(n)pn-1
那么如何计算b中1到n子序列的hash值呢?
可以令pos(b(i))=i,将pos(1)=1到pos(n)=n放入线段树对应的pos中,让线段树维护这个序列的hash值
一开始让我很疑惑的点:排列b的子序列可能是不连续的,那么中间会出现空节点,那base不就歪了吗?
仔细查看了别人的代码发现是新开了一个信息标记Sum,用来记录节点区间有多少个非空叶子节点
区间节点合并的时候,根据右节点非空叶子数量来进行便宜,而不是平常的按下标固定偏移,学到了。
这样就相当于跳过了空的位置,计算出的hash值就是正确的hash值。
pushup代码:
void pushup(int node){
Sum[node]=Sum[node*2]+Sum[node*2+1];
A[node]=A[node*2]*base[Sum[node*2+1]]+A[node*2+1];
}
当d=1的时候,查询的是2到n+1子序列的hash值,那么将pos(1)=1从线段树删除,然后将pos(n+1)=n加入。
因为排列a变成了a(i)+1,每个数位都增加了1,hash值增加了p0+p1+…pn-1,这个就是base的前缀和。
枚举d,重复进行上述操作即可。
code:
#include<bits/stdc++.h>
using namespace std;
#define ull unsigned long long
const int maxm=2e5+5;
const int p=1331;
int a[maxm];
int b[maxm];
int pos[maxm];
ull base[maxm];
ull temp;
ull sum;
int n,m;
//
ull A[maxm<<2];
int Sum[maxm<<2];
void pushup(int node){
Sum[node]=Sum[node*2]+Sum[node*2+1];
A[node]=A[node*2]*base[Sum[node*2+1]]+A[node*2+1];
}
void update(int x,int val,int l,int r,int node){
if(l==r){
Sum[node]=(val!=0);
A[node]=val;
return ;
}
int mid=(l+r)/2;
if(x<=mid)update(x,val,l,mid,node*2);
else update(x,val,mid+1,r,node*2+1);
pushup(node);
}
//
signed main(){
ios::sync_with_stdio(0);
cin>>n>>m;
base[0]=1;
for(int i=1;i<=n;i++){
base[i]=base[i-1]*p;//base
sum+=base[i-1];//base前缀和,一次移动的总增量
}
for(int i=1;i<=n;i++){
cin>>a[i];
temp=temp*p+a[i];//排列a的hash值
}
for(int i=1;i<=m;i++){
cin>>b[i];
pos[b[i]]=i;
}
int ans=0;
for(int i=1;i<=n;i++){
update(pos[i],i,1,m,1);
}
if(A[1]==temp){
ans++;
}
for(int i=n+1;i<=m;i++){
update(pos[i-n],0,1,m,1);
update(pos[i],i,1,m,1);
if(A[1]==sum*(i-n)+temp){
ans++;
}
}
cout<<ans<<endl;
return 0;
}