树状数组大杂烩
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了树状数组大杂烩相关的知识,希望对你有一定的参考价值。
一、引言
作为胸牌退役狗决定再搞一发noip=。=
今天遇到好几个有关差分的暴力解法(noip就是要暴力!!!),又想起之前有个树状数组的区间修改&区间查询很玄学的表现为正确,决定想办法证明一发,但是内容单调不是我的性格,然后就有了这个大杂烩。。。(XX:树状数组被玩坏的日常)
本文借鉴了以下博文的部分内容,在此表示感谢分享交流,如有遗漏还请见谅(毕竟有的是一年前蒯的= =):
http://blog.csdn.net/qq_21841245/article/details/43956633(关于差分的思路)
(还有一个影响最大的找不到了,如果这位朋友发现本文有您的原创内容请评论联系我= =)
二、基础部分 (非初学者请自行忽略= =)
1、树状数组的正确性保证
树状数组之所以正确,在于问题中修改集合与查询集合的关系,我们需要保证修改的部分在查询时只被计算一次。
也就是说,对于某次修改,被修改的值的集合与查询这次修改时的查询集合的交集有且仅有一个元素。
而树状数组是通过每次将所在前缀末端在序列中rank值的二进制数,从低位向高位依次将1变为0,过程中出现的二进制数对应序列中rank值所在位置进行修改。
以点修改为例,修改某点的值,相当于将这个点的位置和后面的位置的前缀和进行了修改,所以将它依次将rank值的二进制数末尾的0变为1(从小到大)修改,然后查询某点位置的前缀和时将rank值的二进制数末尾位置的1进位(从大到小)求该前缀被修改的量,与初始前缀和相加即可(PS:其实赋初值时也可以看作对一个初始元素全部为0的序列的单点修改)。
上一段话中,若查询位置rank小于修改位置,则两者的方向是背道而驰的,不可能有交集;只有查询位置rank大于修改位置时,两者才可能有交集,由于我们是从二进制数的低位向高位变化,所以不存在查询位置rank将所有修改位置跳过,因为此时的查询位置rank较大,肯定有且仅有一次进位使得两集合同时存在这个元素。
2、单点修改+区间查询
这是树状数组最纯洁的时候的用法= =
1中说的将末尾的1进位和消去有个很简单的方法,利用C++补码的性质,对于对于一个位置 i 需要“跳”的距离为 lowbit = i & (-i)
也是正确性保证时证明的东西,直接贴代码:
1 void add(int x,int c){//修改位置x的值加上c 2 for(int i = x;i <= n;i += i&(-i)) 3 a[i] += c; 4 } 5 6 int sum(int x){//查询1~x的和 7 int ret = 0; 8 for(int i = x;i > 0;i -= i&(-i)) 9 ret += a[i]; 10 return ret; 11 }
3、区间修改+单点查询
其实与上述方式一样,点修改时我们考虑到对后续部分前缀和的影响,那么我们在段修改(前缀修改)时只需考虑对前面部分点的值的影响。
1 void add(int x,int c){//1~x每个数加上c 2 for(int i = x;i > 0;i -= i&(-i)) 3 b[i] += c; 4 } 5 6 int sum(int x){//查询位置x的值 7 int ret = 0; 8 for(int i = x;i <= n;i += i&(-i)) 9 ret += b[i]; 10 return ret; 11 }
三、正文:区间修改+区间查询 如何玩坏它= =
1、差分思想
引入数组d[i]表示 [i,n] 的共同增量,利用差分的思想可以发现,对于修改 [l,r] 只需修改d[l]和d[r+1],以下代码来自文首链接:
1 /* 2 作者:Airy 3 题目:p1082 线段树练习 3 4 */ 5 6 #include <cstdio> 7 #include <iostream> 8 9 #define lowbit(i) (i & (-i)) 10 11 using namespace std; 12 13 int readint() 14 { 15 int sign = 1, n = 0; char c = getchar(); 16 while(c < ‘0‘ || c > ‘9‘){ if(c == ‘-‘) sign = -1; c = getchar(); } 17 while(c >= ‘0‘ && c <= ‘9‘) { n = n*10 + c-‘0‘; c = getchar(); } 18 return sign*n; 19 } 20 21 const int Nmax = 200100; 22 23 int N, Q; 24 25 long long delta[Nmax]; // delta的前缀和 26 long long deltai[Nmax]; // delta * i的前缀和 27 long long sum[Nmax]; // 原始前缀和 28 29 long long Query(long long *array, int pos) 30 { 31 long long temp = 0ll; 32 while(pos > 0) 33 { 34 temp += array[pos]; 35 pos -= lowbit(pos); 36 } 37 return temp; 38 } 39 40 void Update(long long *array, int pos, int x) 41 { 42 while(pos <= N) 43 { 44 array[pos] += x; 45 pos += lowbit(pos); 46 } 47 } 48 49 int main() 50 { 51 N = readint(); 52 53 for(int i = 1; i <= N; ++i) 54 { 55 int x = readint(); 56 sum[i] = sum[i - 1] + x; 57 } 58 59 Q = readint(); 60 61 while(Q--) 62 { 63 int sign = readint(); 64 if(sign == 1) // 修改:把[l, r]区间均加上x 65 { 66 int l = readint(), r = readint(), x = readint(); 67 Update(delta, l, x); 68 Update(delta, r+1, -x); 69 Update(deltai, l, x * l); 70 Update(deltai, r+1, -x * (r+1)); 71 } 72 else // 查询:[l, r]区间和 73 { 74 int l = readint(), r = readint(); 75 long long suml = sum[l - 1] + l * Query(delta, l - 1) - Query(deltai, l - 1); 76 long long sumr = sum[r] + (r + 1) * Query(delta, r) - Query(deltai, r); 77 printf("%lld\n", sumr - suml); 78 } 79 } 80 81 return 0; 82 }
虽说风格不同,但还是很好看出其中的分析的。。。(代码中deltai[]即d数组)
2、表示自己还没搞懂,应该和上面差不多,自认为有点玄学
直接上代码,这个背背更健康:
1 void add_a(int x,int c){ 2 for(int i = x;i > 0;i -= i&(-i)) 3 a[i] += c; 4 } 5 6 int sum_a(int x){ 7 int ret = 0; 8 for(int i = x;i <= n;i += i&(-i)) 9 ret += a[i]; 10 return ret; 11 } 12 13 void add_b(int x,int c){ 14 for(int i = x;i <= n;i += i&(-i)) 15 b[i] += c * x; 16 } 17 18 int sum_b(int x){ 19 int ret = 0; 20 for(int i = x;i > 0;i -= i&(-i)) 21 ret += b[i]; 22 return ret; 23 } 24 25 void add(int l,int r,int c){ 26 add_a(r,c),add_b(r,c); 27 if(l > 1){ 28 add_a(l-1,-c); 29 add_b(l-1,-c); 30 } 31 } 32 33 void sum(int x){//这个是1~x的前缀和 34 if(x) 35 return sum_a(x) * x + sum_b(x-1); 36 else 37 return 0; 38 }
四、结语
呃,也没什么好说的了,给自己一句祝福:while(1){rp++;}
以上是关于树状数组大杂烩的主要内容,如果未能解决你的问题,请参考以下文章