[关闭]
@zsh-o 2018-03-17T18:56:18.000000Z 字数 3931 阅读 1202

前缀和数组 树形数组 线段树

算法


这是上上周学的知识点,但现在发现对树形数组的存储细节和节点访问时的下标计算有些忘了~忘了~~还是不贪快,多整理总结的好

这三种最终都可以理解成一颗的形式,按照树的方式更能理解每一种表达方式面临的问题

前缀和数组

用于求区间和,用于中间元素不修改的区间求和,如果修改中间元素AA[i],则该元素对应的前缀和数组PSUM[i]及其后所有元素都需要刷新

前缀和数组树

数组 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
A[i] 0 2 6 3 10 4 3 11 7 4 9 8 2 1 5 6 3
PSUM[i] 0 2 8 11 21 25 28 39 46 50 59 67 69 70 75 81 84

  1. for(int i=1; i<=n; i++){
  2. PSUM[i] = PSUM[i-1] + A[i];
  3. }

  1. for(int mi=0; mi<m; mi++){
  2. int a,b;
  3. cin>>a>>b;
  4. cout<<PSUM[max(a,b)]-PSUM[min(a,b)-1]<<endl;
  5. }

树形数组

树形数组

如上图所示树形数组的构造成了一种二叉树的形式,具体的思想是:现有的二进制,其二进制末尾有个0,那么树形数组的第项就与原数组的前个数有关,这里是求和,有神人把这个用了一个特别简洁的式子给结合起来了

  1. int lowbit(int x){
  2. return x&(-x);
  3. }

那么第i项的值与前lowbit(i)项有关,也就是说第项的二进制是lowbit(10) = 2,则BST[10] = A[9] + A[10],并且修改A[10],需要更新BST[10]BST[12]BST[16],在总结点是的情况下
然而,如果要计算前项的和的话,我们知道BST[10]只是为第的和,我们还需要前8项的和,而BST[8]正好是前项的和,而且,这样正好构成了一个循环

这里最巧妙的就是根据第i项的i的二进制编码,把原数据用二叉树的形式组织起来了

这样初始化树形数组需要借助于前缀和数组

  1. for(int i=1; i<=CTSIZE; i++){
  2. BST[i] = PRE[i] - PRE[i-lowbit(i)];
  3. }

计算前项的和是一个递归或者循环

  1. int sum(int idx){
  2. int total = 0;
  3. while(idx>0){
  4. total += BST[idx];
  5. idx -= lowbit(idx);
  6. }
  7. return total;
  8. }

如果需要改变第项的值,也是需要借助于函数,这个函数设计的真实巧妙

  1. //idx增加val
  2. void update(int idx, int val){
  3. while(idx <= CTSIZE){
  4. BST[idx] += val;
  5. idx += lowbit(idx);
  6. }
  7. }

最后如果要计算项的和需要做一次差

  1. sum(j) - sum(i-1)

线段树

线段树又叫区间树,顾名思义,其很容易求解区间问题,树形数组不容易求解区间问题,以至于在求区间和的时候用的是两个大区间相减的做法,但遇到求什么区间最值呀,就不行了
而且树形数组的节点修改的时间复杂度是,如果要修改一个区间中的所有值的话,树形数组需要一个一个的改,时间复杂度为

而线段树把区间表示成了一个完全二叉树,这里像树形数组一样,为了能够完全用数组来表示,而且能够用下标来提取相应元素,我们需要首先把节点扩成的幂次的形式

  1. //把N扩充为2的幂
  2. int t = 1;
  3. while(t<N)
  4. t = t<<1;
  5. N = t;

线段树

上图中,蓝色中性笔代表节点在数组中的位置,从上图可以看出来,我们用二分法的方法把每个二分区间表示了出来,这样其相应的关系也出来了,由于我们事先扩展成了的幂次,故,该树一共有层,总节点个数为,,非叶子节点也即区间个数为,叶子节点个数为

那么线段树的数组的第项为原始数组的值

  1. //叶子节点位置:[N, 2N-1]
  2. for(int i=0; i<N; i++){
  3. Tree[N+i] = A[i];
  4. }

从图也可以看出,第个节点的左右子树的下标正好是,也可以写成i<<1i<<1|1,因此用即可完成线段树初始化

  1. //第i个节点左右子树的位置为[2*i, 2*i+1]=> [i<<1, i<<1|1]
  2. //Tree从1开始
  3. for(int i=N-1; i>=1; i--){
  4. Tree[i] = min(Tree[i<<1], Tree[i<<1|1]);
  5. }

