【算法】树状数组数据结构

news2024/10/6 22:26:07

文章目录

  • Part.I 预备知识
    • Chap.I 一些前提和概念
    • Chap.II lowbit 函数
  • Part.II 树状数组
    • Chap.I 树状数组的思想
    • Chap.II 树状数组的构造
  • Part.III 树状数组的应用
    • Chap.I LeetCode: 2426. 满足不等式的数对数目
      • Sec.I 题目描述与分析
      • Sec.II 代码实现
    • Chap.II LeetCode: 51. 数组中的逆序对

Part.I 预备知识

参考:
树状数组简单易懂的详解

Chap.I 一些前提和概念

  • 负数在计算机中的二进制表示
  • 前缀和:前缀和指一个数组的某下标之前的所有数组元素的和(包含其自身)。前缀和分为一维前缀和,以及二维前缀和。前缀和是一种重要的预处理,能够降低算法的时间复杂度。比如,一维前缀和的公式:sum[i] = sum[i-1] + arr[i] ; sum是前缀和数组,arr是内容数组。拥有前缀和数组后,我们可以在O(1)的时间复杂度内求出区间和。
  • 后缀和:
  • 离散化:把无限空间中有限的个体映射到有限的空间中去,以此提高算法的时空效率。通俗的说,离散化是在不改变数据相对大小的条件下,对数据进行相应的缩小。当数据只与它们之间的相对大小有关,而与具体是多少无关时,可以进行离散化。设有四个数1234567, 123456789, 12345678, 123456,我们先对它们进行排序123456<1234567<12345678<123456789 → 1<2<3<4;所以原数据就可以映射为:2, 4, 3, 1

Chap.II lowbit 函数

暂且不考虑它的用途,首先了解这个函数是怎么算的。顾名思义,lowbit这个函数的功能就是求某一个数的二进制表示中最低的一位1,举个例子,x = 6,它的二进制为110,那么lowbit(x)就返回2,因为最后一位1表示2。

怎么求lowbit呢?一般有两种方式:

  • 先消掉最后一位1(x & (x - 1)x-1并不会影响lowbit左边的1),然后原数减去消掉最后一位1后的数x - (x & (x - 1))。比如8位机中x = 24的二进制表示为00001100x - 1的二进制表示为00001011x & (x - 1)的二进制表示为00001000,所以x - (x & (x - 1))其二进制表示为00000100就是我们要的lowbit
  • 根据『计算机表示负数的方法』(2的补码),数本身与数取反的与(x & -x)。比如8位机中x = 24的二进制表示为00001100-x的二进制表示为11110100x & -x的二进制表示为00000100就是我们要的lowbit

Part.II 树状数组

树状数组是一种数据结构,为什么要构造这样的数据结构呢?这是因为它在解决某些问题方面有其独特的优势。考虑这样一个问题:现有一个长度为n的数组a[n],我们想对其进行一些操作:比如『查询』(查询某个区间的所有元素的和),『更新』(将某个元素的值更改一下)。现在我想做q次更新和q次查询,这q次更新和q次查询是穿插操作的!


如果采用原始的数据结构,每一次『更新』的时间复杂度为O(1)(因为我想改i的值的话直接a[i]=value即可),每一次『查询』的时间复杂度为O(n)(因为要求n个数的和,就要做一个长度为n的循环);


如果采用树状数组的话,每一次『查询』的时间复杂度就可以减小到O(log(n)),但是每一次『更新』的时间复杂度也是O(log(n))。为什么呢?原因暂且不表,后面会详细分析。


Chap.I 树状数组的思想

下面先上一个图(来源于 知乎@orangebird)
在这里插入图片描述
不行的话,再上一个(来源于 CSDN@FlushHip)

在这里插入图片描述

