树状数组教程(含习题)


ps.题目多因python自身特性会T掉几个点,只要没WA就不用在意。部分内容在我的这篇文章中也有展示:
前缀和、树状数组和线段树的区别_陈子昂-北工大的博客-CSDN博客
因为树状数组的内容过多所以打算单独成篇进行讲解。(要是能替掉所有线段树就好了,线段树太难了orz)

树状数组:

非常高效的数据结构,可以满足大部分需求。可以区间求和、区间最大值、单点修改。一般不能区间修改(就这个结构而言)

lowbit(最低位)

  1. 是二进制从右向左的第一个1,即最低为的1.如6的二进制是110,所以返回的是2

在这里插入图片描述

图片出处:树状数组(求逆序对)_baby的我的博客-CSDN博客_树状数组求逆序对,已征得作者同意使用

特征

  1. x-lowbit(x)是tr(x)管辖外的最大的一个,tr(x)维护的区间是[x-lowbit(x)+1, x]。一定注意x-lowbit(x)不是指子节点,x-lowbit(x)是一个==“新的开始”==。
  2. x+lowbit(x)是tr(x)的父节点
  3. 索引必须从1开始,如果从0开始,不管+lowbit还是-lowbit都会是0,即会陷入死循环

原理

  1. 树状数组tr[x]维护的是x从去掉最低位+1到其本身的范围,所以x-lowbit(x)会直接跳到下一个管辖范围
  2. 因为是2进制,x+lowbit(x)会使最低位进位,就可以得到最小的维护这个值的节点,也就是x的父节点

题型

区间和

题目传送门

P3374 【模板】树状数组 1 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

code
def lowbit(x):  # 求的一个数的二进制的第一个1的位置,比如6,它的二进制:0110,从右往左第一个1的位置是1(从零开始),那么lowbit(6)就等于2的1次方就等于2
    return x & -x


def query(x, y):  # 查询[x+1, y]
    ans = 0
    while y > x:
        ans += tr[y]
        y -= lowbit(y)
    while x > y:
        ans -= tr[x]
        x -= lowbit(x)
    return ans


def add(x, k):
    while x <= n:
        tr[x] += k
        x += lowbit(x)


n, m = map(int, input().split())
tr = [0]*(n+1)
a = list(map(int, input().split()))
for i in range(1, n+1):
    tr[i] += a[i-1]
    if (fa := i+lowbit(i)) <= n:
        tr[fa] += tr[i]
for _ in range(m):
    i, j, k = map(int, input().split())
    if i == 1:
        add(j, k)  # 向上叠加
    else:
        print(query(j-1, k))
建树
1. O(nlogn)建树:就是对每个点单点更新
2. O(n)建树(都以区间和建树为例):
code1(利用前缀和:会浪费空间)
for i in range(1, n):
    tr[i] = d[i] - d[i-lowbit(i)]  # d是前缀和数组, 因为lowbit(i)就是tr(i)维护的原数组的长度

​ 原本nlogn就是因为有重复计算的数组,那么只要把已经建好的部分直接累加到后面就可以保证每个都只计算一次,即O(n)

code2
for i in range(1, n):
    tr[i] = a[i]
    j = 1
    flag = lowbit(i)  # 对应上面的图,每个数字下面连的就是要加上去的
    while j < flag:
        tr[i] += tr[i-j]
        j <<= 1
code3首推(简洁)
for i in range(1, n):
    tr[i] += a[i]
    if (fa := i+lowbit(i)) <= n:
    	tr[fa] += tr[i]  # +最低位变父亲

code2\3先进的地方就在于不是利用a来推,而是利用已经建好的树,避免了a的重复叠加。code2和code3进行操作的次数一定是一样的,code2看起来多但是有些节点甚至是没子结点的

单点修改
def add(x, k):
    while x <= n:
        tr[x] += k
        x += lowbit(x)
区间查询
正常查询
def query(x):
    ans = 0
    while x:
        ans += tr[x]
        x -= lowbit(x)
    return ans
print(query(j)-query(i-1))
优化查询
def query(x, y):
    """对区间和而言是[x+1, y], 因为到x都被减掉了"""
    ans = 0
    while y > x:
        ans += tr[y]
        y -= lowbit(y)
    while x > y:
        ans -= tr[x]
        x -= lowbit(y)
