2024牛客多校7 D-Interval Selection,数字哈希+状态记录

题目描述

给定一个数组和一个整数 k,我们要找到所有的子数组 [l, r],其中每个元素在子数组中恰好出现 k 次。这个类型的子数组被称为“良好的”子数组。询问整个数组里能找出几个良好子数组。

输入描述

链接:登录—专业IT笔试面试备考平台_牛客网
来源:牛客网

The first line contains an integer T(eq?1%5Cleq%20T%20%5Cleq%2010%5E5), indicating the number of test cases.

For each test case:
The first line contains two integers eq?n%281%5Cleq%20n%20%5Cleq%202*10%5E5%29     eq?k%281%5Cleq%20k%20%5Cleq%20n%29

The second line contains nnn integers eq?a_%7B1%7D...a_%7Bn%7Deq?%280%3C%3Da_%7Bi%7D%3C%3D10%5E9%29--- each element of the array.

It is guaranteed that the total sum of nnn for all test cases does not exceed eq?2*10%5E5

Sample input:

4
3 2
1 2 2
3 1
1 2 3
6 2
1 1 4 5 1 4
6 3
1 2 1 1 1 1

Sample output:

1
6
1
2

求解 :

分析:

思考前缀和的想法

如果只有一个数:如果sum[]记录这个数字的出现次数。

sum[r]-sum[l-1]==k,那就符合

sum[r]-sum[l-1]>k, l++

sum[r]-sum[l-1]<k, r++

如果两个数呢?

两个数同时满足[l,r]内前缀差是k即可

n个数呢?

是不是要满足:

eq?sum_%7Bi%7D%5Br%5D-sum_%7Bi%7D%5Bl-1%5D%3D%3Dk%2Ci%3D1%2C2%2C...n

那有没有什么方法可以快速的计入每个位置这么多数字的状态呢?可以哈希。

typedef unsigned long long int ll;
map<ll,int>num;//数字出没出现
map<ll,ll>ha;//数字哈希
......
if(!num[x]){
	ha[x]=(rand()+1)*(rand()+1)*(rand()+1)*(rand()+1)*(rand()+1);
}
num[x]=1;

 这样把每个数字哈希处理一下后,我们可以用相加的方法,把哈希值之和认为是状态。

比如2*2+3*3表示2个2,3个3,但是直接数字相加的话------这个结果和1*3+2*5一样,哈希可以避免这个问题。

接下来考虑枚举右端点,用a[i]记录这个端点的状态,如果之前出现过这个状态(比如在j(j<i)处出现),那么[j+1,i]这个区间就合法(类比之前的前缀和相减等于定值),ans+=m(m表示这个状态之前出现过几次),特别的,初始状态为0。

typedef unsigned long long int ll;
int j=0;//左端点
mp[0]++;//初状态(空的)
a[0]=0;a[1]=0;
ll ans=0;

从之前的分析,我们可以感觉到,每个数字x出现的次数很重要,那么为了知道x在i位置到底出现了几次,我们考虑把x出现的位置记录下来,这也为了x次数超过k之后移动左端点提供了方便。