树状数组结构(因为它的结构像数,又是数组所以叫做树状数组)是依托于二进制的,看着上面的图可以很清晰地掌握它的思路,但是为甚么要这样划分呢?这就要用到上面的lowbit了,下面考虑一个长度为8的数组a,新数组叫做c,新数组由旧数组通过上图的组织方式得到。

  • 查询:比如我想求sum(1:7),首先7的二进制表示为111 ∑ i = 1 n = 7 a i = ( a 1 + a 2 + a 3 + a 4 ) + ( a 5 + a 6 ) + a 7 \sum\limits_{i=1}^{n=7}{a_i}=(a_1+a_2+a_3+a_4)+(a_5+a_6)+a_7 i=1n=7ai=(a1+a2+a3+a4)+(a5+a6)+a7,写成伪码的方式(数组下标是二进制)就是sum(001:111)=c[111]+c[110]+c[100],就是sum(1:7)=c[7]+c[7-lowbit(7)]+c[6-lowbit(6)],时间复杂度就是 ⌈ l o g 2 ( n ) ⌉ \lceil log_2(n) \rceil log2(n)⌉,即O(log n)
  • 更新:比如我想更改a[3]的值,首先3的二进制表示为0011,那么对于长度为8的数组,我需要更新c[3], c[4], c[8];换言之,我就需要更新(二进制下标)a[0011], a[0100], a[1000];也就是说,我需要更新a[3], a[3+lowbit(3)], a[4+lowbit(4)]。显然,它的时间复杂度也是O(log n)

上面就解释了树状数组的『查询』和『更新』操作为什么时间复杂度是O(log n)的原因。
之前的我有个疑问:那为什么不同时保存ac呢?如果要做更新操作,直接在a上做,时间复杂度为O(1);如果要做查询操作,在c上做,时间复杂度是O(log n)。注意,『查询』和『更新』操作是交替进行的,在a上做『更新』,只有重构c之后,做后续『查询』时才能体现出『新息』,但是重构c的时间复杂度就是O(n),这样搞的话,优化就优化了个寂寞。

Chap.II 树状数组的构造

根据上面的讨论,构造出这样一个类,其中包含的函数有:

  • lowbit:获取一个整数的lowbit
  • BIT:构造函数,根据vector<int>初始化
  • update:更新函数,第i>0个数加val
  • query:查询函数,返回前m个数的和
  • print:输出tree
class BIT {
private:
    int n;              // the length of the tree
    vector<int> tree;   // the data tree

public:
    int lowbit(int x) { return x & -x; }

    BIT(vector<int> a)
    {
        n=a.size();
        vector<int>  temp(n,0);
        tree=temp;
        for(int i=0;i<n;i++)
        {
            update(i+1,a[i]);
        }
    }

    /**
     * @brief  updata the tree array
     * @param[in] i         the index, >=1
     * @param[in] val       the value of the update, =now-origin
     * @return              none
     */
    void update(int i, int val)
    {
        for(;i<=n;tree[i-1]+=val,i+=lowbit(i));
    }

    /**
     * @brief  query the summary of the first m terms
     * @param[in] m         the index, >=1
     * @param[out] sum      the sum
     * @return              int
     */
    int query(int m)
    {
        int sum=0;
        for(;m>0;sum+=tree[m-1],m-=lowbit(m));
        return sum;
    }

    void print()
    {
        for (int i = 0; i < n; cout << tree[i] << "    ", i++);
        cout << endl;
    }
};

调用示例:

int main()
{
    int test[7]={1,2,3,4,5,6,7};
    vector<int> origin(test, test + 7);
    BIT bt(origin);
    bt.print();                 // 打印 tree 的内容
    cout<<bt.query(5)<<endl;    // 输出前5项和
    bt.update(3,6);             // 第3项加6
    bt.print();                 // 打印更新后的 tree 的内容
    cout<<bt.query(5)<<endl;    // 输出更新后的前5项和
    getchar();
    return 0;
}
// ----------------- output ------------------
1    3    3    10    5    11    7
15
1    3    9    16    5    11    7
21

上面的代码可以免费下载:下载地址

Part.III 树状数组的应用

  • LeetCode: 2426. 满足不等式的数对数目
  • 剑指 Offer 51. 数组中的逆序对

Chap.I LeetCode: 2426. 满足不等式的数对数目

没错,就是因为刷题的时候遇到这个题2426,所以才有这篇笔记的,最后终于露出了獠牙(RUA!!)。


Sec.I 题目描述与分析

首先,题目描述为:

给你两个下标从 0 开始的整数数组 nums1nums2 ,两个数组的大小都为 n ,同时给你一个整数 diff,统计满足以下条件的数对 (i, j)

  • 0 <= i < j <= n - 1
  • nums1[i] - nums1[j] <= nums2[i] - nums2[j] + diff

请你返回满足条件的 数对数目 。

解题视频:bilibili@灵茶山艾府