这时如果要改变一个单一的节点,只需要按照树修改相应的结构即可,其实也就是一直除以

  1. void change(int id, int value){
  2. int tid = N+id-1; //该节点Tree数组中的位置
  3. Tree[tid] = value;
  4. while(tid>1){
  5. tid = tid>>1;
  6. Tree[tid] = min(Tree[tid<<1], Tree[tid<<1|1]); //这里是求最小值
  7. }
  8. }

线段树可以完成区间查询,区间查询使用一个递归遍历该树,然后后序遍历返回值即可

  1. //[i,j]要查询的区间,[p,q]当前节点表示的区间,id当前节点下标
  2. int query(int i, int j, int p, int q, int id){
  3. if(i<=p && j>=q){
  4. return Tree[id];
  5. }
  6. if(p>j || q<i){
  7. return INT_MAX;
  8. }
  9. //左移可以使用id<<1|1,右移相当于/2不可以这样用,只能((p+q)>>1)+1
  10. int a1 = query(i, j, p, (p+q)>>1, id<<1);
  11. int a2 = query(i, j, ((p+q)>>1)+1, q, id<<1|1);
  12. return min(a1, a2);
  13. }

线段树-查询

例如,这里要查询区间,所有的波浪线为遍历到的节点,红波浪线不加星的表示要查询的区间与当前区间是交叉的,由此还要进行分裂查询;而红波浪线带红星的表示当前区间在要查询区间内部(i<=p && j>=q),直接返回当前数组中的值即可;黑色波浪线表示要查询的区间与当前区间不相关(p>j || q<i),需要返回一个无关的值,这里求得是最小值,所以返回的是极大值INT_MAX

有时候我们的问题是要修改一个区间中所有的值,例如,我们要把区间全部换成一个值,如上面所说,一个一个换时间复杂度是,我们要想办法把其优化到。如果用上面区间查询的方法修改区间值,那么该查询区间所有相关的区间均要修改,也就是说在区间查询中节点由于完全在查询区间里面而且自身就是各自子节点的统计,所以不必再遍历其子节点,而区间修改需要用后续遍历的方式改变非叶子节点的统计值,所以的子节点也需要遍历,因此时间复杂度为

为了能够达到,大哥们是这样考虑的,我们做区间修改的时候不改变中间区间的值,也即不改变6和11的值,反正其子节点的值都要改成同一个值,因此当前节点得值是区间长度X要改变的值,并增加一个“脏”标志位,表示该节点已经做了更改但其子节点未更改,然后下一次访问到该节点的时候,再把脏标志传递给左右两个子节点,这样段的修改和查询变为

  1. int seg_change(int i, int j, int p, int q, int value, int id){
  2. if(i<=p && j>=q){
  3. Lazys[id] = value;
  4. //这里只是简单的把区间内的数全部设定同一个值
  5. Tree[id] = value * (q-p+1);
  6. return Tree[id];
  7. }
  8. if(p>j || q<i){
  9. return Tree[id];
  10. }
  11. if(Lazys[id]!=-1){
  12. Lazys[id<<1] = Lazys[id];
  13. Tree[id<<1] = ((q-p+1)>>1) * Lazys[id];
  14. Lazys[id<<1|1] = Lazys[id];
  15. Tree[id<<1|1] = ((q-p+1)>>1) * Lazys[id];
  16. Lazys[id] = -1;
  17. }
  18. int a = seg_change(i, j, p, (p+q)>>1, value, id<<1);
  19. int b = seg_change(i, j, ((p+q)>>1)+1, q, value, id<<1|1);
  20. //后序遍历刷新和
  21. Tree[id] = a+b;
  22. return Tree[id];
  23. }
  24. //[i,j]要查询的区间,[p,q]当前节点表示的区间,id当前节点下标
  25. int query(int i, int j, int p, int q, int id){
  26. if(i<=p && j>=q){
  27. return Tree[id];
  28. }
  29. //求和,不在查询区间内的返回0
  30. if(p>j || q<i){
  31. return 0;
  32. }
  33. if(Lazys[id]!=-1){
  34. Lazys[id<<1] = Lazys[id];
  35. Tree[id<<1] = ((q-p+1)>>1) * Lazys[id];
  36. Lazys[id<<1|1] = Lazys[id];
  37. Tree[id<<1|1] = ((q-p+1)>>1) * Lazys[id];
  38. Lazys[id] = -1;
  39. }
  40. //左移可以使用id<<1|1,右移相当于/2不可以这样用,只能((p+q)>>1)+1
  41. int a1 = query(i, j, p, (p+q)>>1, id<<1);
  42. int a2 = query(i, j, ((p+q)>>1)+1, q, id<<1|1);
  43. return a1 + a2;
  44. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注