分桶法是把一排物品或者平面分成桶,每个桶分别维护自己内部的信息,以达到高效计算的目的的方法,感觉就像分封制,国家太大了,中央政府管不下来,就分封了很多的小封国,这样叫封国再管理自己,我们只需要管理封国就行了。
其中,平方分割是把排成一排的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.