原题地址:点击打开链接
题目:
Problem Description
C国的死对头A国这段时间正在进行军事演习,所以C国间谍头子Derek和他手下Tidy又开始忙乎了。A国在海岸线沿直线布置了N个工兵营地,Derek和Tidy的任务就是要监视这些工兵营地的活动情况。由于采取了某种先进的监测手段,所以每个工兵营地的人数C国都掌握的一清二楚,每个工兵营地的人数都有可能发生变动,可能增加或减少若干人手,但这些都逃不过C国的监视。
中央情报局要研究敌人究竟演习什么战术,所以Tidy要随时向Derek汇报某一段连续的工兵营地一共有多少人,例如Derek问:“Tidy,马上汇报第3个营地到第10个营地共有多少人!”Tidy就要马上开始计算这一段的总人数并汇报。但敌兵营地的人数经常变动,而Derek每次询问的段都不一样,所以Tidy不得不每次都一个一个营地的去数,很快就精疲力尽了,Derek对Tidy的计算速度越来越不满:"你个死肥仔,算得这么慢,我炒你鱿鱼!”Tidy想:“你自己来算算看,这可真是一项累人的工作!我恨不得你炒我鱿鱼呢!”无奈之下,Tidy只好打电话向计算机专家Windbreaker求救,Windbreaker说:“死肥仔,叫你平时做多点acm题和看多点算法书,现在尝到苦果了吧!”Tidy说:"我知错了。。。"但Windbreaker已经挂掉电话了。Tidy很苦恼,这么算他真的会崩溃的,聪明的读者,你能写个程序帮他完成这项工作吗?不过如果你的程序效率不够高的话,Tidy还是会受到Derek的责骂的.
中央情报局要研究敌人究竟演习什么战术,所以Tidy要随时向Derek汇报某一段连续的工兵营地一共有多少人,例如Derek问:“Tidy,马上汇报第3个营地到第10个营地共有多少人!”Tidy就要马上开始计算这一段的总人数并汇报。但敌兵营地的人数经常变动,而Derek每次询问的段都不一样,所以Tidy不得不每次都一个一个营地的去数,很快就精疲力尽了,Derek对Tidy的计算速度越来越不满:"你个死肥仔,算得这么慢,我炒你鱿鱼!”Tidy想:“你自己来算算看,这可真是一项累人的工作!我恨不得你炒我鱿鱼呢!”无奈之下,Tidy只好打电话向计算机专家Windbreaker求救,Windbreaker说:“死肥仔,叫你平时做多点acm题和看多点算法书,现在尝到苦果了吧!”Tidy说:"我知错了。。。"但Windbreaker已经挂掉电话了。Tidy很苦恼,这么算他真的会崩溃的,聪明的读者,你能写个程序帮他完成这项工作吗?不过如果你的程序效率不够高的话,Tidy还是会受到Derek的责骂的.
Input
第一行一个整数T,表示有T组数据。
每组数据第一行一个正整数N(N<=50000),表示敌人有N个工兵营地,接下来有N个正整数,第i个正整数ai代表第i个工兵营地里开始时有ai个人(1<=ai<=50)。
接下来每行有一条命令,命令有4种形式:
(1) Add i j,i和j为正整数,表示第i个营地增加j个人(j不超过30)
(2)Sub i j ,i和j为正整数,表示第i个营地减少j个人(j不超过30);
(3)Query i j ,i和j为正整数,i<=j,表示询问第i到第j个营地的总人数;
(4)End 表示结束,这条命令在每组数据最后出现;
每组数据最多有40000条命令
每组数据第一行一个正整数N(N<=50000),表示敌人有N个工兵营地,接下来有N个正整数,第i个正整数ai代表第i个工兵营地里开始时有ai个人(1<=ai<=50)。
接下来每行有一条命令,命令有4种形式:
(1) Add i j,i和j为正整数,表示第i个营地增加j个人(j不超过30)
(2)Sub i j ,i和j为正整数,表示第i个营地减少j个人(j不超过30);
(3)Query i j ,i和j为正整数,i<=j,表示询问第i到第j个营地的总人数;
(4)End 表示结束,这条命令在每组数据最后出现;
每组数据最多有40000条命令
Output
对第i组数据,首先输出“Case i:”和回车,
对于每个Query询问,输出一个整数并回车,表示询问的段中的总人数,这个数保持在int以内。
对于每个Query询问,输出一个整数并回车,表示询问的段中的总人数,这个数保持在int以内。
Sample Input
1 10 1 2 3 4 5 6 7 8 9 10 Query 1 3 Add 3 6 Query 2 7 Sub 10 2 Add 6 3 Query 3 10 End
Sample Output
Case 1: 6 33 59
说明:
最近刚开始研究算法...第一次接触树状数组,趁还记得的时候记录下来,一方面方便自己复习,一方面也造福后人(笑),本人在理解此算法的时候借助了 fancy_boy 的博客(当然本文是我自己写的),原博客地址如下:
目的只是为了学习,如果原博主认为本人侵犯了他的权益,我马上删除=。=
我的代码里的输入语法是C++与C混合,是因为都用C++的输入会超时....所以本题其实更适合用C编写。
顺带一提,本题还有线段树算法,有兴趣的可以自己上网查找。
我的源代码:
#include <iostream>
#include <string.h>
#include <stdio.h>
using namespace std;
int a[50005], n;
int lowbit(int x)
{
return x&(-x);
}
int sum(int i)
{
int res = 0;
while (i>0)
{
res += a[i];
i -= lowbit(i);
}
return res;
}
void change(int i, int j)
{
while (i <= n)
{
a[i] += j;
i += lowbit(i);
}
}
int main()
{
int num, t, i, j;
char s[15];
cin >> t;
for (int k = 0;k<t;k++)
{
memset(a, 0, sizeof(a));
cin >> n;
printf("Case %d:\n", k + 1);
for (int m = 1;m <= n;m++)
{
cin >> num;
change(m, num);
}
scanf("%s", s);
while (s[0] != 'E')
{
cin >> i >> j;
switch (s[0])
{
case ('Q'):
{
printf("%d\n", sum(j) - sum(i - 1));
break;
}
case ('E'):break;
case ('A'):change(i, j);break;
case ('S'):change(i, -j);break;
}
if (s[0] != 'E')
scanf("%s", s);
}
}
return 0;
}
算法解析:
以下说法是按我个人的理解结合他人博客形成的,没有任何专业书籍基础,有错处欢迎交流。
本题的一般思路是创建数组a(数组存储数据默认从1开始,a[0]=0),然后逐个填入人数,每次操作改变数字,
在Query命令时将对应a[i]~a[j]的数字求和。因为每次算n个营地总人数就要计算n次相加,
这个方法明显太慢了
。
所以这里用到树形数组(如下图):
简单点概括,树形数组将原本较大的数组相加拆为好几个小数组相加。比如原本计算第1个营地至第13个营地的
和需要从
a[1]逐个加至a[13],但根据树形数组只需要计算a[8]+a[12]+a[13]就足够了。原因是树形数组用算法将
之前几
段数据的和储存在特定的位置
。如第1营地至第8营地的和就在a[8]里,第9至12营地的和在a[12]里。
树形数组是如何做到的呢?这里就涉及到位运算,也是算法最为天才和关键的地方。
源代码中的lowbit函数中,返回了一个神奇的值——x&(-x)。这个值所代表的含义为,若x的二进制写法中末尾有r
个0,则该值为
2^r
。
而i-=lowbit(i),就相当于抹掉 i 二进制写法最末尾的1。
是不是懵逼了?慢慢来。
&运算是只有两个数字为1才为1,其他情况都是0。
-x是x的补码,即x的反码+1。
所以,x&(-x)的结果都会化为(0....0)1(0...0)的形式,即只有一个1,其余都是0,1的位置是x从后往前第一次不为0的位置。
所以,假设末尾有r个0,最后的1在十进制中的数值就是2^r。
举例,13的二进制为1101,末尾为1,则反码为0010,补码为0011,作&运算后结果为0001,正是末尾一位,r=0,2^r=1,返回值为1。
12的二进制为1100,倒数第3位出现1,它的补码是0100,&运算后结果为0100,正是倒数第3位,r=2,2^r=4,返回值为4。
所以,i-=lowbit(i)相当于将i二进制写法的最后一个1抹去。
那么,这个返回值有什么意义吗?摸去最后一个1又代表着什么呢?
lowbit的返回值(即2^r),代表着当前数据的“监控范围”,也就是说,从这个数字往回数2^r个数据,这些数据的和存储在当前数据内。如a[8],lowbit(8)==8,所以从1~8的和都存在a[8]下。要说明的是,这里的1~8指的是各个营地中的人数,而非树状数组中a[1]至a[8]的和(因为a数组存的并非是单个营地人数)。所谓的“监控范围”,如图所示:
可以把它想象成高度不一的哨岗,2^r越大,哨岗越高,能掌控的范围就越大。
因为1,3,5,7,9,11,13,
15二进制尾数是1,他们的lowbit(x)(即2^r)==0,所以他们只能掌握自己。同理,2,6,10,14的2^r==2,
所以
他们掌握的范围就是自己和前一个数(即a[2]为第2营地与第1营地的和,a[6]为第5和第6营地的和,以此
类推)。
到了这里,你就应该能够理解lowbit返回值的意义——用特殊的方法改变数组下标的增加方式。
现在回头看change函数,它完成三个功能,加,减,赋值。
void change(int i, int j)
{
while (i <= n)
{
a[i] += j;
i += lowbit(i);
}
}
赋值的时候 i 从小到大,逐个将输入数字加到数组中(数组每个位置都会有数据,这个的正确性下文证明)。
加减则是将指定位置及之后的数据都增加或减少。
同理,sum函数就是将1~i营地的和用lowbit分为几段,用a上赋值时已经计算完成的和替代逐个相加的过程。
(每段都恰好能前后邻接,正确性下文证明)
以上是lowbit这个核心函数的解析。但是还有两个问题,那就是:
1、为什么lowbit的返回值能使数组每个成员都被访问?
2、为什么用lowbit分段恰好能前后邻接?
这两个问题其实是同一个答案。
之前有提到lowbit的返回值有“监控范围”的作用,监控范围的值是2^r。假设我们要计算sum(i),即第1营地至第 i 营地的人数和。现假设各营地的人数按顺序存在 f 里,则a[i]=f[i-2^r+1]+...+f[i](lowbit的值是2^r),又因为a[i-2^r]=
f[i2^(r+1)+1]+...+f[i-2^r]
,
这就是这个算法最流弊的地方!!
恰好无缝衔接!!
因为 i 是任意值(前提大于0),所以无论从哪里开始,是前移还是后移,都能访问每个位置。这样,两个问题就都有了答案。
或许有人还有一个问题,为什么用位运算?怎么想到的?这个...我只能说算法作者的脑子不一般...
总结:
树状数组之所以节省时间,是因为用了lowbit的位运算巧妙地将数组的和存在了特定的位置。具体位运算实现的过程如果无法理解可以略过,但lowbit函数起到的作用和树状数组的结构一定要多加理解。其余部分较为简单,就不多花笔墨了。
——要是有更好的理解或是我的说法有误的,欢迎留言讨论~