1. 程式人生 > >【資料結構】分桶法和平方分割

【資料結構】分桶法和平方分割

分桶法是把一排物品或者平面分成桶,每個桶分別維護自己內部的資訊,以達到高效計算的目的的方法,感覺就像分封制,國家太大了,中央政府管不下來,就分封了很多的小封國,這樣叫封國再管理自己,我們只需要管理封國就行了。
其中,平方分割是把排成一排的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.
這裡寫圖片描述