Update:添加了一种常数小的做法
一.闲话
看了下大佬们的题解,然后。。。二分+尺取???
我:。。。
二.题解
题目简意:
数组b的元素是,数组a中所有区间长度大于等于k的区间的第k大数(有点绕?)
求数组b中第m大的数
要做这题,首先我们需要明白一个简单的性质,对于一个序列,我们如果添加进一个新数进去后,其中第k大数一定不会减小。
证明:
1.如果添加进去的数比原k大数大,那么,k大数改变,变成原k-1大的数(若k=1,就是添加进去的这个数)
2.如果添加进去的数小于等于原k大数,则排序后对前k大的数没任何影响,所以,k大数不变
知道了这个性质后,我们就可以开做了
直接枚举肯定很麻烦,所以我们考虑二分答案。
假设,我们二分出了一个x,那么,我们现在就需要判断的是,x和我们要求的b数组中第m大数的大小关系。
那么,我们就需要求,b数组中有多少个数大于等于x,即等价于,有多少个长度大于等于k的区间的第k大数比x大。
我们先枚举左端点l,那么,假设我们找到了一个右端点r,使得l-r的第k大数大于等于x,那么由我们之前得到的那个性质,就一定有:l-(r+1),l-(r+2)...l-n的第k大数都大于等于x
所以,我们对于每个左端点l,求出最小的满足区间第k大数大于等于x的右端点r即可,答案加上(n-r+1)
那么,很明显,这就又是一个二分问题了,我们就可以考虑:枚举左端点,二分右端点。
问题就又来了,我们如何找区间第k大数呢?
主席树了解下?
于是,我们就可以做出这道题了,复杂度高达
交一发,果然T了(并且百般卡常无效qwq),那么,我们怎么优化呢?
还是利用那个性质,我们倒序枚举左端点,设上次的右端点答案为r,每次左端点移动后,kth一定不会减小,因为我们之前有(l+1)-r的第k大数大于等于x,那么同理就一定有l-r的第k大数大于等于x,那么,我们只需再继续判断l-(r-1)...的第k大数是否大于等于x即可,如果有一个小于了,那么这时的右端点加1就是我们的答案,r这个右端点我们可以使用单调指针维护,复杂度就优化到了优秀的了,可以通过此题。
PS:三年OI一场空,不开long long见祖宗
#pragma GCC optimize(3,"inline","Ofast") #include<bits/stdc++.h> using namespace std; #define int long long const int N=1e5+1; struct zxs{ int lson,rson,w; }t[N<<5]; int sav[N]; int rt[N],cnt; int a[N],b[N],s[N]; inline void insert(int pas,int &now,int l,int r,int x){ now=++cnt; t[now]=t[pas]; ++t[now].w; if(l==r){ return; } int mid=(l+r)>>1; if(x<=mid){ insert(t[pas].lson,t[now].lson,l,mid,x); }else{ insert(t[pas].rson,t[now].rson,mid+1,r,x); } } inline int kth(int pas,int now,int l,int r,int k){//因为求的第k大,所以左右区间要反过来写 if(l==r){ return l; } int sum=t[t[now].rson].w-t[t[pas].rson].w,mid=(l+r)>>1; if(sum>=k){ return kth(t[pas].rson,t[now].rson,mid+1,r,k); } return kth(t[pas].lson,t[now].lson,l,mid,k-sum); } int n,k,m,al; inline int calc(int x){ int ans=0,r=-1;//单调指针维护 for(int i=n-k+1;i;--i){//枚举区间左端点 if(sav[i]<x){//小剪枝 continue; } if(r==-1){ int L=i+k-1,R=n; while(L<=R){ int mid=(L+R)>>1; if(kth(rt[i-1],rt[mid],1,al,k)>=x){ r=mid,R=mid-1; }else{ L=mid+1; } } }else{ while((r-i+1)>k&&kth(rt[i-1],rt[r-1],1,al,k)>=x){ --r; } } ans+=(n-r+1); } return ans; } inline int read(){ char ch=getchar();int w=0,ss=0; while(ch<'0'||ch>'9')w|=(ch=='-'),ch=getchar(); while(ch>='0'&&ch<='9')ss=(ss*10+(ch-'0')),ch=getchar(); return w?-ss:ss; } signed main(){ int T=read(); while(T--){ memset(rt,0,sizeof(rt));cnt=0;//清空主席树 n=read(),k=read(),m=read(); for(int i=1;i<=n;++i){ a[i]=read(); b[i]=a[i]; } sort(b+1,b+n+1); al=unique(b+1,b+n+1)-b-1; for(int i=1;i<=n;++i){ int x=a[i]; a[i]=lower_bound(b+1,b+al+1,x)-b; s[a[i]]=x; } for(int i=1;i<=n;++i){ insert(rt[i-1],rt[i],1,al,a[i]); } for(int i=n-k+1;i;--i){ sav[i]=kth(rt[i-1],rt[n],1,al,k); } int l=1,r=al,ans=0; while(l<=r){ int mid=(l+r)>>1; int tot=calc(mid);//区间kth大于等于mid的有几个 if(tot>=m){ ans=mid;l=mid+1; }else{ r=mid-1; } } printf("%lld\n",s[ans]); } return 0; }
三.新增
关于此题,我们分析一下:
一个区间第k大的数不小于x的条件是什么?
答案就是一个区间内不小于x的数的个数不小于k
那么,我们就会发现,我们其实并不需要知道每个数的值,实际上对我们有用的只有每个数与x的大小关系,然后,我们就可以直接用贡献法计算。
我们把所有值不下于x的赋为1,剩下的赋为0,那么,二分求的东西就被转换成了:
有多少个区间的区间和不下于k,且序列里面的值只可能是0或1
然后随便搞个前缀和加个单调指针就行了
(其实可以不用单调指针直接用桶存的,常数更小,但是,由于脑袋有点晕,出锅了,就懒得改了)
代码:
#pragma GCC optimize(3,"inline","Ofast") #include<bits/stdc++.h> using namespace std; #define int long long const int N=1e5+1; int a[N],b[N]; int n,k,m; inline int calc(int x){ for(int i=1;i<=n;++i){ b[i]=(a[i]>=x); b[i]+=b[i-1]; } int ans=0,l=1; for(int i=1;i<=n;++i){//枚举右端点 while(b[i]-b[l-1]>=k)++l; ans+=(l-1); } return ans; } inline int read(){ char ch=getchar();int w=0,ss=0; while(ch<'0'||ch>'9')w|=(ch=='-'),ch=getchar(); while(ch>='0'&&ch<='9')ss=(ss*10+(ch-'0')),ch=getchar(); return w?-ss:ss; } signed main(){ int T=read(); while(T--){ n=read(),k=read(),m=read(); for(int i=1;i<=n;++i){ a[i]=read(); } int l=1,r=1e9,ans=0; while(l<=r){ int mid=(l+r)>>1; int tot=calc(mid);//区间kth大于等于mid的有几个 if(tot>=m){ ans=mid;l=mid+1; }else{ r=mid-1; } } printf("%lld\n",ans); } return 0; }