数据结构——树状数组

我们今天来讲一个应用比较广泛的数据结构——树状数组

它可以在O(nlogn)的复杂度下进行单点修改区间查询,下面我会分成三个模块对树状数组进行详细的解说,分别是树状数组基本操作、树状数组区间修改单点查询的实现、树状数组查询最值的实现



一.

树状数组一般分为三种操作,初始化、修改、查询

在讲基本操作之前,我们先来看一张图

这张图就是树状数组的存储方式,对于没有接触过树状数组的人来说看懂上面这张图可能有些困难,上图的A数组就是我们的原数组,C数组则是我们需要维护的数组,这样存储能干什么呢,比如我们在查询A[1~7]数组的前缀和时,可以将C[4]、C[6]、C[7]三个数组的值加起来,那么它就是我们要求的A[1~7]的前缀和,那么为什么是C[4]、C[6]、C[7]三个数组呢

我们来看一下4、6、7在二进制下是什么

7 = 2+ 21 + 2 = (111)2

6 = 22 + 2 = (110)2

4 = 22 = (100)2

于是我们发现,从7到4,我们的二进制的1从最后一位开始减少,再根据上图,我们可以发现,C[i]数组存储区间为[ i - 2k + 1,i],其中k是二进制下i末尾的1

那么我们如何来求这个末尾的1呢,这里我们用了lowbit(x)表示x在二进制下末尾的1,lowbit的求法如下:

inline int lowbit(int x)
{
  return x&(-x);
}

那么为什么可以用x & (-x)求出lowbit,这要从二进制负数存储方法开始说了

二进制下的负数是将原来的整数取反后加1表示的,就像这样:

原数字x:(100101000)2

取反~x:(011010111)2

负数~x+1:(011011000)2

我们发现,这样操作后,末尾1前面的数都被取反了,而后面的数全部变成了0,于是

x & (-x) = (000001000)2

这样,我们就将二进制下的末尾1求出来了

下面我们讲树状数组的基本操作

inline void add(int x,int y) //给x加上y
{
  while(x <= n)
  {
    c[x] += y;
    x += lowbit(x);
  }
  return ;
}

inline int query(int x) //查询1~7的前缀和
{
  int ans = 0;
  while(x)
  {
    ans += c[x];
    x -= lowbit(x);
  }
  return ans;
}

当我们求区间l ~ r的区间和时,可以用C[r] - C[l - 1]

如果你还是不太懂,就结合着上面的图和代码,感性理解一下理解



二.

下面我们讲树状数组如何做到区间修改(单点查询过于简单我就不提了)

我们可以发现,当我们让区间l ~ r加上v时,实际上相当于我们先将区间l ~ n加上v,再将区间r + 1 ~ n减去v

所以,第一种方法,我们可以再维护一个树状数组B用来存区间修改的情况,当我们查询某一点i时,用B[1~i]的前缀和加上我们原来维护的数组,就可以做到区间修改单点查询了

不过,在这里我们有一种巧妙的方法可以不用维护两个树状数组,我们可以维护一个差分数组C,让A[i] - A[i - 1]加入数组C,这样我们在修改区间l ~ r时,只要让l加上v,再让r + 1减去v就行了,证明非常简单,因为l ~ r中的数同时加上了一个数,A[i] - A[i - 1]不变

这样我们就用一个树状数组进行了区间修改单点查询操作,在查询一个数x时,A[x]就是C[1~x]的前缀和

有一道洛谷的板子题可供练习

洛谷 P3368

代码如下:

#include <cstdio>
#include <cstring>
#include <algorithm>

const int maxn = 1e6 + 6;
int n,m;
int cost[maxn];

inline int read()
{
  char ch = getchar();int x = 0,f = 1;
  while(ch<‘0‘||ch>‘9‘){if(ch == ‘-‘) f = -1; ch = getchar();}
  while(ch>=‘0‘&&ch<=‘9‘){x = x*10 + ch -‘0‘; ch = getchar();}
  return x*f;
}

inline int lb(int  x)
{
  return x&(-x);
}

inline void add(int a,int v)
{
  while(a<=n)
  {
    cost[a] += v;
    a += lb(a);
  }
}

inline int query(int a)
{
  int ans = 0;
  while(a)
  {
    ans += cost[a];
    a -= lb(a);
  }
  return ans;
}

