1. 程式人生 > >[題解][樹狀數組] POJ 2352 - Stars 樹狀數組的簡單應用

[題解][樹狀數組] POJ 2352 - Stars 樹狀數組的簡單應用

line 應用 超時 goto stream csdn ostream sta 數組下標

VJudge題目:https://cn.vjudge.net/contest/283317#problem/A

POJ 2352 - Starshttp://poj.org/problem?id=2352

題目要求:輸入所有星星的坐標,處於某一星星左下方(包括左與下)的有多少星星,就算它有幾級。如果它左下沒有星星就算0級。現在要求你統計各等級的星星各有多少顆。

輸入輸出:一個數字N1<=N<=15000),代表星星的數目;然後輸入N個坐標(0<=X,Y<=32000)。坐標已按Y

X升序排列好。

示例:

Input :

5
1 1
5 1
7 1
3 3
5 5

Output :

1
2
1
1
0

你可以理解為在一個坐標系中,對於某一星星(X,Y),其等級數=坐標(x<=X,y<=Y)的星星的數目。想象這個星星到坐標軸的垂線把裏面的星星圍起來的樣子,圍了多少顆星就算它幾級。


註意題目的輸入已經是升序排列的了,Y坐標相同的星星,按X坐標排列,那麽對於某個Y坐標的星星,前面有多少相同Y坐標的星星,它至少就有幾級。比如示例中的(1,1)(5,1)(7,1),基礎等級依次是0,1,2。問題在於,如何查找X坐標<=該星星的其他星星?


看到這裏很容易想出一種思路:每輸入一行星星(Y

坐標相同)的同時依次得出星星各自的基礎等級,然後每個星星的 基礎等級 + 下方相鄰星星的最終等級 = 最終等級。如果你真以這樣的思路開兩個數組去寫,確實能得出正確的答案,但估計最糟32000套循環會導致超時(未實測)。


既然已經知道Y是升序的,那麽讀入的Y可以不用管,只需要尋找小於X的星星即可。開一個數組count[32002]={0},每次讀入一個X,就計算count[0]count[X]的總和,作為這個星星的等級,然後count[X]+1。但是每次對區間求和,都會因為循環浪費時間。


1000ms的限制內,必須使用高效的查詢方法來實現這個求和(即統計這些星星的個數)。


這道題是樹狀數組的模板題,容易找到這道題的AC

代碼。


樹狀數組的講解見:https://www.cnblogs.com/acgoto/p/8583952.html

https://blog.csdn.net/FlushHip/article/details/79165701#commentBox


代碼來自:https://www.cnblogs.com/kuangbin/archive/2012/08/09/2630072.html


蒟蒻的代碼基本就是上面鏈接裏的樣子,而且沒有寫註釋,所以去看這個代碼就好了。復制如下。另外拿了樹狀數組的圖來解釋一下。

 1 /*
 2 POJ 2352 Stars
 3 就是求每個小星星左小角的星星的個數。坐標按照Y升序,Y相同X升序的順序給出
 4 由於y軸已經排好序,可以按照x坐標建立一維樹狀數組
 5 */
 6 #include<stdio.h>
 7 #include<iostream>
 8 #include<algorithm>
 9 #include<string.h>
10 using namespace std;
11 const int MAXN=15010;
12 const int MAXX=32010;
13 int c[MAXX];//樹狀數組的c數組
14 int cnt[MAXN];//統計結果
15 int lowbit(int x)
16 {
17     return x&(-x);
18 }
19 void add(int i,int val)
20 {
21     while(i<=MAXX)
22     {
23         c[i]+=val;
24         i+=lowbit(i);
25     }
26 }
27 int sum(int i)
28 {
29     int s=0;
30     while(i>0)
31     {
32         s+=c[i];
33         i-=lowbit(i);
34     }
35     return s;
36 }
37 int main()
38 {
39     //freopen("in.txt","r",stdin);
40     //freopen("out.txt","w",stdout);
41     int n;
42     int x,y;
43     while(scanf("%d",&n)!=EOF)
44     {
45         memset(c,0,sizeof(c));
46         memset(cnt,0,sizeof(cnt));
47         for(int i=0;i<n;i++)
48         {
49             scanf("%d%d",&x,&y);
50             //加入x+1,是為了避免0,X是可能為0的
51             int temp=sum(x+1);
52             cnt[temp]++;
53             add(x+1,1);
54 
55         }
56         for(int i=0;i<n;i++)
57          printf("%d\n",cnt[i]);
58     }
59     return 0;
60 }

