[ 题解 ] [ 树状数组 ] POJ 2352 - Stars 树状数组的简单应用
VJudge题目:https://cn.vjudge.net/contest/283317#problem/A
即POJ 2352 - Stars:http://poj.org/problem?id=2352
题目要求:输入所有星星的坐标,处于某一星星左下方(包括左与下)的有多少星星,就算它有几级。如果它左下没有星星就算0级。现在要求你统计各等级的星星各有多少颗。
输入输出:一个数字N(1<=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),1,1000(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)后,X为5的星星统计数count[5]++。那么在树状数组中,C[5]本身要+1,父层C[6]+1,C[8]、C[16]、C[32]等统统+1。lowbit保证它能正常取父层,而非简单地取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*,保证s随a1变化保持一致。
因此在这个情况下要维护基础数组A[],add()函数不需要改,只需在调用时:
add(x, tmp-A[x]); //tmp是A[x]新的值
代入4,7自行验证,所有父层都实现了+3或-3,树状数组成功更新。