int main(int argc, char const *argv[])
{
  n = read();
  m = read();
  int pre = 0;
  for(int i = 1;i <= n;i ++)
  {
    int val = read();
    add(i,val - pre);
    pre = val;
  }
  for(int i = 1;i <= m;i ++)
  {
    int flag,x,y,k;
    flag = read();
    if(flag == 1)
    {
      x = read();
      y = read();
      k = read();
      add(x,k);
      add(y+1,-k);
    }
    else
    {
      x = read();
      printf("%d\n",query(x));
    }
  }
  return 0;
}


三.

下面我们来讲树状数组查询区间最值,虽然它的复杂度为O(nlognlogn),但是依然是我们可以接受的

我们来考虑如何维护最值,一个显然的方法我们对于每个元素都让它自身的值和覆盖它的C数组取最值,复杂度为O(nlogn),然后当我们要改变一个数的值的时候,将C数组清空然后重新维护,显然这个复杂度并不理想

我们接着考虑有没有更快的维护方法,于是我们发现,对于一个区间C[x],能转移到它的只有C[x - 20]、C[x - 21]、C[x - 22]...C[x - 2 k],且2< lowbit(x),2k+1 >= lowbit(x)

这样,我们就维护出区间C[x]的最值了,且维护的复杂度为O(lognlogn)

代码如下:

inline void modify(int x,int y) //把x的值改为y
{
  a[x] = y;
  while(x <= n)
  {
    c[x] = a[x];
    for(int i = 1;i < lowbit(x);i <<= 1) c[x] = std::max(c[x],c[x - i]);
    x += lowbit(x);
  }
  return ;
}

查询的方法也非常简单,我们从查询的右端点开始找被包含的C数组,若r - lowbit(r) >= l,就用C[r]维护最值,否则,我们就直接用A[r]维护最值

代码如下:

inline int query(int l,int y) //查询区间l ~ r的最大值
{
  int ans = 0;
  while(l <= r)
  {
    ans = std::max(ans,a[r]);
    r--;
    while(l <= r - lowbit(r))
    {
      ans = std::max(ans,c[r]);
      r -= lowbit(r);
    }
  }
  return ans;
}

这里有一道例题

HDU 1754

就是一个树状数组维护最值的板子题,代码如下:

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>

const int maxn = 2e5+5;
int n,m;
int a[maxn];
int cost[maxn];

inline int lb(int x)
{
    return x&(-x);
}

inline void modify(int x)
{
    while(x<=n)
    {
        cost[x] = a[x];
        for(int i = 1;i < lb(x);i<<=1) cost[x] = std::max(cost[x],cost[x-i]);
        x += lb(x);
    }
}

inline int query(int l,int r)
{
    int ans = 0;
    while(l <= r)
    {
        ans = std::max(ans,a[r]);
        r --;
        while(l <= r-lb(r))
        {
            ans = std::max(ans,cost[r]);
            r -= lb(r);
        }
    }
    return ans;
}

int main()
{
    while(scanf("%d %d",&n,&m)!=EOF)
    {
        memset(a,0,sizeof(a));
        memset(cost,0,sizeof(cost));
        for(int i = 1;i <= n;i ++)
        {
            scanf("%d",&a[i]);
            modify(i);
        }
        for(int i = 1;i <= m;i ++)
        {
            char flag = getchar();
            while(flag!=‘Q‘&&flag!=‘U‘)flag = getchar();
            if(flag==‘Q‘)
            {
                int l,r;
                scanf("%d %d",&l,&r);
                printf("%d\n",query(l,r));
            }
            else if(flag==‘U‘)
            {
                int x,v;
                scanf("%d %d",&x,&v);
                a[x] = v;
                modify(x);
            }
        }
    }
    return 0;
} 

原文地址:https://www.cnblogs.com/Ackers/p/10090713.html

时间: 2024-11-10 08:32:06

数据结构——树状数组的相关文章

HDU 1556 数据结构-树状数组-改段求点

题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1556 解题思路:树状数组,只要了解树状数组的原理就不用死记模板了,总之树状数组管理的就是前缀和,高度越高的的结点管理的范围越广 所以要是改点求段:更改一个点就要向上传递 所以要是改段求点:更改一个点就要向下传递 代码: #include <cstdio> #include <iostream> #include <cstring> using namespace std;

数据结构——树状数组篇

