树状数组的python教程_python树状数组-程序员宅基地

技术标签: 算法  python  数据结构  


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

树状数组:

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

lowbit(最低位)

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

在这里插入图片描述

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

特征

  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)
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/m0_73146523/article/details/129041224

智能推荐

CentOS8解决“Failed to download metadata for repo ‘appstream‘”错误_centos linux 8 - appstream 63 b/s | 38 b 00:00 err-程序员宅基地

文章浏览阅读3.5w次,点赞64次,收藏87次。在CentOS8上执行下面命令时报错yum install epel-releaseCentOS Linux 8 - AppStream 23 B/s | 38 B 00:01 Error: Failed to download metadata for repo 'appstream': Cannot prepare internal mirrorlist: No URLs in mirrorlist原因在2022年1..._centos linux 8 - appstream 63 b/s | 38 b 00:00 error: failed to download met

x86指令格式-程序员宅基地

文章浏览阅读774次。学习于逆向工程核心原理IA-32指令章节格式x86指令格式指令前缀出现特定操作码时用作补充说明,图中的冒号前的64就是指令前缀操作码实际的指令,如图中的FF、89、80都是操作码ModR/M辅助说明操作码的操作数(操作数的个数、种类[寄存器、内存地址、常量]),图中的SIB..._ebxmodr

北京的大学计算机专业厉害的,计算机专业最强的大学有哪些?哪个大学计算机专业全国第一?附名单...-程序员宅基地

文章浏览阅读735次。选择科目测一测我能上哪些大学选择科目领取你的专属报告>选择省份关闭请选择科目确定v>计算机类专业一直位于热门专业行列,主要是因为计算机行业的工资待遇是很多行业都不能相比的,所以吸引了众多学子踊跃报考该专业。那么计算机专业最强的大学有哪些?哪个大学的计算机专业全国第一?本期小编将为大家解答相关问题。一、计算机专业最强的大学有哪些?根据教育部全国第四轮学科评估结果进行参考,计算机专业最强的..._北京计算机最好学校

vue3 excel 导出功能_vue3导出excel表格-程序员宅基地

文章浏览阅读1.3k次,点赞2次,收藏3次。vue3 excel导出_vue3导出excel表格

课后作业:创建一个程序将摄氏温度值(C)转换为华氏温度值(F)_用c#编写程序实现输入摄氏温度c的值输出对应华氏温度f的值-程序员宅基地

文章浏览阅读1.2w次,点赞4次,收藏11次。// Copyright (c) 2014软件技术2班_用c#编写程序实现输入摄氏温度c的值输出对应华氏温度f的值

ios文件连接服务器未能完成,iOS Socket-IO https 不能连接 connect error-程序员宅基地

文章浏览阅读2.4k次。LOG SocketManager: Tried connecting socket when engine isn't open. ConnectingLOG SocketManager: Adding engineLOG SocketIOClient{/}: Handling event: statusChange with data: [connecting, 2]LOG SocketMan..._苹果文件共享socket未连接

随便推点

vue2-happyfri 开源项目分析-程序员宅基地

文章浏览阅读1.9k次,点赞3次,收藏7次。Vue.js是一套构建用户界面的渐进式框架。自诞生之日起就受到了广泛的关注,和广大的前端开发者一样,笔者着迷于其简洁友好的语法、强大的单文件组件以及丰富的生态系统。通过这样一个简单但全面的开源项目分析,笔者希望可以入门这个框架且做一定的深入了解。_vue2-happyfri

黑马程序员_01 基础小结-程序员宅基地

文章浏览阅读488次。最常用的编程元素一: 常量与变量 1, 常量:就是固定不变的量,一旦被定义,他的值就不会被改变 。 往往用final修饰。 2,变量:变量是利用声明的方式,将内存中的某个块保留下来。 (1)说明 1:要求在使用一个变量之前要对变量的类型加以声明。 2:java中一个变量的声明就是一条完整的语句

maya_[maya学习笔记(1)] 视窗的基本操作-程序员宅基地

文章浏览阅读589次。三点照明法_[maya学习笔记(1)] 视窗的基本操作

C++ vector变量等导致内存泄露问题的解决方法_c++ vector是否会造成内存泄漏-程序员宅基地

文章浏览阅读1.2w次,点赞2次,收藏7次。之前在做一个音频特征提取的批量处理程序,老是出现内存泄露问题,用Visual Leak Detector(VLD)工具做了下检测,检测出了一些问题,解决后还是会有问题。之后继续排查,因为我的代码中,大量的音频相关处理的数据都存成了vector变量,推测是不是vector变量的析构问题,上网查了些资料,现写出解决过程:1、关于Visual Leak Detector的配置与使用主要也_c++ vector是否会造成内存泄漏

2023最新SSM计算机毕业设计选题大全(附源码+LW)之java宠物商店信息展示与服务订购系统7q5ic-程序员宅基地

文章浏览阅读77次。现在流行的 是Spring Boot 做的 SSM框架:SpringMVC + Spring + MyBatisVUE + Spring Boot主要还是结合自己的实际水平来。如果你真的在选题这一方面完全没思路的话,下面有一些题目可以供你参考下,是之前上半年完成的部分的毕设程序,具体获取见文末。面对老师五花八门的设计要求,首先自己要明确好自己的题目方向,并且与老师多多沟通,用什么编程语言,使用到什么数据库,确定好了,在开始着手毕业设计。ssm基于ssm的校园失物招领平台h5xpq。

python的flask实现接口_python+flask:实现POST接口功能-程序员宅基地

文章浏览阅读314次。1、首先需要安装python和flask,这个是必须的嘛。2、我们这里实现的是一个POST功能的简单接口。from flask import Flask, request, jsonifyimport jsonapp = Flask(__name__)app.debug = [email protected]('/add/student/',methods=['post'])def add_stu():..._python flask post 接口 app.route