题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=4417
题目意思给出一个序列,叫我们求一个区间里面小于等于k的数字个数。
这里面我用分块和主席树两种方法都做了一遍,恩,主席树虽然费空间,但是还是比分块块很多的。
因为每个人的风格不一样,所以我的代码可能比较长,比较繁琐。
首先是分块,分块的思想就是把整个区间划分成多个块,用数组来记录每个块的信息,当我们对一个区间进行查询或者修改的时候,一般来说就会有一些块完全的在这个区间里面,对于这种块被区间完全包含的情况,我们就可以对这些块进行整体的操作,把每一块看成一个整体来进行查询或者修改,而对于那种不完整的块(这种块一般就在区间的两头,最多就只有两块是没有完全被包含在区间里面的,其实左右两边的块就算是完整的我们也把它看成不完整的块来处理),我们对它进行暴力修改或者查询。
在这道题目里面,我们把给出的初始序列分块,假设我用a数组来储存,那么我把a数组复制给b数组,对b数组的每一个块进行块内排序,这样当我们查询区间小于等于k的的数字个数时,对于完整的块我们就可以在b数组里面用二分在块内进行查找(b数组是有序的),对于不完整的块,我们将在原来的数组a里面用暴力查找。
代码:
#include<iostream> #include<cstring> #include<algorithm> #include<queue> #include<map> #include<stack> #include<cmath> #include<vector> #include<set> #include<cstdio> #include<string> #include<deque> using namespace std; typedef long long LL; #define eps 1e-8 #define INF 0x3f3f3f3f #define maxn 100005 int n,m,t,num; int a[maxn],b[maxn]; int block,zu[maxn];//zu[i]表示第i个数字在第几块 void init(){ block=(int)sqrt(n); num=-1; for(int i=1;i<=n;i++){ zu[i]=(i-1)/block+1; num=max(num,zu[i]); } } int ask(int l,int r,int k){ int ans=0; for(int i=l;i<=min(r,zu[l]*block);i++){//左边不完整的块暴力查找 if(a[i]<=k) ans++; } for(int i=zu[l]+1;i<=zu[r]-1;i++){//中间完整的块二分查找 int L=(i-1)*block+1;//块内左边界 int R=i*block; //块内右边界 ans+=upper_bound(b+L,b+R+1,k)-(b+L);//找块内第一个大于k的数字 } if(zu[l]!=zu[r]){//判断左边不完整的块和右边不完整的块是不是同一个块 for(int i=(zu[r]-1)*block+1;i<=r;i++){ if(a[i]<=k) ans++; } } return ans; } int main() { int Case=0; scanf("%d",&t); while(t--){ scanf("%d%d",&n,&m); init(); for(int i=1;i<=n;i++){ scanf("%d",&a[i]); b[i]=a[i]; } for(int i=1;i<=num;i++){ int l=block*(i-1)+1;//当前块的左边界 int r=min(block*i,n);//当前块的右边界 sort(b+l,b+r+1);//块内排序 } printf("Case %d: ",++Case); while(m--){ int l,r,k; scanf("%d%d%d",&l,&r,&k); printf("%d ",ask(l+1,r+1,k));//下标加一,因为我是从下标1开始的 } } return 0; }
然后就是主席树,我也是刚刚学,如果不会可以看这篇博客:
https://blog.csdn.net/bestFy/article/details/78650360
主席树可以用来求区间第k大,也可以用来求区间里面小于等于k的数字个数(发现新姿势),我的思路可能绕了圈圈,我们对给出的序列进行离散化,这样就得到了序列里面每个数字的相对大小(我们对每一个数字进行编号,编号为1的数字就是区间里面最大的数字),然后在建n课线段树,线段树的每一个节点代表着序列里面每一个数字的编号,从左往右编号依次增加,也就是说从左往右依次是第1大,第二大......第n大的数字;第 i 颗线段树就代表着原始序列里从1到i这段前缀中所有编号出现的情况(每一个编号的数字在这一段编号里面出现过没,出现了几次),这样我们建成的线段树就可以相互之间进行相减,和前缀和差不多,第5颗线段树的每一个节点减去第2颗线段树的每一个节点就是[3,5]这个区间的信息。可能不清楚,看上面博客好吧。
求区间比k大的数字的个数,我们可以在排序之后的序列里面找小于等于k的最后一个数字对应的位置,假设是index,那么线段树叶子中从1到index里所有叶子节点所对应的数字就是全部小于等于k了,这里面的都是相对大小,假如给出的区间是L和R,那么我们最后查询时就是查询第R颗线段树减去第L颗线段树,得到的一棵虚拟的数里面在区间[1,index]里面的和。
代码:
#include<iostream> #include<cstring> #include<algorithm> #include<queue> #include<map> #include<stack> #include<cmath> #include<vector> #include<set> #include<cstdio> #include<string> #include<deque> using namespace std; typedef long long LL; #define eps 1e-8 #define INF 0x3f3f3f3f #define maxn 100005 struct node{ int l,r,sum; }tree[maxn*25]; int n,m,k,t,cnt; struct point{ int id,w; }a[maxn]; int b[maxn],rt[maxn]; bool operator <(point s1,point s2){ if(s1.w!=s2.w) return s1.w<s2.w; else return s1.id<s2.id; } void update(int root){ tree[root].sum=tree[tree[root].l].sum+tree[tree[root].r].sum; } void build_0(int &root,int l,int r){ root=++cnt; tree[root].l=l; tree[root].r=r; tree[root].sum=0; if(l==r) return; int mid=(l+r)/2; build_0(tree[root].l,l,mid); build_0(tree[root].r,mid+1,r); update(root); } void build(int pre,int &root,int l,int r,int index){ root=++cnt; tree[root]=tree[pre]; if(l==r){ tree[root].sum++; return; } int mid=(l+r)/2; if(index<=mid) build(tree[pre].l,tree[root].l,l,mid,index); else build(tree[pre].r,tree[root].r,mid+1,r,index); update(root); } int binary(int l,int r,int k){//二分查找区间里面小于等于k的最后一个数字所在的位置 while(l<=r){ int mid=(l+r)/2; if(a[mid].w>k) r=mid-1; else l=mid+1; } return r; } int ask(int root1,int root2,int L,int R,int l,int r){ if(L>R) return 0; if(l>=L&&r<=R){ return tree[root2].sum-tree[root1].sum; } int mid=(l+r)/2; int ans=0; if(mid>=L) ans+=ask(tree[root1].l,tree[root2].l,L,R,l,mid); if(mid<R) ans+=ask(tree[root1].r,tree[root2].r,L,R,mid+1,r); return ans; } int main() { scanf("%d",&t); int Case=0; while(t--){ scanf("%d%d",&n,&m); cnt=0; for(int i=1;i<=n;i++){ scanf("%d",&a[i].w); a[i].id=i; } //离散化,我的离散化结果是没有重复的编号,这里其实有点多余 sort(a+1,a+n+1); for(int i=1;i<=n;i++){ b[a[i].id]=i; } build_0(rt[0],1,n);//建第0颗树 for(int i=1;i<=n;i++) build(rt[i-1],rt[i],1,n,b[i]);//建第i颗树 int l,r,k; printf("Case %d: ",++Case); while(m--){ scanf("%d%d%d",&l,&r,&k); l++; r++; int index=binary(1,n,k);//查找序列里面最后一个小于等于k的数字的位置,对应于线段树的节点位置,因为都是排序之后的相对大小 printf("%d ",ask(rt[l-1],rt[r],1,index,1,n)); } } return 0; }