树状数组和线段树常用模型

来源:互联网 发布:2015中国造船数据分析 编辑:程序博客网 时间:2024/06/11 18:25

树状数组d:

树状数组(Fenwick tree,又名binary indexed tree),是一种很实用的数据结构。它通过用节点i,

记录数组下标在[ i –2^k + 1, i]这段区间的所有数的信息(其中,k为i的二进制表示中末尾0的个数,

设lowbit(i) = 2^k),实现在O(lg n) 时间内对数组数据的查找和更新。

树状数组的传统解释图,不能很直观的看出其所能进行的更新和查询操作。其最主要的操作函数

lowbit(k)与数的二进制表示相关,本质上仍是一种二分。因而可以通过二叉树,对其进行分析。

事实上,从二叉树图,我们对它所能进行的操作和不能进行的操作一目了然。

和前面提到的点树类似,先画一棵二叉树,然后对节点中序遍历(点树是采用广度优先),

每个节点仍然只记录左子树信息,见图:

 

由于采用的是中序遍历,从节点1到节点k时,刚好有k个叶子被统计。

可以证明:

     叶子k,一定在节点k的左子树下。

     以节点k为根的树,其左子树共有叶子lowbit(k)

节点k的父节点是:k + lowbit(k) 或 k - lowbit(k)

节点k + lowbit(k) 是节点k的最近父节点,且节点k在它的左子树下。

节点k - lowbit(k) 是节点k的最近父节点,且节点k在它的右子树下。

节点k,统计的叶子范围为:(k - lowbit(k),  k]。

节点k的左孩子是:k - lowbit(k) / 2

 

下面分析树状数组两面主要应用:

1 更新数据x,进行区间查询。

2 更新区间,查询某个数。

由于,树状数组只统计了左子树的信息,因而只能查询更新区间[1, x]。只在在满足[x,y]的

信息可以由[1,x-1]和[1,y]的信息推导出时,才能进行区间[x,y]的查询更新。

这也是树状数组不能用于任意区间求最值的根本原因。

 

先定义两个集合:

up_right(k) : 节点k所有的父节点,且节点k在它们的左子树下。

up_left(k) :  节点k所有的父节点,且节点k在它们的右子树下。

 

1  更新数据x,查询区间[1,y]。

显然,更新叶子x,要找出叶子x在哪些节点的左子树下。因而节点k、所有的up_right(k)

都要更新。

查询[1, y],实际上就是把该区间拆分成一系列小区间,并找出统计这些区间的节点。

可以通过找出y在哪些节点的右子树下,这些节点恰好不重复的统计了区间[1, y-1]。

因而要访问节点y、所有的up_left(y)。

 

2 更新区间[1,y],查询数据x

  这和前面的操作恰好相反。与前面的最大不同之处在于:节点保存的不再是其叶子总个数这些信息,

而是该区间的所有叶子都改变了多少。也就是说:每个叶子的信息,分散到了所有对它统计的节点上。

因此操作和前面相似:

  更新[1,y]时,更新节点y、所有up_left(y)。

  查询x时,  访问x、所有up_right(x)。

 

区间问题 :

(1)“改点求段”型,即对于序列A有以下操作:

【1】修改操作:将A[x]的值加上c;

【2】求和操作:求此时A[l..r]的和。

这是最容易的模型,不需要任何辅助数组。树状数组中从x开始不断减lowbit(x)(即x&(-x))可以得到整个[1..x]的和,

而从x开始不断加lowbit(x)则可以得到x的所有前趋。