原理
  1. 完全避免了重复部分的计算,比如查询[6, 7](一定要注意这样写和原始的都是对应7-5,左边界始终差了一位),则需要计算([7]+[6]+[4])-([5]+[4]) ,其中[4]就是重复的
  2. 以上面为例5:101,7:111。两个同时抹掉最后一位,不相等,再抹掉倒数第二位,两个相等了,不用再计算了,必定消掉
  3. 但是一位一位算就失去了lowbit的优势,所以采取两个互相逼近的方式,能一直取到两个相等
  4. 这样一定不会抹到开头相同的部分,因为到相同的部分一定小于或相等

区间修改,单点查询

题目传送门

P3368 【模板】树状数组 2 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

code
def lowbit(x):  # 树状数组可借助差分数组实现区间修改,单点求值
    return x & -x


def add(x, k, t):  # 树状数组的单点修改,原数组的区间修改
    while x <= n:
        t[x] += k
        x += lowbit(x)


def query(x, t):
    ans = 0
    while x:
        ans += t[x]
        x -= lowbit(x)
    return ans


n, m = map(int, input().split())
a = [0]+list(map(int, input().split()))   # 和前缀和一样前面要留一个0
tr = [0]*(n+1)
for i in range(1, n+1):
    x = a[i]-a[i-1]  # 利用差分数组实现区间修改,单点查询
    tr[i] += x
    if (fa := i+lowbit(i)) <= n:
        tr[fa] += tr[i]
for _ in range(m):
    a = list(map(int, input().split()))
    if len(a) == 4:
        x, y, k = a[1], a[2], a[3]
        add(x, k, tr)
        if y < n:
            add(y+1, -k, tr)  # 一定要注意正负
    else:
        print(query(a[1], tr))

区间修改,区间查询

题目传送门

P3372 【模板】线段树 1 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

首先我声明一下:这题是线段树的模板题,因为树状数组是单点修改、区间求和的数据结构。在差分数组(也是单点修改、区间求和的数据结构)的加持下树状数组可以实现区间修改、单点求和的功能。

那么怎么才能实现从区间修改、单点求和转化为我们要的区间修改区间求和呢?这需要一点数学知识。

原理

假设原数组是a,差分数组是d,那么

求a的前缀和: ∑ i = 1 n a [ i ] = ∑ i = 1 n ∑ j = 1 i d [ j ] \large \sum ^n_{i=1}a[i] = \sum^n_{i=1}\sum^i_{j=1}d[j] i=1na[i]=i=1nj=1id[j]

然后我们以d出现的次数为基础重新排列,易得d[1]出现n次,d[2]出现n-1次……

则原式= ∑ i = 1 n d [ i ] ∗ ( n − i + 1 ) = ( n + 1 ) ∑ i + 1 n d [ i ] − ∑ i = 1 n d [ i ] ∗ i \large \sum ^n_{i=1}d[i]*(n-i+1)=(n+1)\sum^n_{i+1}d[i]-\sum^n_{i=1}d[i]*i i=1nd[i](ni+1)=(n+1)i+1nd[i]i=1nd[i]i

有一点要注意,因为数据是在不断更新着,并且树状数组来存d[i]和存d[i]*i并没有直接的倍数关系,所以我们要建两棵树tree1和tree2来分别维护d[i]和d[i]*i。

ps. python因自身特性而无法通过全部数据,经检测C++可以通过,时间复杂度没有问题。

code
# 前三个函数均为树状数组模板,但是修改和求值因为这题两树的特性而略有变动
def lowbit(x):
    return x & -x


def add(x, k1, k2, t1, t2):  # 单点修改
    while x <= n:
        t1[x] += k1
        t2[x] += k2
        x += lowbit(x)


def query(x, t1, t2):  # 区间求值
    x0 = x
    ans1 = ans2 = 0
    while x:
        ans1 += t1[x]  # 分别累计两树的区间和
        ans2 += t2[x]
        x -= lowbit(x)
    return (x0+1)*ans1-ans2


n, m = map(int, input().split())
a = [0]+list(map(int, input().split()))
tr1 = [0]*(n+1)
tr2 = [0]*(n+1)
for i in range(1, n+1):
    x = a[i]-a[i-1]
    tr1[i] += x
    tr2[i] += i*x
    if (fa := i+lowbit(i)) <= n:
        tr1[fa] += tr1[i]
        tr2[fa] += tr2[i]