原地址:My CSDN Blog  那边还未通过审核,先看这边的吧…… 线段树是一个很好的维护区间关系的这样的一个数据结构,但是,很多时候我们可以用更小空间.更快速度(更大尺寸呢.,全景天窗,五菱宏光?)的数据结构来维护一个前缀关系. ??? 上面的这张图是表示的一个有8个叶子节点的线段树,接下去,我们给它进行一个变形: ??? 然后呢,我们把2.4.6.8.……这样的元素推到最顶端的空上边去,想让2表示1-2这段区间,让4表示1-4这段区间,让6表示5-6这段区间,让8表示1-8这段区间. 然

实用数据结构---树状数组(二叉索引树)

树状数组适用于动态连续和查询问题,就是给定一个区间, 查询某一段的和或者修改某一位置的值. 关于树状数组的结构请去百度百科,否则将看不懂下面内容 我们看这个题 士兵杀敌(二) 时间限制:1000 ms  |  内存限制:65535 KB 难度:5 描述 南将军手下有N个士兵,分别编号1到N,这些士兵的杀敌数都是已知的. 小工是南将军手下的军师,南将军经常想知道第m号到第n号士兵的总杀敌数,请你帮助小工来回答南将军吧. 南将军的某次询问之后士兵i可能又杀敌q人,之后南将军再询问的时候,需要考虑到新

用树状数组处理逆序对[数据结构][树状数组]

逆序对 ——!x^n+y^n=z^n 可以到这里[luogu]: https://www.luogu.org/problem/show?pid=1908 题意:对于给定的一段正整数序列,逆序对就是序列中ai>aj且i<j的有序对.知道这概念后,他们就比赛谁先算出给定的一段正整数序列中逆序对的数目. 假如为这些数为: 8 2 3 1 7 如果我们把数一个个加进来,用一个数组a[i]统计i出现了几次. a的初始状态: 8加进来后: 由于不存在比8大的数,说明没有产生逆序对 2加进来后: 统计比2大

ACM数据结构-树状数组

模板: int n; int tree[LEN]; int lowbit(int x){ return x&-x; } void update(int i,int d){//index,delta while(i<=n){ tree[i]+=d; i+=lowbit(i); } } int getsum(int i){ int ans=0; while(i>0){ ans+=tree[i]; i-=lowbit(i); } return ans; } 示意图: 1.Ultra-Quic

[数据结构] 树状数组 的C程序实现

int tree[100001];//树状数组,用于取区间[x,y]的数据的和 /* & 特殊运算,t&(-t)的值(十进制),就是t在2进制下,从右往左数第一个1出现的位置. 结合树状数组的特殊性质,这个值有用 */ int lowbit(int t) { return t&(-t); } /* 假设对处在数组序号x的数据进行了更改,让x位置的数据有了增量v 对树状数组进行如下修改,使相关的包含x位数据的和都增加v 根据树状数组的性质,也就是对下标为 x, x+lowbit(x)

2019.9.25 初级数据结构——树状数组

一.树状数组基础 学OI的同学都知道,位运算(对二进制的运算)比普通运算快很多.同时我们接触到了状态压缩的思想(即将0-1的很多个状态压缩成十进制的一个数,每一个二进制位表示一个状态).由于在实际做题过程当中,由于数据范围限制,我们必须采用更高效的存储.查询方法,于是树状数组应运而生. 首先我们检查传统的存储状态.对于数组的每一个下标i,其所存储的有效信息只有一个a[i](这是传统数组).而对于树状数组,我们每一位下标可以存储lowbit(i)个有效信息.这个lowbit运算一会再说.所以尽管树

C++-hdu1166-敌兵布阵[数据结构][树状数组]

单点修改+区间查询=树状数组 空间复杂度O(n) 时间复杂度O(mlogn) 1 #include <set> 2 #include <map> 3 #include <cmath> 4 #include <queue> 5 #include <vector> 6 #include <cstdio> 7 #include <cstdlib> 8 #include <cstring> 9 #include <

C++-hdu1394-Minimum Inversion Number[数据结构][树状数组]

给出0~n-1的一个排列,可以整体移动,求逆序对最小值 把数字num[i]的加入,等价于树状数组的第n-num[i]位加1 因为num[i]是第 (n-1)-num[i]+1=n-num[i]大的数字,产生逆序对,只可能在其之前已经插入了数字,此时直接区间查询即可 1 #include <set> 2 #include <map> 3 #include <cmath> 4 #include <queue> 5 #include <vector>