题意
有两个排列a和b,长度为n和m,现在将第一个排列的所有数字加上一个整数x,使得第一个排列为第二个排列的子序列,求x有多少种取值。
分析
1、x的改变是解决本题的一个难点,那我们可以考虑枚举x的值,以此排除x的影响 (这里用掉了n的复杂度)
2、得知x的情况下,这题是一个选取 b 序列中的部分数匹配 a 排列的问题。而匹配的问题解法无非那几种,再结合这里匹配的是子序列,并不连续,再加上时间复杂度的限制,考虑用哈希判断。
解决题目
1、枚举x:由于序列 a 和 b 都为排列,所以 x 从0枚举到序列 b 的长度 m。
2、维护 a序列加x 的哈希值(记得用unsigned long long)
一、原序列 a 的哈希值很容易得到,用一个数组存 base 的次方即可,这里记为 hsa
二、怎么求加上 x 的哈希值?
考虑:长度为 n 的 a 排列,所有元素加上 x 后的哈希值
应该为
将柿子展开,发现其等于
这意味着我们只需要提前对 base 的幂求和,就可以在 的时间内得到新的哈希值
3、维护相应的 b 子序列的哈希值。由于 a 序列在改变,选取相应的 b 排列中的数发生改变。考虑用一棵线段树来维护(平衡树也行),每次对于新的 a 排列,删除没用的数,加上新增的。
4、怎样维护?这里用上了很妙的一种方法。
一、考虑到 b 子序列可能不连续,不能用普通权值线段树维护。我们可以用数组 pos 记录 i 这个数在 b 排列中的位置,用 为线段树节点编号,i为该节点权值 。我们发现对于每个 x ,我们只需要保留 b 数组中值为 (1+x) ~ (n+x) 的数。这样进行更新就很方便
二、怎样求和
求和求的是哈希值。在 10 进制下,两个数拼接起来:11 和 45 接起来,11*10^2+45=1145
在 base 进制下,两数 x , y 拼接,就为 x*base^(y 的长度) +y
所以线段树多维护一个 数的个数 cnt 即可
代码详解
变量名大部分同上
#include<bits/stdc++.h>
using namespace std;
#define us unsigned long long
#define int long long
const int N=2e5+100;
int n,m,base=131;
int a[N],b[N],ans;
int pos[N];
us hsa,sum,q[N];
struct tree{
us ans;
int cnt;
}tr[N*4];
void up(int p){
tr[p].cnt=tr[p*2].cnt+tr[p*2+1].cnt;
tr[p].ans=tr[p*2].ans*q[tr[p*2+1].cnt]+tr[p*2+1].ans;
}
void ud(int l,int r,int pos,int p,int k){//k的作用为 赋值或删除没用节点
if(l==r){
if(!k) {
tr[p].cnt--;//删除
}
else tr[p].cnt++;
tr[p].ans=k;
return;
}
int mid=(l+r)>>1;
if(pos<=mid) ud(l,mid,pos,p*2,k);
else ud(mid+1,r,pos,p*2+1,k);
up(p);
}
signed main(){
cin>>n>>m;
q[0]=1;
for(int i=1;i<=n;i++) {
cin>>a[i];
q[i]=q[i-1]*base;
sum+=q[i-1];
}
for(int i=1;i<=n;i++) {
hsa+=q[n-i]*a[i];
}
for(int i=1;i<=m;i++) {
cin>>b[i];
pos[b[i]]=i;
}
//从1到 m 枚举,当 i>=n时计数本质还是枚举 x
for(int i=1;i<=m;i++){
if(i>n)ud(1,m,pos[i-n],1,0);//删除没用节点
ud(1,m,pos[i],1,i);//每次更新
int x=i-n;
if(x<0) continue;
us hsaa=x*sum+hsa; //新 hsa
if(tr[1].ans==hsaa) ans++;
}
cout<<ans;
return 0;
}
时间复杂度
枚举 + 线段树 = 显而易见
撒花——————