技術分享圖片

這三個函數是維護樹狀數組用的。從這幅圖看得出來,這裏是通過存儲後綴和(吧?)來提高求和速度的。2^15=32768>32000,意味著本題更新C[]數組中一個數最多只需更新16層(16個數),就可以保證後綴和的一致性。而求和時即使是最大的數組下標2^15-1=32767=0111,1111,1111,1111,也僅需對15個後綴和求和。這個數組做到高效地更新和求和。


不理解樹狀數組,就這幅圖簡單地解釋下:

count數組是上文的星星統計數,而非代碼中的cnt,代碼中cnt是等級統計數)

count數組即兩篇講解中的基礎數組A[]

(原代碼中的c數組被我寫為講解中的大寫C


0100(4)0110(6)0111(7)1000(8) 為例,取這些數的最低位1,會變成100(4)10(2)11000(8),這表示在C[]數組中,C[4]存儲了count[4][3][2][1]4個數的和,C[6]存儲了count[6][5]2個數的和,C[7]只有count[7]1個數,C[8]就是count[8]-[1]8個數了。


至於求count[0]count[X]的總和,以0110(6)0111(7)01011(11)為例,同樣看這幅圖,sum(6)=C[6]+C[4],用二進制來看是C[0110]+C[0100] (+C[0000])

sum(7)=C[7]+C[6]+C[4],二進制C[0111]+C[0110]+C[0100] (+C[0000])

sum(11)=C[11]+C[10]+C[8] ==> C[01011]+C[01010]+C[01000] (+C[00000])


你會發現,從左往右每一項中二進制的最低位1被替換為0,直至變成整個二進制數字變為0


在這道題中樹狀數組只有這兩個用途,因此只需要寫兩個函數,更新後綴和與求和。另外一個函數lowbit用於取最低位,直接寫進兩函數裏也可以。不過為了程序整潔寫作一個函數比較好。


由於讀入一個星星只需對統計數+1,此處僅僅維護了樹狀數組而沒有保存原來的基礎數組。


lowbit()不再解釋;


add():讀入一個星星如(5,5)後,X5的星星統計數count[5]++。那麽在樹狀數組中,C[5]本身要+1,父層C[6]+1C[8]C[16]C[32]等統統+1lowbit保證它能正常取父層,而非簡單地取C[2^n]


由於此題所謂樹狀數組更新就是+1,代碼中val可以去掉,直接寫成++,方便讀懂:

void add(int i)
{
    while(i<=32000)
    {
        C[i]++;
        i=i+lowbit(i);
    }
}

sum()即上文count[0]-[X]求和的代碼實現。


原代碼的寫法可以任意+val,但是如果在其他地方下出現基礎數組A[x]的值從4變成7或者從7變成4呢?


回來看原來的+1,這個+1表示基礎數組中count[X]的值比原來增大了1,變化量為|+1|,而C[X]及父層表示的是一段後綴和,對於求和公式s=a1+a2+a3+...


當一個成員a1=>a*變化了|a|時,原公式要加上|a|,才能使a1=>a*,保證sa1變化保持一致。


因此在這個情況下要維護基礎數組A[]add()函數不需要改,只需在調用時:


add(x, tmp-A[x]); //tmpA[x]新的值


代入47自行驗證,所有父層都實現了+3-3,樹狀數組成功更新。

[題解][樹狀數組] POJ 2352 - Stars 樹狀數組的簡單應用