题目分析(基于python):

  • 首先进行移项: nums1[i] - nums2[i] <= nums1[j] - nums2[j] + diff,令nums[i] = nums1[i] - nums2[i] ,我们只需找到当0 <= i < j <= n - 1 时满足nums[i] <= nums[j] + diff的所有数据对(i, j)即可。
  • 因为nums[i]中不免会存在数值相同的元素,因此我们可以将其用set进行唯一化,然后进行排序得到b
  • 离散化:构造一个树状数组bt(所有元素初始化为0),树状数组的长度等于num中互异元素的个数len(set(nums))(相当于将nums分为这么多档次【不论数据大小,只关心数据的相对大小,这就是离散化】,树状数组的每一个元素存储的是在这个档次的数据个数)。树状数组有两个主要函数,一个是add(x)(将索引为x的值加一,这里值的是上面的A,但是树状数组存储的是C,所以要变的不只一个元素),另一个是query(x)(求索引小于x的所有数据的和)。
  • 我们用一个指针i遍历nums,在遍历的过程中并填充树状数组bt,树状数组存储的是x=nums[i]左边每一个『档次』元素的个数,我们先用index=bisect_right(b, x + diff)b中找到元素大于等于x+diff的索引最小值,然后用query(index)统计nums[i]左边元素大于等于x+diff的数目和(也就是找到满足nums[m] <= nums[i] + diffm<i的所有的m的个数和)
  • 然后用index2=bisect_left(b, x)得到b中元素小于等于x的所有元素的索引最大值(也就是找到x所对应的『档次』索引),然后用add(index2)函数将其加入树状数组中去,为进入下一次query(i+1)做准备。
  • 对所有的query(index)求和就得到我们所需

注意,这道题虽然使用了树状数组,但是数组存储的并不是元素值,而是元素个数。另外,树状数组并不是一下就构造好的,而是在遍历查询添加元素的过程中逐步建立的。知道这两点,看着视频讲解应该就很好理解了。笔者已经尝试尽可能地将这个思想整理出来,但是回过头看还是有点拗口 orz


Sec.II 代码实现

下面是C++代码实现

class BIT {
private:
    int length=0;
    vector<int> tree;
public:
    BIT(int n)
    {
        length=n;
        vector<int> temp(n,0);
        tree=temp;
    }
    int lowbit(int x){ return x & -x; }
    void add(int i)
    {   // i=index+1,>=1
        while(i<=length){ tree[i-1]++; i=i+lowbit(i); }
    }
    int query(int i)
    {   // i=index+1,>=1
        int sum=0;
        while(i>0){
            sum+=tree[i-1];
            i-=lowbit(i);
        }
        return sum;
    }
};

class Solution {
public:
    long long numberOfPairs(vector<int>& nums1, vector<int>& nums2, int diff) {
        int n=nums1.size();
        vector<int> nums(n,0);
        for(int i=0;i<n;i++) { nums[i]=nums1[i]-nums2[i]; }
        vector<int> b(nums);
        sort(b.begin(),b.end());
        b.erase(unique(b.begin(),b.end()),b.end());
        BIT bt(b.size());
        long ans=0;
        for(int i=0;i<n;i++)
        {
            ans+=bt.query(upper_bound(b.begin(),b.end(),nums[i]+diff)-b.begin());
            bt.add(lower_bound(b.begin(),b.end(),nums[i])-b.begin()+1);
        }
        return ans;
    }
};

值得注意的点:

  • upper_bound(b.begin(),b.end(),val)函数的作用是查找容器b(数据已经有序)中元素值大于等于val的最小索引迭代器(可以理解为指针),*upper_bound(xx)返回索引的元素值,upper_bound(xx)-b.begin()是索引值
  • upper_bound(b.begin(),b.end(),val)函数的作用是查找容器b(数据已经有序)中元素值小于val的最大索引迭代器(可以理解为指针),其他的使用同upper_bound

下面是python的代码实现:

class BIT:
    def __init__(self,n: int):
        self.length=n
        self.tree=[0]*n
    def add(self, i: int):
        while(i<=self.length):
            self.tree[i-1]+=1
            i+=(i & -i)
    def query(self, i: int) -> int:
        sum=0
        while(i>0):
            sum+=self.tree[i-1]
            i-=(i & -i)
        return sum
 

