2024牛客多校7 D-Interval Selection,数字哈希+状态记录
题目描述
给定一个数组和一个整数 k
,我们要找到所有的子数组 [l, r]
,其中每个元素在子数组中恰好出现 k
次。这个类型的子数组被称为“良好的”子数组。询问整个数组里能找出几个良好子数组。
输入描述
链接:登录—专业IT笔试面试备考平台_牛客网
来源:牛客网
The first line contains an integer T(), indicating the number of test cases.
For each test case:
The first line contains two integers
The second line contains nnn integers ,
--- each element of the array.
It is guaranteed that the total sum of nnn for all test cases does not exceed
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个数呢?
是不是要满足:
那有没有什么方法可以快速的计入每个位置这么多数字的状态呢?可以哈希。
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]状态是2*1+1*2计入,此时2*1+1*2计数为1,1*1+1*2计数为1
a[7]进1,此时j移动到3,把a[1]、a[2]踢出,则1*1计数变为0,1*1+1*2计数-1,变为0
a[7]=3*1+1*2,发现计数为1(a[3])ans+=1
这里可以发现相当于a[7]-a[3]即[4,7]是合法区间,有前缀和作差的感觉没?
3*1+1*2的计数+1变为2
a[8]进1,j移动到4,3*1+1*2状态-1变为1,a[3]踢出,2*1+1*2计数-1变为0
a[9]进1,此时再次到达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之间,这也是本题记录状态时的精妙操作。