活用各種數據結構——RMQ/樹狀數組/分桶法和平方分割

對《挑戰程序設計競賽》的一個記錄

第三章 出類拔萃——中級篇

上一篇:3.3活用各種數據結構——線段樹篇

3.3活用各種數據結構——RMQ/樹狀數組/分桶法和平方分割

RMQ(區間最值查詢)

有一個長度爲n的亂序序列,要求求出區間[L,R]內的最大值或最小值(或者有多次詢問發生)
(1)普通解法:每次詢問遍歷一遍數組(效率低,n很小時,可以考慮一下)
(2)ST算法:具體看之前寫過的這篇,ST算法可以進行O(1)的查詢,但是不能維護值得更改,適合用在需要大量詢問,但是不改變值得情況下。
(3)線段樹:可以看線段樹篇內的習題
(4)樹狀數組:樹狀數組見下面詳解
用樹狀數組求區間[L,R]最值時,如果待查區域在[L,R]範圍內,則去該區域最值,否則,只取當前位置的數據,索引後減1並重覆上述步驟,直至全部查詢完

int getMax(int r,int l)
{
    int ans = val[r];
    while(l <= r)
    {
        ans = max(ans,val[r]);
        for(--r; r - l >= lower_bit(r);r -= lower_bit(r))
        {
            ans = max(ans,cnt[r]);
        }
    }
    return ans;
}

(5)平方分割:見下面詳解
(6)。。。待補充

樹狀數組

經常用到YB學長的這份自己整理的資料,覺得挺好,直接貼上來記錄一下(侵刪,勿轉載)

單一更新區間查詢
樹狀數組的原英文表達:Binary Indexed Tree(BIT),直譯的意思便是:二進制標記樹。這提示我們,樹狀數組這種數據結構的原理是二進制數。
如果數組A是基礎數組,數組C是區間數組。那麼,在具體介紹數組C的特點前,先給出如下的樹狀關係圖:
這裏寫圖片描述

仔細觀察上圖,容易發現:
數組C[]分別代表的區間爲:
C1=A1 [1,1]
C2=C1+A2=A1+A2 [1,2]
C3=A3 [3,3]
C4=C2+C3+A4=A1+A2+A3+A4 [1,4]
C5=A5 [5,5]
C6=C5+A6=A5+A6 [5,6]
C7=A7 [7,7]
C8=C4+C6+C7+A8=A1+A2+A3+A4+A5+A6+A7+A8 [1,8]
C9=A9 [9,9]

也就是說,每個數組Ci,至少包含Ai,同時包含所有滿足j+lowest_bit(j)=i的Cj數組。例如C8不僅包含A8,同時還包含了C4,C6,C7。而
410=10021002+1002=10002=810
610=11021102+102=10002=810
710=11121112+12=10002=810
:lowest_bit(i)表示計算數字i的二進制表示中,從右往左數,第一個1所代表的數字。

利用位運算,
我們容易得到lowest_bit()的快速計算方法:

int lowbit(int x)
{
    return x&-x;
}

於是,在Ai更新時,只需縱向分別更新即可:C[i],C[i=i+lowest_bit(i)]……直到i>n。
例如在更新A1時候,我們分別更新C1,C2,C4與C8。(這裏假設n=9)
這裏寫圖片描述

對於更新過程我們可以這樣理解:更新所有包含Ai的數組Cj。計算下標j的過程類似於在樹形結構中尋找父節點的過程

實現代碼:

void add(int x,int val)
{
    while(x<=n)
    {
        c[x]+=val;
        x+=lowbit(x);
    }
}

對於每個數組Ci,至少包含Ai,同時包含所有滿足j+lowest_bit(j)=i的Cj數組。因此,可以得到如下結論:數組Ci代表的區間一定是:[i-lowest_bit(i)+1,i]。(這裏略去了該結論的證明過程)
於是,對於A1+A2+……Ai的和,我們只需找到一組能完美覆蓋區間[1,i]的數組集合{C[]}即可:C[i]+C[i=i-lowest_bit(i)]+……直到i=0。

例如在查詢A1+A2+……A7的值時,我們累加C7+C6+C4
這裏寫圖片描述
實現代碼:

int sum(int x)
{
    int rt=0;
    while(x)
    {
        rt+=c[x];
        x-=lowbit(x);
    }
    return rt;
}

可以看出,樹狀數組的代碼實現非常簡潔,極易編碼。同時,我們容易計算出樹狀數組的更新操作的時間複雜度爲log(n),查詢操作的時間複雜度同樣爲log(n),因此總時間複雜度爲log(n)。

題目:

poj 1166 敵兵佈陣

#include<iostream>
#include<cstdio>
#include<algorithm>