class Solution:
    def numberOfPairs(self, nums1: List[int], nums2: List[int], diff: int) -> int:
        n=len(nums1)
        nums=[0]*n
        for i in range(n):
            nums[i]=nums1[i]-nums2[i]
        b=sorted(set(nums))
        bt=BIT(len(b))
        ans=0
        for i in range(n):
            ans+=bt.query(bisect_right(b,nums[i]+diff))
            bt.add(bisect_left(b,nums[i])+1)
        return ans

Chap.II LeetCode: 51. 数组中的逆序对

这道题应该是比较经典的一道题,毕竟都已经被『剑指 Offer』录入了。它实际上和上面的一道题很像,比那道题简单。因此下面就不分析了,只贴一个解决方案


下面是基于python的代码:

class Solution:
    def reversePairs(self, nums: List[int]) -> int:
        b = sorted(set(nums))
        ans = 0
        n = len(b)
        bt = BIT(n)
        for x in nums:
            temp=n-bisect_left(b, x)
            ans += bt.query(temp-1)
            bt.add(temp)
        return ans

class BIT:
    def __init__(self,n: int):
        self.length=n
        self.tree=[0]*n
    def add(self, i: int):
        while(i<=self.length):
            self.tree[i-1]+=1
            i+=(i & -i)
    def query(self, i: int) -> int:
        sum=0
        while(i>0):
            sum+=self.tree[i-1]
            i-=(i & -i)
        return sum

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/4817.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

计算机网络-网络层(ARP协议,DHCP协议,ICMP协议)

文章目录1. ARP协议2. DHCP协议3. ICMP协议1. ARP协议 首先数据在从网络层向下传递到数据链路层&#xff0c;在数据链路层中&#xff0c;要给报文封装源MAC地址和目的MAC地址。 其中获取目的MAC地址就是通过ARP协议 首先&#xff1a;每台主机都有一个ARP高速缓存&#xff08…

【VC++】字符串详解窗口第一个windows程序

注&#xff1a;最后有面试挑战&#xff0c;看看自己掌握了吗 文章目录系统调用顺序对比怎样避免确实动态链接库基本知识类型列表指针类型匈牙利标记法字符串详解Unicode 和 ANSI 函数TCHARs窗口WinMain我的博客即将同步至腾讯云开发者社区&#xff0c;邀请大家一同入驻&#xf…

微信小程序开发(超详细保姆式教程)

介绍&#xff1a; 微信里面app&#xff0c;16年推出 竞品&#xff1a;支付宝小程序&#xff0c;钉钉&#xff0c;美团&#xff0c;头条&#xff0c;抖音qq小程序 优点 1&#xff0c;在微信里面自由分享&#xff0c;2&#xff0c;不用下载app&#xff0c;3,能快速的开发&#xf…

【MySQL】如何把Windows上的MySQL数据库迁移到Linux服务器上

目录1. 前言2. 物理备份与逻辑备份3. mysqldump实现逻辑备份4. 逻辑恢复1. 前言 最近在学黑马的《瑞吉外卖》&#xff0c;前期的基础版本一致在 Windows 电脑上开发&#xff0c;包括 MySQL 数据库也是安装在 Windows 电脑上。最近才学到优化篇&#xff0c;安装了 Linux 虚拟机…

【成为红帽工程师】第二天 ssh远程连接服务器

目录 一、远程连接服务器 二、连接加密技术 三、ssh远程连接服务 四、sftp用法介绍 五、相关实验 一、远程连接服务器 &#xff08;一&#xff09;什么是远程连接服务器 远程连接服务器通过文字或图形接口方式来远程登录系统&#xff0c;让你在远程终端前登录linux主机…

2022年最新山东交安安全员模拟真题及答案

百分百题库提供交安安全员考试试题、交安安全员考试真题、交安安全员证考试题库等&#xff0c;提供在线做题刷题&#xff0c;在线模拟考试&#xff0c;助你考试轻松过关。 43.危险性较大工程专项施工方案需要论证的&#xff0c;应当由建设单位组织召开专家论证会。 答案&#…

计算机毕业设计SSM财务管理系统【附源码数据库】

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

计算机毕业设计SSM城市智能公交系统【附源码数据库】

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

MyBatis(4)---多表查询