一维数组:

 1 传入数组A[]的长度n,及对A[i]出的修改,传出A[1`````i]处的和
2 (1): add(i,w) 在数组A[i]加上w
3 (2): sum(i) 求A[1````i]的和
4
5 #define lowbit(x) ((x)&(-(x)))
6 const int Max = 50005;
7
8 int n, ar[Max];
9
10 void add(int i, int w){
11 while(i <= n){
12 ar[i] += w;
13 i += lowbit(i);
14 }
15 }
16
17 int sum(int i){
18 int ans = 0;
19 while(i > 0){
20 ans += ar[i];
21 i -= lowbit(i);
22 }
23 return ans;
24 }
25 void init(){
26 memset(ar, 0, sizeof(ar));
27 }



二维数组:

 1 传入矩阵行列rol,col;
2 操作1:add(i,j,w)在A[1```i][1````j]加上w
3 操作2:sum(i,j)求A[i][j]处的值
4
5 const int Max = 1005;
6
7 int row, col;
8 int ar[Max][Max];
9
10 int lowbit(int x){
11 return x & (-x);
12 }
13 void add(int i, int j, int w){
14 int tmpj;
15 while(i <= row){
16 tmpj = j;
17 while(tmpj <= col){
18 ar[i][tmpj] += w;
19 tmpj += lowbit(tmpj);
20 }
21 i += lowbit(i);
22 }
23 }
24 int sum(int i, int j){
25 int tmpj, ans = 0;
26 while(i > 0){
27 tmpj = j;
28 while(tmpj > 0){
29 ans += ar[i][tmpj];
30 tmpj -= lowbit(tmpj);
31 }
32 i -= lowbit(i);
33 }
34 return ans;
35 }
36 void init(){
37 memset(ar, 0, sizeof(ar));
38 }

 

(2)“改段求点”型,即对于序列A有以下操作:

【1】修改操作:将A[l..r]之间的全部元素值加上c;

【2】求和操作:求此时A[x]的值。

这个模型中需要设置一个辅助数组B:B[i]表示A[1..i]到目前为止共被整体加了多少(或者可以说成,

到目前为止的所有ADD(i, c)操作中c的总和)。则可以发现,对于之前的所有ADD(x, c)操作,

当且仅当x>=i时,该操作会对A[i]的值造成影响(将A[i]加上c),又由于初始A[i]=0,

所以有A[i] = B[i..N]之和!而ADD(i, c)(将A[1..i]整体加上c),将B[i]加上c即可——只要对B数组

进行操作就行了。这样就把该模型转化成了“改点求段”型,只是有一点不同的是,SUM(x)不是求B[1..x]的和而是

求B[x..N]的和,此时只需把ADD和SUM中的增减次序对调即可(模型1中是ADD加SUM减,这里是ADD减SUM加)。

void ADD(int x, int c){for (int i=x; i>0; i-=i&(-i)) b[i] += c;}int SUM(int x){int s = 0;for (int i=x; i<=n; i+=i&(-i)) s += b[i];return s;}

操作【1】:ADD(l-1, -c); ADD(r, c);操作【2】:SUM(x)。

 

(3)“改段求段”型,即对于序列A有以下操作:

【1】修改操作:将A[l..r]之间的全部元素值加上c;

【2】求和操作:求此时A[l..r]的和。

这是最复杂的模型,需要两个辅助数组:B[i]表示A[1..i]到目前为止共被整体加了多少(和模型2中的一样),

C[i]表示A[1..i]到目前为止共被整体加了多少的总和(或者说,C[i]=B[i]*i)。

对于ADD(x, c),只要将B[x]加上c,同时C[x]加上c*x即可(根据C[x]和B[x]间的关系可得);

而ADD(x, c)操作是这样影响A[1..i]的和的:若x<i,则会将A[1..i]的和加上x*c,否则(x>=i)

会将A[1..i]的和加上i*c。也就是,A[1..i]之和 = B[i..N] 之和 * i + C[1..i-1]之和。

这样对于B和C两个数组而言就变成了“改点求段”(不过B是求后缀和而C是求前缀和)。
另外,该模型中需要特别注意越界问题,即x=0时不能执行SUM_B操作和ADD_C操作!

 