using namespace std;

int maxn;
int a[55000];
int lowbit(int x)
{
    return x&-x;
}
void Add(int x,int val)
{
    while(x<=maxn)
    {
        a[x]+=val;
        x+=lowbit(x);
    }    
}

void Sub(int x,int val)
{
    while(x<=maxn)
    {
        a[x]-=val;
        x+=lowbit(x);

    }
}
int Sum(int x)
{
    int rt=0;
    while(x)
    {
        rt+=a[x];
        x-=lowbit(x);
    }
    return rt;
}
int main()
{
    int t,text=1;
    scanf("%d",&t);
    while(t--)
    {
        printf("Case %d:\n",text++);
        memset(a,0,sizeof(a));
        int n;
        scanf("%d",&n);
        maxn=n;
        for(int i=1;i<=n;i++)
        {
            int v;
            scanf("%d",&v);
            Add(i,v);
        }   
        char ch[10];
        int x,y;
        while(1)
        {
            scanf("%s",ch);
            if(ch[0]=='E')
                break;
            scanf("%d%d",&x,&y);
            if(ch[0]=='Q')
                printf("%d\n",Sum(y)-Sum(x-1));    
            else if(ch[0]=='A')
                Add(x,y);
            else
                Sub(x,y);
        }
    }
    return 0;
}

hdu1754 I Hate It
hdu1394 Minimum Inversion Number
hdu2795 Billboard
poj2828 Buy Tickets
poj2886 Who Gets the Most Candies?

區間更新單一查詢
若需要區間更新,單一查詢,那麼只要改變數組C的含義即可:數組C表示區間共同增量,例如,
C1=A1 [1,1]
C2=C1+A2=A1+A2 [1,2]
C3=A3 [3,3]
C4=C2+C3+A4=A1+A2+A3+A4 [1,4]
分別表示區間[1,1],[1,2],[3,3],[1,4]的共同增量,於是在更新區間[1,i]時,我們只需找到一組能完美覆蓋區間[1,i]的數組集合{C[]}即可,這剛好對應着之前樹狀數組的sum操作。於是,我們將sum操作更改爲add操作,即

void add(int x, int val)
{
    while(x)
    {
        c[x]+=val;
        x-=lowbit(x);
    }
}

對於區間[s,t],我們只需執行add(t,val)與add(s-1,-val)即可。

對於單一查詢:query(i),我們只需累加所有包含Ai的數組Cj即可,這對應着之前樹狀數組的add操作。於是,我們將add操作更改爲sum操作,即

int sum(int x)
{
    int rt=0;
    while(x<=n)
    {
        rt+=c[x];
        x+=lowbit(x);
    }
    return rt;
}

題目:
hdu1698 Just a Hook
poj2528 Mayor’s posters
poj3225 Help with Intervals
poj1436 Horizontally Visible Segments
poj2991 Crane
Another LCIS
Bracket Sequence

區間更新區間查詢

poj3468 A Simple Problem with Integers
題意:給出n個數和Q個操作,操作如下:
C a b c:將[a, b]區間中的每個數加上c。
Q a b: 計算[a, b ]區間內的數值之和。

這個題目求的是某一區間的數組和,而且要支持批量更新某一區間內元素的值,怎麼辦呢?實際上,還是可以把該問題轉化爲求數組的前綴和

首先,看更新操作update(s, t, d)把區間A[s]……A[t]都增加d,我們引入一個數組delta[i],表示
A[i]……A[n]的共同增量,n是數組的大小。那麼update操作可以轉化爲:
1)令delta[s] = delta[s] + d,表示將A[s]……A[n]同時增加d,但這樣A[t+1]……A[n]就多加了d,所以
2)再令delta[t+1] = delta[t+1] - d,表示將A[t+1]……A[n]同時減d

再看查詢操作query(s, t),求A[s]……A[t]的區間和,轉化爲求前綴和,設sum[i] = A[1]+ …… +A[i],
則A[s]+ …… +A[t] = sum[t] - sum[s-1],

那麼前綴和sum[x]又如何求呢?它由兩部分組成,一是數組的原始和,二是該區間內的累計增量和, 把數組A的原始值保存在數組org中,並且delta[i]對sum[x]的貢獻值爲delta[i]*(x+1-i),
那麼
sum[x]=org[1]++org[x]+delta[1]x+delta[2](x1)++delta[x]1=org[1]++org[x]+(delta[i](x+1i))=(org[i])+(x+1)(delta[i])(delta[i]i)

1 <= i <= x

這其實就是三個數組org[i], delta[i]和delta[i]*i的前綴和,org[i]的前綴和保持不變,事先就可以求出來,delta[i]和delta[i]*i的前綴和是不斷變化的,並且每次都是單一更新,所以可以用兩個樹狀數組來維護。