for _ in range(m):
    a = list(map(int, input().split()))
    x, y = a[1], a[2]
    if a[0] == 1:
        k1 = a[3]
        add(x, k1, x*k1, tr1, tr2)
        if y < n:  # 如果y已经到最后一位了,就只需要更新d[x]
            add(y+1, -k1, -(y+1)*k1, tr1, tr2)
    else:
        print(query(y, tr1, tr2)-query(x-1, tr1, tr2))  # 树状数组通过相减实现区间和的询问

最值查询

题目传送门

[P2880 USACO07JAN] Balanced Lineup G - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

code
def lowbit(x):
    return x & -x


def query1(x, y, t1, a):
    if y > x:
        s = y-lowbit(y)
        return max(query1(x, s, t1, a), t1[y]) if s >= x else max(query1(x, y-1, t1, a), a[y])
    return a[x]


def query2(x, y, t2, a):
    if y > x:
        s = y-lowbit(y)
        return min(query2(x, s, t2, a), t2[y]) if s >= x else min(query2(x, y-1, t2, a), a[y])
    return a[x]


n, q = map(int, input().split())
a = [0]+[int(input()) for _ in range(n)]
tr1 = [0]*(n+1)  # max
tr2 = [float('inf')]*(n+1)  # min
for i in range(1, n+1):  # 建树
    x = a[i]
    if x > tr1[i]:
        tr1[i] = x
    if x < tr2[i]:
        tr2[i] = x
    if (fa := i+lowbit(i)) <= n:
        if tr1[i] > tr1[fa]:
            tr1[fa] = tr1[i]
        if tr2[i] < tr2[fa]:
            tr2[fa] = tr2[i]
for _ in range(q):
    x, y = map(int, input().split())
    print(query1(x, y, tr1, a)-query2(x, y, tr2, a))

说明:因为python自身特性,部分点会TLE

O(n)建树
#  初始化
tr1 = [0]*(n+1)  # max
tr2 = [float('inf')]*(n+1)  # min
#  开始建树
for i in range(1, n+1):
    x = a[i]
    if x > tr1[i]:  # 最大值的更新
        tr1[i] = x
    if x < tr2[i]:  # 最小值的更新
        tr2[i] = x
    if (fa := i+lowbit(i)) <= n:
        if tr1[i] > tr1[fa]:
            tr1[fa] = tr1[i]
        if tr2[i] < tr2[fa]:
            tr2[fa] = tr2[i]

直接传递给相邻的父节点,实现O(n)建树

区间最值查询
def query1(x, y, t1, a):
    """max查询"""
    if y > x:
        s = y-lowbit(y)
        return max(query1(x, s, t1, a), t1[y]) if s >= x else max(query1(x, y-1, t1, a), a[y])
    return a[x]


def query2(x, y, t2, a):
    """min查询"""
    if y > x:
        s = y-lowbit(y)
        return min(query2(x, s, t2, a), t2[y]) if s >= x else min(query2(x, y-1, t2, a), a[y])
    return a[x]
原理

据上面树状数组的特征,便可知在y-lowbit(y) >= x时可以拆成tr[y]维护的部分和tr[y]之外的部分(在相等时外面的部分就只有一个),即t1[y] ,query1(x, s, t1, a)。而在y-lowbit(y) < x时将a[y]挑出剩下的部分继续处理。一直到x = y时,就只有一个a[x],即可将其取出。

逆序对

题目传送门

P1908 逆序对 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

code
def low_bit(m):
    return m & -m


n = int(input())
a = list(map(int, input().split()))
c = dict(zip(sorted(a), range(1, n+1)))  # 为实现数据的离散化,强行减少数据使用空间,所以等价地将数字替换成跟他们大小顺序对应的数字
tree = [0]*(n+1)
ans = 0
for i in range(len(a)-1, -1, -1):  # 让a逆序遍历,因为要的是顺序的效果,所以如果在加入的时候已经存在比加入的还小的,那就是要求的逆序对
    num2 = c[a[i]]
    x = num2-1
    while x:  # 区间求和(到零才退出)
        ans += tree[x]
        x -= low_bit(x)
    while num2 <= n:  # 单点更新
        tree[num2] += 1
        num2 += low_bit(num2)
print(ans)
  • 12
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陈子昂-北工大

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值