void ADD_B(int x, int c){for (int i=x; i>0; i-=i&(-i)) B[i] += c;}void ADD_C(int x, int c){for (int i=x; i<=n; i+=i&(-i)) C[i] += x * c;}int SUM_B(int x){int s = 0;for (int i=x; i<=n; i+=i&(-i)) s += B[i];return s;}int SUM_C(int x){int s = 0;for (int i=x; i>0; i-=i&(-i)) s += C[i];return s;}inline int SUM(int x){if (x) return SUM_B(x) * x + SUM_C(x - 1); else return 0;}

操作【1】:ADD_B(r, c); ADD_C(r, c);if (l > 1) {ADD_B(l - 1, -c); ADD_C(l - 1, -c);}操作【2】:SUM(r) - SUM(l - 1)。



参考文献 :

 http://www.cnblogs.com/flyinghearts/archive/2011/04/11/2013111.html

http://www.cppblog.com/MatoNo1/archive/2011/03/19/142226.html


更精简的版本:http://blog.sina.com.cn/s/blog_9dd49f8101014v8g.html

线段树:

一: 区间求和

给定一个数组A,定义Add(s, t, d)的操作为对A[s]...A[t]加d,定义Query(s, t)的

操作为查询A[s] + ... + A[t]的值

实现 :每条线段加一个sum属性,用来保存<s,t>的部分和,再加一个d属性,保存A[s]...A[t]的

共同增量,Add操作的时候 只需遍历到匹配线段,修改其d属性,不用再往下修改被匹配线段

覆盖的孩子节点了。求A[s]+...+A[t]的时候,把<s,t>的sum和那些覆盖<s,t>的线段的累计

增量乘以(t-s+1)加起来即为所求:
A[s]+...+A[t] = <s,t>'s sum + 累计增量*(t-s+1)

 1 传入数组num[]初始值,传出l到r段num[]数组的总和ans;
2 操作1:build(l, r, id) 初始化线段树
3 2:add(l,r,val,id) 将l到r段num[]数组的值都加上val;
4 3:query(int l,int r,int id) 查询l到r段num[]数组的总和ans;
5
6 const int Max = 100050;
7
8 #define L(x) ((x)<<1)
9 #define R(x) ((x)<<1|1)
10
11 struct node{
12 int l,r;
13 int mid(){ return (l+r)>>1; }
14 long long sum,add;
15 }sg[3*Max];
16 int num[Max];
17 long long ans;
18
19 void build(int l, int r, int id){
20 sg[id].l = l; sg[id].r = r;
21 sg[id].add = 0;
22 if(l == r) sg[id].sum = num[l];
23 else{
24 build(l, sg[id].mid(), L(id));
25 build(sg[id].mid()+1, r, R(id));
26 sg[id].sum = sg[L(id)].sum + sg[R(id)].sum;
27 }
28 }
29 void add(int l,int r,int val,int id){
30 if(sg[id].l==l&&sg[id].r==r){
31 sg[id].add+=val;
32 return ;
33 }
34 sg[id].sum+=(r-l+1)*val;
35 if(l<=sg[L(id)].r) add(l,min(r,sg[L(id)].r),val,L(id));
36 if(r>=sg[R(id)].l) add(max(l,sg[R(id)].l),r,val,R(id));
37 }
38 void query(int l,int r,int id){
39 ans+=(r-l+1)*sg[id].add;
40 if(sg[id].l==l&&sg[id].r==r) ans+=sg[id].sum;
41 else {
42 if(l<=sg[L(id)].r) query(l,min(r,sg[L(id)].r),L(id));
43 if(r>=sg[R(id)].l) query(max(l,sg[R(id)].l),r,R(id));
44 }
45 }
46 void init(){
47 memset(num,0,sizeof(num));
48 }

 

题目: poj  3468

 

参考文献 :http://kenby.iteye.com/blog/954112




本文转自:
http://www.cnblogs.com/louisnit/archive/2012/2/29.html

线段树理解:

原创粉丝点击