對於delta[i]*i,其實就delta[i]的簡單映射關係,於是update操作可以轉化爲:
1)令delta[s]* s = (delta[s] + d)* s = delta[s]* s+s* d,
2)再令delta[t+1]* (t+1) = delta [t+1]* (t+1) – (t+1)* d,

代碼如下:

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>

using namespace std;

const int maxn=100010;
typedef long long LL;
int n;
LL delta1[maxn],delta2[maxn],A[maxn];
int lower_bit(int x)
{
    return x&-x;
}
void Init(int x,LL val)
{
    while(x<=n)
    {
        A[x]+=val;
        x+=lower_bit(x);
    }
}
void Add(int x,LL val,int f)
{
    if(f==0)
    {
        while(x<=n)
        {
            delta1[x]+=val;
            x+=lower_bit(x);
        }
    }
    else
    {
        while(x<=n)
        {
            delta2[x]+=val;
            x+=lower_bit(x);
        }
    }

}
LL query(int x,int f)
{
    LL ans=0;
    if(f==0)
    {
        while(x>0)
        {
            ans+=A[x];
            x-=lower_bit(x);
        }
    }
    else if(f==1)
    {
        while(x>0)
        {
            ans+=delta1[x];
            x-=lower_bit(x);
        }

    }
    else
    {
        while(x>0)
        {
            ans+=delta2[x];
            x-=lower_bit(x);
        }
    }
    return ans;
}
int main()
{
    char s[2];
    int m,x,y;
    LL v,z;
    while(~scanf("%d%d",&n,&m))
    {
        for(int i=1;i<=n;i++)
        {
            scanf("%lld",&v);
            Init(i,v);
        }
        while(m--)
        {
            scanf("%s%d%d",s,&x,&y);
            if(s[0]=='Q')
            {
                LL ans=query(y,0)-query(x-1,0)+(y+1)*query(y,1)-(x)*query(x-1,1)-query(y,2)+query(x-1,2);
                printf("%lld\n",ans);
            }
            else
            {
                scanf("%lld",&z);
                Add(x,z,0);
                Add(y+1,-z,0);
                Add(x,z*x,1);
                Add(y+1,-z*(y+1),1);
            }
        }
        memset(delta1,0,sizeof(delta1));
        memset(delta2,0,sizeof(delta2));
        memset(A,0,sizeof(A));
    }
    return 0;
}

分桶法和平方分割

直接貼書上的:
這裏寫圖片描述

這裏寫圖片描述

這裏寫圖片描述

這裏寫圖片描述

poj 2104 K-th Number
代碼如下:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <algorithm>
#include <cmath>
#include <vector>
#define sf scanf
#define pf printf

using namespace std;

const int Maxn = 100010;
int val[Maxn],sort_val[Maxn];
int n,m,l,r,k;
vector<int> vec[Maxn];

int binary_sort(int k,int i)
{
    int l = 0, r = vec[i].size() - 1;
    while(l <= r)
    {
        int mid = (l + r) >>1;
        if(vec[i][mid] <= k)
            l = mid + 1;
        else
            r = mid - 1;
    }
    return r + 1;
}
int main()
{
    while(~sf("%d%d",&n,&m))
    {
        int b = ceil(sqrt(n * log(n*1.0)));
        if(n == 1) b = 1;

        for(int i = 0;i < n;i ++)
        {
            sf("%d",&val[i]);
            sort_val[i] = val[i];
            vec[i/b].push_back(val[i]);
        }
        for(int i = 0;i <= (n-1)/b;i++)
            sort(vec[i].begin(),vec[i].end());
        sort(sort_val,sort_val + n);
        while(m --)
        {
            sf("%d%d%d",&l,&r,&k);
            l --;
            r --;
            int x = l / b;
            int y = r / b;
            int L = 0,R = n - 1,mid;
            while(L <= R)
            {
                mid = (L + R) >> 1;
                int key = sort_val[mid];
                int ll = l,rr = r,ans = 0;
                while(ll <= rr && ll < (x + 1) * b)
                    if(val[ll++] <= key) ans ++;

                while(ll <= rr && rr >= y * b)
                    if(val[rr--] <= key) ans ++;

                for(int i = x + 1;i < y;i ++)
                    ans += binary_sort(key,i);
                if(ans < k)
                    L = mid + 1;
                else if(ans >= k)
                    R = mid - 1;

            }
            pf("%d\n",sort_val[L]);
        }
        for(int i = 0;i <= (n-1)/ b;i++)
            vec[i].clear();
    }
    return 0;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章