线段树+哈希好题——CF213E

文章讲述了如何通过枚举整数x并结合哈希和线段树数据结构,解决将一个排列转换成另一个排列子序列的问题,目标是在满足特定条件下的x值数量。关键步骤包括维护加x后排列的哈希值和使用线段树管理b序列的子集哈希值。
摘要由CSDN通过智能技术生成

luogu坐标

题意

有两个排列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 后的哈希值

                应该为 \sum_{i=1}^n (a_i + x)*base^ {n-i}

                将柿子展开,发现其等于  x\times\sum_{i=1}^{n}base^{n-i}

                这意味着我们只需要提前对 base 的幂求和,就可以在 O(1)  的时间内得到新的哈希值

3、维护相应的 b 子序列的哈希值。由于 a 序列在改变,选取相应的 b 排列中的数发生改变。考虑用一棵线段树来维护(平衡树也行),每次对于新的 a 排列,删除没用的数,加上新增的。         

4、怎样维护?这里用上了很妙的一种方法。

        一、考虑到 b 子序列可能不连续,不能用普通权值线段树维护。我们可以用数组 pos 记录 i 这个数在 b 排列中的位置,用pos_i 为线段树节点编号,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;
}

时间复杂度

枚举 + 线段树 = 显而易见

撒花——————

        

  • 18
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值