【数据结构】分桶法和平方分割

分桶法是把一排物品或者平面分成桶,每个桶分别维护自己内部的信息,以达到高效计算的目的的方法,感觉就像分封制,国家太大了,中央政府管不下来,就分封了很多的小封国,这样叫封国再管理自己,我们只需要管理封国就行了。
其中,平方分割是把排成一排的n个元素每根号n个分在一个桶内进行维护的方法的统称。这样的分割方法可以使对区间的操作的复杂度降至O(√n)。
和线段树一样,根据维护的数据不同,平方分割可以支持很多不同的操作。不过时间复杂度不同。
比如RMQ问题。
这里写图片描述
1. 基于平方分割的RMQ
给定一个数列a1,a2,…,an,目标是在O(根号n)复杂度内实现两个功能
- 给定s,t,求as,as+1,…,at的最小值
- 给定t, x,把ai的值变为x.
2. 基于平方分割RMQ的预处理
令b=floor(√n),把a中的元素每b分成一个桶,并且计算出每个桶内的最小值。
3. 基于平方分割的RMQ的查询
- 如果桶完全包含在区间内,则查询桶的最小值
- 如果元素所在的桶不完全被区间包含,则逐个检查最小值
这里写图片描述
4. 基于平方分割的RMQ的值的更新
在更新元素的值时,需要更新该元素所在的桶的最小值。这时只要遍历一遍桶内的元素就可以了。
5. 基于平方分割的RMQ的时间复杂度
在更新值时,因为每个桶内有b个元素,所以时间复杂度是O(b)。
而在查询时
- 完全包含在区间内的桶的个数是O(n/b)
- 所在的桶不被区间完全包含的元素个数是O(b)
因为设b=√n,则操作的时间复杂度是
O(n/b+b)=O(√n+√n)=O(√n);
6. 平方分割和线段树
因此,在平方分割中,对于任意区间,完全包含于其中的桶的数量和剩余元素的数量都是O(√n),所以可以在O(√n)时间内完成各种操作。
在上面的RMQ的例题中,线段树进行各种操作的复杂度是O(logn),比平方分割更快一些。一般地,如果线段树和平方分割都能实现某个功能,多数情况下线段树会比平方分割快。但是,因为平方分割在实现上比线段树简单,所以如果运行时间限制不是太紧时,也可以考虑使用平方分割。除此之外,也有一些功能是线段树无法高效维护但是平方分割却可以做到的。
7. 题目

K-th Number POJ - 2104

意思就是查找区间第k大。
虽然很明显可以用可持久化线段树(而且快很多),但是这道题的数据范围我们可以用平方分割,如果你懒得码可持久化线段树,简单的平方分割是很好的选择。
首先,如果x是第k个数,那么一定有:
- 在区间中不超过x的数不少于k个。
- 在区间中小于x的数不到k个。
所以,如果可以快速求出区间里不超过x的数的个数,就可以用二分查找来找到答案。
那么我们如何统计一个区间比x小的数的个数呢?如果不预处理那么分不分桶都一个样。当然,每个桶可以先预处理,我们把每个桶进行排序,到时候二分查找就行了。至于边边上的元素暴力O(n)扫一遍就行了。
归纳一下:
- 对于完全包含在区间内的桶,用二分搜索计算;
- 对于所在的桶不完全包含在区间的元素,逐个检查;
如果b设为√n,那么复杂度是
O((n/b)*logb+b)=O(√(n)logn)
很显然,处理一个元素要O(1)的时间,但是一个桶要O(logb),所以为了让速度更快,则设b=√(nlogn),那么时间复杂度为O(√(nlogn)).
所以总的复杂度为O(nlogn+m√n*log^1.5 n);

代码

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
#include<queue>
using namespace std;
const int MAXN=100005,B=1072,MAXM=5005;
int n,m;
int nums[MAXN],a[MAXN];
vector<int>bucket[MAXN/B+5];//桶
int main()
{
    for(int i=0;i<=MAXN/B;i++) 
        bucket[i].reserve(B+5);
    scanf("%d %d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&nums[i]);
        a[i]=nums[i];
    }
    sort(a+1,a+n+1);
    for(int i=1;i<=n;i++)//快到桶里来
        bucket[i/B].push_back(nums[i]);
    for(int i=0;i<=n/B;i++)
        sort(bucket[i].begin(),bucket[i].end());
    for(int i=1;i<=m;i++)
    {
        int l,r,k;
        scanf("%d %d %d",&l,&r,&k);
        int lb=0,ub=n;
        while(ub-lb>1)
        {
            int che=0,cl=l,cr=r+1;//左闭右开
            int mid=(lb+ub)>>1;
            int x=a[mid];
            while(cl<cr&&cl%B!=0)
                if(nums[cl++]<=x)
                    che++;
            while(cl<cr&&cr%B!=0)
                if(nums[--cr]<=x)
                    che++;
            for(int i=cl/B;i<cr/B;i++)
            {
                che+=upper_bound(bucket[i].begin(),bucket[i].end(),x)-bucket[i].begin();  
            }
            if(che>=k)
                ub=mid;
            else
                lb=mid;
        }
        printf("%d\n",a[ub]);
    }
}

注意:这题巨坑,b=1000过不了,b=1288过不了,实测1072最佳(poj数据)
但是我不知道1072怎么来的,但是998也能够过,而且我知道998是怎么来的

因为中间的时间复杂度是O((n/b)* logb+b),所以让b=(n/b) * logb时时间复杂度最小。所以b^2/logb=n;所以用几何画板解得998.
这里写图片描述

猜你喜欢

转载自blog.csdn.net/qq_35713030/article/details/53304063