[ 题解 ] [ 树状数组 ] POJ 2352 - Stars 树状数组的简单应用

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)。坐标已按YX升序排列好。

示例:

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,树状数组成功更新。

posted @ 2019-02-16 20:19  Kaidora  阅读(207)  评论(1编辑  收藏  举报