多表查询: 一对一:一篇博客对应着一个作者 一对多:一个作者对应着多篇博客 ResultMap和ResultType的区别: 1)字段映射不同:resultType适用于数据库字段名和实体类的名字是相同的&#xff0c;但是假设实体类的名字叫做username&#xff0c;但是数据库的名字是name&#xff0c;这…

MyBatis(3)

我们在进行指定ID进行删除的时候还可以加上一个属性:表示要传递的参数的类型是啥 <delete id"Delete" parameterType"java.lang.Integer">delete from user where userID#{userID}</delete> 我们现在先实现一个场景----我们来进行查询一下User…

【毕业设计】大数据共享单车数据分析系统 - python

文章目录0 前言1 项目背景2 项目分析思维导图3 项目分析具体步骤3.1 读取数据3.2 数据分析3.1.1 数据预处理——每日使用量分析3.1.2 连续7天的单日使用分析结论3.1.3 数据预处理——每日不同时间段的使用量分析3.1.4 每日不同时间段使用量分析结论3.1.5 数据预处理——骑行距离…

【C++】智能指针

一、资源的管理 RAII:Resource Acquisition Is Initialization的简称&#xff0c;其翻译过来就是“资源获取即初始化”&#xff0c;即在构造函数中申请分配资源&#xff0c;在析构函数中释放资源&#xff0c;它是C语言中的一种管理资源、避免泄漏的良好方法。 C语言的机制保证…

python快速实现简易超级玛丽小游戏

《超级玛丽》是一款超级马里奥全明星的同人作品&#xff0c;也是任天堂公司出品的著名横版游戏。 《超级马里奥》是一款经典的像素冒险过关游戏。最早在红白机上推出&#xff0c;有多款后续作品&#xff0c;迄今多个版本合共销量已突破4000万套。其中的主角马里奥、路易、碧琪…

[附源码]计算机毕业设计JAVAjsp闲置物品线上交易系统

[附源码]计算机毕业设计JAVAjsp闲置物品线上交易系统 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM …

tensor和numpy相互转换

tensor转成numpy b a.numpy()import torcha torch.arange(5) b a.numpy() print(a) print(type(a)) print(b) print(type(b))numpy转成tensor b torch.tensor(a)import torch import numpy as npa np.ones(5) b torch.tensor(a) print(a) print(type(a)) print(b) prin…

Spring Cloud框架(原生Hoxton版本与Spring Cloud Alibaba)基础入门篇 ---- 搭建环境

springcloud官方文档&#xff08;Hoxton SR5&#xff09;&#xff1a;https://cloud.spring.io/spring-cloud-static/Hoxton.SR5/reference/htmlsingle/ springcloud中文文档&#xff1a;https://www.springcloud.cc/ springcloud中国社区文档&#xff1a;http://docs.springcl…

【C++】栈~~(很详细哦)

在前几天&#xff0c;我们刚一起学过顺序表&#xff0c;链表&#xff08;无头单向不循环&#xff0c;有头双向循环&#xff09;&#xff0c;这两种都属于线性表因为是一系列存储的。而以后的哈希表则是散列表 今天我们看一下栈 目录 1.栈的介绍 2.实现 3.题目 1.栈的介绍 …

mindspore::dataset::GetAffineTransform的输出与cv2的输出不同

在使用C进行推理时用到了函数mindspore::dataset::GetAffineTransform&#xff0c;但是输入相同的数据后&#xff0c;与Python的cv2中的同名函数cv2.getAffineTransform所输出的结果不同。 C Ascend310端测试核心代码 #include <iostream> #include <vector>#…

synchronized 关键字背后的锁升级流程

文章目录前言一、基本特点二、加锁过程总结前言 博主个人社区&#xff1a;开发与算法学习社区 博主个人主页&#xff1a;Killing Vibe的博客 欢迎大家加入&#xff0c;一起交流学习~~ 一、基本特点 结合多线程锁的策略, 我们就可以总结出, Synchronized 具有以下特性(只考虑 J…

基于51单片机的智能台灯设计

一.硬件方案 本文介绍了一种基于PWM调光的智能台灯设计。把单片机技术和PWM调光技术结合起来实现台灯光强的调节。即在不改变PWM方波周期的前提下&#xff0c;利用单片机控制PWM的占空比&#xff0c;从而来改变电压的大小实现灯光亮度的调节。 当人体在台灯的范围内且环…