typedef unsigned long long int ll;
map<ll,vector<int> >po;//记录位置
map<ll,ll>ha;//数字哈希
......
for(ll i=1;i<=n;i++){
    ll x;scanf("%lld",&x);
	if(!num[x]){
		ha[x]=(rand()+1)*(rand()+1)*(rand()+1)*(rand()+1)*(rand()+1);
	}
	num[x]=1;
	po[x].push_back(i);
...

mp[]是状态计数。

typedef unsigned long long int ll;
map<ll,ll>mp;//状态

当前数字记为x。

如果po[x].size()<k,很明显i不会是合法右端点,a[i]直接加ha[x],mp[a[i]]++

如果po[x].size()>k,很明显[1,i]不会是合法的,这个时候,我们要找到那个正好包含k个x的地方

po[po[x].size-1]指向的是x的最后一个位置,也就是我们当前的位置,所以我们的目标是po[po[x].size()-1-k]。因此移动左指针j到这个位置

注意每次移动前要把该处状态移除,即先mp[a[j]]--,再j++

如果po[x].size()%k==0,那么需要更新x的状态——我们知道,一个合法状态的x的数量是<=k的,所以到k需要做操作:

考虑第一个k,当第k个x加入时,这一“轮”走完了完整的一轮,需要剔除之前(k-1)个x的影响(当前这个x还没进来)

a[i]=a[i-1]-(k-1)*ha[x]

以上操作做完之后,先算答案,再记录当前状态

完整代码:

#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long int ll;	
//哈希--->可以用相加表示几个x,几个y
//如果新进来的数字没有出现过,则对其做映射
//定右端点i,mp[]记录i的状态
map<ll,int>num;//数字出没出现
map<ll,ll>ha;//数字哈希
map<ll,vector<int> >po;//记录位置
map<ll,ll>mp;//状态
ll a[2000050]={0};
void solve(){
	ll n,k;scanf("%lld %lld",&n,&k);
	int j=0;//左端点
	mp[0]++;//初状态(空的)
	a[0]=0;a[1]=0;
	ll ans=0;
	for(ll i=1;i<=n;i++){
		ll x;scanf("%lld",&x);
		if(!num[x]){
			ha[x]=(rand()+1)*(rand()+1)*(rand()+1)*(rand()+1)*(rand()+1);
		}
		num[x]=1;
		po[x].push_back(i);
		if(po[x].size()>k){//窗口更改
			while(j<po[x][po[x].size()-1-k]){//左端点移动到这个x之前k个的位置
			//v.size()-1是末尾,-k找到目标
				mp[a[j++]]--;//先删除状态,再动j;
			}
		}
		if(po[x].size()%k==0){//删去k-1个x的影响                      *   *
			a[i]=a[i-1]-ha[x]*(k-1);//(当前这个x是要进来的)//k=3时.  2 1 1 1两个*号状态一样,之间的区间是符合的
		}else{//<k个x,状态直接放进来
			a[i]=a[i-1]+ha[x];
		}
		ans+=mp[a[i]];//之前有过这个状态就加
		mp[a[i]]++;//状态记录
	}
	cout<<ans;
}
int main(){
	int t=0;
	scanf("%d",&t);
	while(t--){
		solve();
		printf("\n");
		if(t){
			num.clear();
			ha.clear();
			po.clear();
			mp.clear();
		}
	}
	return 0;
}

注释:

注意到,当po[x].size()没有走满k时,a[i]是要加入x的。

当po[x].size()走满一轮k后,每次新进一个x,就要对应踢掉一个x(通过移动j实现)。

此时a[i]继续加入x

直到下次又到了一轮k,这意味着前面一轮的k要全被踢出去了,此时a[i]把k个x移除,这个时候新加入的x还没记,所以减(k-1)个,即a[i]=a[i-1]+(k-1)*ha[x]

 

 单步模拟举例:k=4时

1 2 3 4 5 6 7 8 9 10 11:序号 

1 2 1 1 1 1 1 1 1  1  1:  值

以下形如3*1表示3个1的哈希值的和

a[1]=1*1

a[2]=1*1+1*2

a[3]=2*1+1*2

a[4]=3*1+1*2

a[5]=4*1+1*2,此时到达第一轮,状态更改为1*2计入mp[]

由于上次出现1*2是a[2]但a[2]=1*1+1*2而不是1*2,所以ans+=0

a[6]进1,1开始第二轮k,此时j移动到1,把a[0]的状态从mp[]中删去,0的状态变为0,由于1*1+1*2计数是1,所以ans+=1,a[6]状态是1*1+1*2计入,此时1*1+1*2计数为2

a[7]进1,此时j移动到3,把a[1]、a[2]踢出,则1*1计数变为0,1*1+1*2计数-1,变为1

a[7]=2*1+1*2,发现计数为1(a[3])ans+=1

这里可以发现相当于a[7]-a[3]即[4,7]是合法区间,有前缀和作差的感觉没?

2*1+1*2的计数+1变为2

a[8]进1,j移动到4,(a[3])2*1+1*2状态-1变为1,a[3]踢出

a[9]进1,踢出a[4],此时再次到达4的倍数,a[9]本应是4*1+1*2,此时是1*2,ans+=1(a[5]是这个状态)

发现相当于a[9]-a[5]即[6,9]是合法区间,有前缀和作差的感觉没?!!

po[x].size()%k==0这一步的操作相当于一直把x的个数控制在0---k-1之间,这也是本题记录状态时的精妙操作。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值