Binary Indexed Tree

news2024/11/24 16:39:14

refs:

裸题之灵神题解:
<https://leetcode.cn/problems/range-sum-query-mutable/solutions/2524481/dai-ni-fa-ming-shu-zhuang-shu-zu-fu-shu-lyfll>

灵神的视频讲解:
<https://www.bilibili.com/video/BV14r421W7oR>

1. 用来解决什么问题

区间和

这个问题原先的做法是维护前缀和,查找O(1),但问题是前缀和一旦确定,数组就不能修改了,改了之后前缀和还得花O(n)的时间去改。而树状数组可以做到查找和修改都是O(logn)的,超过前缀和的查找O(1),修改O(n)。

2. 思想

2.1. 分块

先介绍一个分块的前缀和的思想,比如数组:

[ 1 1 2 3 5 6]

对他进行长度为B=2的分块:

[ 1 1 | 2 3 | 5 6 ]

然后维护每个分块的区间和。那么如果查询区间[3:6]的和,就是后两个分块,这样查询就是O(n/B)。(你可能会说如果不是整数个区间呢?那多出来的部分单算,比如[2:6],索引2上的1单独算就行了,复杂度O(n/B+B),由于B大概率比n小,那就是O(n/B))

修改的话,维护对应分块的前缀和,O(B)。

这样查找O(n/B),修改O(B)。那么怎么均衡以下,这个东西很明显是耐克函数啊,取B = sqrt(n)即可。

那有没有更优的呢?比sqrt(n)更好的,是logn,前者是幂级别的,后面是指数级别递减的。树状数组就可以做到这一点。

2.2. 树状数组

树状数组指把一个数组按照树形规则划分为多个子数组。在修改时,只需要修改相关的关键区间(通过一个顶级的位运算trick),在查询时,只需要计算logn个关键区间的和即可。

怎么划分树状数组

我们先来说怎么划分树状数组。首先要明确的是,树状数组的索引从1开始,也就是[1:n]

对于[1:n]中间的任意一个索引i,划分[1:i]的规则如下:

  1. 先把i转为二进制串,例如13D=1101B

  2. 从右往左(从小到大),对于二进制串的每一位,如果是1,那么计算当前二进制串的值。例如1101的从右往左的第3位,为1,那么0100=4D。然后划分出一个长度为4的区间。

    对于13来说,划分如下:

    1101B
    
    0001 = 1D [13,13]
    
    0000 不是1 跳过
    
    0100 = 4D [9,12]
    
    1000 = 8D [1,8]
    

    这样就把[1,13]划分成了三个区间,[1,8] [9,12] [13,13]。大家可以看上面那张图,看看是不是c[13]单指一个13,c[12]指[9,12],c[8]指[1,8]

而且这种划分有个性质,对于索引i,拆分所有的满足1≤j≤i的[1,j],将恰好得到i个不同的区间。这个想法类似于DP。例如你拆[1,13],那么递归到底部必然拆了[1,1] ⇒ [1,1],[1,2] ⇒ [2,2],[1,3] ⇒ [1,2] + [3,3]…

怎么修改树状数组

举个例子啊,如果你修改索引为10的元素,那么哪些相关的区间会变动呢?

查看上图,你会发现,比10小的都不会受影响,因为一定会有关键区间“管着”前面的元素。但是后面的关键区间也不是都得修改。比如11,有c[11]管着。

需要修改的是10,12,16。12,16都是祖先节点。那么怎么找到12和16呢?

我们来找找规律:

10D = 01010B
12D = 01100B
16D = 10000B

那么01010B和01100B差了2D,恰好是01010B的最低位的1的值,也就是00010B。

这有个专门的说法,叫做lowbit,lowbit(i)为i的二进制下最低位1的值。i为正整数

而01100B和10000B差了4D,恰好是01100B的最低位的1的值,也就是00100B。

这是个树状数组很重要的性质,就是后向的相关的关键区间就是当前区间加上lowbit的值,一直迭代到数组越界为止。

怎么查询树状数组

由于我们是按照二进制串对数组进行的划分,因此对于一个区间,划分的数量是log2n的。比如13,划分了3个,就是下取整log2(13)。所以我们至多查询logn个区间。

那么前向相关区间都在哪呢?还是lowbit,修改看后向加lowbit,那查询看前向,不就是减去lowbit吗?

比如你要查[1,7]的,你直接一看7的lowbit,1,那就算上c[7]=[7,7],然后跳到7-1=6去。你再一看6的lowbit,2,那就算上c[6]=[5,6],然后跳到6-2=4去。再一看4的lowbit,4,那就算上c[4] = [1,4],然后跳到4-4=0去,到此跳出了树状数组,查询结束,返回和。

那么任意区间也是很简单的,整体的减左边的即可。比如你查[3,7],那就是[1,7]-[1,3]就可以了。两次logn,还是logn。

3. 搞点题⑧

1. LC 307 区域和检索-数组可修改

树状数组裸题。就是板子。

对于初始化,我们要先初始化所有的关键区间,就得调update。那么之前说了,修改树状数组查询lowbit,要用一个很nb的位运算trick。比如12D = 1100B,lowbit=4。咋查?

设i=1100B

~i = 0011B
~i+1 = 0100B

i & (~i+1) = 1100 & 0100 = 0100

也就是说每个数和它的取反+1与一下就是lowbit。

那么取反+1是什么?补码嘛,那就是-i啊。所以就是i&(-i),就能算出来lowbit的值。

public class NumArray {
    private int[] nums; // 保存各数字
    private int[] tree; // 树状数组保存前缀和

    public NumArray(int[] nums) {
        int n = nums.length;
        this.nums = new int[n]; // 使 update 中算出的 delta = nums[i]
        tree = new int[n + 1];
        for (int i = 0; i < n; i++) {
            update(i, nums[i]);
        }
    }

    public void update(int index, int val) {
        int delta = val - nums[index];
        nums[index] = val;
        for (int i = index + 1; i < tree.length; i += i & -i) { // i+= i & -i:增加lowbit,找到相关的关键区间
            tree[i] += delta;
        }
    }

    private int prefixSum(int i) {
        int s = 0;
        // 举个例子,如果是[1,13] 相当于求 [13,13] + [9,12] + [1,8]的区间和
        for (; i > 0; i &= i - 1) { // i -= i & -i 的另一种写法
            s += tree[i];
        }
        return s;
    }

    public int sumRange(int left, int right) {
        return prefixSum(right + 1) - prefixSum(left);
    }
}

这样初始化是n次logn,就是O(nlogn),也是整个树状数组的瓶颈。怎么提升呢?

其实我们可以在初始化的时候就采用update的那种迭代更新后向相关的关键区间的方式。比如说你初始化好了c[6],那么lowbit=2,这样你后向的相关区间就是6+2=8。所以你可以直接在这次迭代的时候先把c[6]累加到c[8]上去,这样就变成O(n),有点像DP的刷表。

public class NumArray {
    private int[] nums;
    private int[] tree;

    public NumArray(int[] nums) {
        int n = nums.length;
        this.nums = nums;
        tree = new int[n + 1];
        for (int i = 1; i <= n; i++) {
            tree[i] += nums[i - 1];
            int nxt = i + (i & -i); // 下一个关键区间的右端点
            if (nxt <= n) {
                tree[nxt] += tree[i];
            }
        }
    }

    public void update(int index, int val) {
        int delta = val - nums[index];
        nums[index] = val;
        for (int i = index + 1; i < tree.length; i += i & -i) {
            tree[i] += delta;
        }
    }

    private int prefixSum(int i) {
        int s = 0;
        for (; i > 0; i &= i - 1) { // i -= i & -i 的另一种写法
            s += tree[i];
        }
        return s;
    }

    public int sumRange(int left, int right) {
        return prefixSum(right + 1) - prefixSum(left);
    }
}

2. LC 3072 将元素分配到两个数组中Ⅱ

周赛387T4。这题我本来是手搓了一个sortedList然后二分做的。但其实可能被极限用例构造O(n²)从而T掉,这个要看官方是否要rej了。

这题真正的做法是树状数组+名次排序+哈希表。

首先我们先把nums去重排序,然后给每个不同的数标个rank放到哈希表。这个名次是数越小名次越小。例如:

nums: 5 14 3 1 2

排序: 1 2 3 5 14

rank: 1 2 3 4 5

随后我们定义在树状数组中,nums[i]代表名次为i+1的数字出现的次数,那么比名次为i+1的数字大的数的出现次数就是sumRange(i,nums.length-1)。这个查询是O(logn)的,随后把数字放入arr1或arr2后的更新树状数组,也是O(logn)的。总共n个数,就是O(nlogn)的。树状数组初始化我用的默认初始化O(1),排序O(nlogn)。所以时间总体上O(nlogn),空间O(n)。

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;

class BIT{
    public final int[] nums;
    public final int[] tree;

    public BIT(int n){
        this.nums = new int[n];
        this.tree = new int[n+1];
    }

    public BIT(int[] init){
        int n = init.length;

        this.nums = init;
        this.tree = new int[n+1];

        for(int i=1;i<=n;i++){
            tree[i]+=init[i-1];

            int next = i+(i&(-i));
            if(next<=n){
                tree[next] += tree[i];
            }
        }
    }

    public void update(int index,int val){
        int diff = val-nums[index];

        nums[index] = val;
        for(int i=index+1;i<tree.length;i+=(i&(-i))){
            tree[i]+=diff;
        }
    }

    private int prefixSum(int index){
        int sum = 0;

        for(int i=index;i>0;i-=(i&(-i))){
            sum+=tree[i];
        }

        return sum;
    }

    public int sumRanges(int l,int r){
        return prefixSum(r+1)-prefixSum(l);
    }
}

class Solution {
    public int[] resultArray(int[] nums) {
        HashMap<Integer, Integer> m = new HashMap<>();

        int[] tmp = nums.clone();
        // 排序去重计算rank
        Arrays.sort(nums);
        int rank = 1;
        for(int i=0;i<nums.length;){
            int j = i;
            while(j<nums.length&&nums[j]==nums[i]){
                j++;
            }
            m.put(nums[i],rank++);
            i = j;
        }

        BIT BT1 = new BIT(rank-1);
        BIT BT2 = new BIT(rank-1);

        nums = tmp;
        int rk;
        rk = m.get(nums[0]);
        BT1.update(rk-1,BT1.nums[rk-1]+1);
        rk = m.get(nums[1]);
        BT2.update(rk-1,BT2.nums[rk-1]+1);

        ArrayList<Integer> arr1 = new ArrayList<>();
        arr1.add(nums[0]);
        ArrayList<Integer> arr2 = new ArrayList<>();
        arr2.add(nums[1]);

        int gc1,gc2;
        for(int i=2;i<nums.length;i++){
            rk = m.get(nums[i]);

            gc1 = BT1.sumRanges(rk,rank-1-1);
            gc2 = BT2.sumRanges(rk,rank-1-1);

            if(gc1>gc2 || gc1==gc2 && arr1.size()<=arr2.size()){
                arr1.add(nums[i]);
                BT1.update(rk-1,BT1.nums[rk-1]+1);
            }else{
                arr2.add(nums[i]);
                BT2.update(rk-1,BT2.nums[rk-1]+1);
            }
        }

        int[] result = new int[nums.length];
        for (int i = 0; i < arr1.size(); i++) {
            result[i] = arr1.get(i);
        }
        for (int i = 0; i < arr2.size(); i++) {
            result[i+arr1.size()] = arr2.get(i);
        }

        return result;
    }
}

喜欢我手搓树状数组嘛。

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

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

相关文章

【Vue3】Hooks:一种全新的组件逻辑组织方式

&#x1f497;&#x1f497;&#x1f497;欢迎来到我的博客&#xff0c;你将找到有关如何使用技术解决问题的文章&#xff0c;也会找到某个技术的学习路线。无论你是何种职业&#xff0c;我都希望我的博客对你有所帮助。最后不要忘记订阅我的博客以获取最新文章&#xff0c;也欢…

【论文阅读】Mamba:选择状态空间模型的线性时间序列建模(二)

文章目录 3.4 一个简化的SSM结构3.5 选择机制的性质3.5.1 和门控机制的联系3.5.2 选择机制的解释 3.6 额外的模型细节A 讨论&#xff1a;选择机制C 选择SSM的机制 Mamba论文 第一部分 Mamba:选择状态空间模型的线性时间序列建模(一) 3.4 一个简化的SSM结构 如同结构SSM&#…

【MybatisPlus】BaseMapper详解,举例说明

一、BaseMapper 简介 MyBatis-Plus 的核心类 BaseMapper 主要是用于提供基本的 CRUD&#xff08;创建、读取、更新、删除&#xff09;操作的接口定义。它是 MyBatis-Plus 框架中的一个重要组成部分&#xff0c;可以大大简化基于 MyBatis 的数据访问层代码的编写。 BaseMapper…

0基础跨考408|一战上岸复盘及经验分享

基础阶段‼️ 王道的四本书的选择题部分要都做完、订正完。 王道的四门视频课要一轮刷完&#xff08;或者题主在B站看了其他的老师&#xff0c;这其实也是算一轮的&#xff0c;只要题主是认真学习了的&#xff0c;题主说自己不知道看什么课&#xff0c;王道就好了&#xff09;…

成功的SOHO在接待方面值得我们思考的地方

有个客户离春节放假之前的一个月就说要来访工厂&#xff0c;后面直到放假的最后一天也没等到他&#xff0c;中间商说他去了另外一个省&#xff0c;忙别的生意去了。 刚接触他的时候&#xff0c;是一位中国代理商联系我们工厂&#xff0c;做了不少设计和项目&#xff0c;期间修…

GEE入门篇|图像处理(三):阈值处理、掩膜和重新映射图像

阈值处理、掩膜和重新映射图像 本章前一节讨论了如何使用波段运算来操作图像&#xff0c; 这些方法通过组合图像内的波段来创建新的连续值。 本期内容使用逻辑运算符对波段或索引值进行分类&#xff0c;以创建分类图像。 1.实现阈值 实现阈值使用数字&#xff08;阈值&#xf…

Java实现读取转码写入ES构建检索PDF等文档全栈流程

背景 之前已简单使用ES及Kibana和在线转Base64工具实现了检索文档的demo&#xff0c;并已实现WebHook的搭建和触发流程接口。 实现读取本地文件入库ES 总体思路&#xff1a;基于前面已经搭建的WebHook触发流程&#xff0c;接收到push更新消息之后&#xff0c;使用本地的git工…

【QT】布局介绍

布局 水平布局垂直布局网格布局 widget的应用 将对应的按钮&#xff0c;label放入到widget中 水平弹簧和垂直弹簧 使用弹簧来对他们布局 设置弹簧属性&#xff1a; 最后结果——页面中的内容和随页面的缩小和方法自适应。 水平布局和垂直布局只能针对一行一列 将用户密码放入…

第七篇 - 人工智能与机器学习技术VS量测(Measurement)- 我为什么要翻译介绍美国人工智能科技巨头IAB公司 - 它是如何赋能数字化营销生态的?

IAB平台&#xff0c;使命和功能 IAB成立于1996年&#xff0c;总部位于纽约市。 作为美国的人工智能科技巨头社会媒体和营销专业平台公司&#xff0c;互动广告局&#xff08;IAB- the Interactive Advertising Bureau&#xff09;自1996年成立以来&#xff0c;先后为700多家媒…

2024年字节跳动+京东+美团面试总结,程序员经验分享

现在的IT行业竞争压力越来越大&#xff0c;尤其是Android开发行业。而很多Android程序员却每天都在重复CRUD,原地徘徊&#xff01; 今年年初&#xff0c;你就想改变现状&#xff0c;于是在网上刷了大量面试题&#xff0c;强行记下之后&#xff0c;开始参加面试&#xff01;但是…

AI日报:这种病毒从生成式AI工具中窃取您的数据

文章目录 人工智能计算机病毒蠕虫像细菌一样传播病毒测试 人工智能计算机病毒 一组研究人员创造了一种能够利用生成人工智能系统的计算机病毒&#xff0c;包括Gemini Pro和GPT-4驱动的ChatGPT版本。 Morris II是一种蠕虫&#xff0c;它操纵生成的人工智能模型来执行恶意任务&…

AndroidStudio连不上adb报错ADB Connection Error

之前笔者一直通过AndroidStudio来看日志&#xff0c;也一直用的一套自己的SDK&#xff0c;用了好几年了。 但是突然有一天&#xff0c;AndroidStudio启动后就弹出警告窗&#xff1a;ADB Connection Error&#xff0c;如下&#xff1a; 在Event Log面板还持续性的输出&#x…

前端工具网站合集(持续更新)

综合类网站 那些免费的砖 统计推荐免费工具网站 那些免费的砖 - 优雅地白嫖各种免费资源 (thosefree.com)https://www.thosefree.com/ CSS样式网站 毒蘑菇-配色 CSS 配色&#xff0c;阴影网站 一个好用的配色网站! 毒蘑菇 - 配色 (dumogu.top)https://color.dumogu.top/ …

工艺美术设计VR仿真教学软件为教师提供更丰富的教学资源

随着科技的飞速发展&#xff0c;我们的生活正在经历着前所未有的变革。其中&#xff0c;虚拟现实(VR)技术的出现&#xff0c;不仅改变了我们的娱乐方式&#xff0c;更在教育领域中开辟出一片新的天地。今天&#xff0c;我们就来探讨一下VR美术绘画教学平台带来的价值。 首先&am…

day14_用户前台项目环境搭建(首页接口开发,分类接口开发,网关服务搭建,Redis缓存,Spring Cache)

文章目录 1 尚品甄选H5介绍1.1 业务功能介绍1.2 系统架构1.3 前端H5开发说明 2 搭建项目环境2.1 项目结构说明2.2 模块依赖说明2.3 环境说明2.4 项目模块创建2.4.1 spzx-parent2.4.2 spzx-service2.4.43 service-product 2.5 导入接口文档 3 首页接口开发3.1 需求分析3.3 接口开…

[最佳实践] Windows上构建一个和Linux类似的Terminal

感谢大佬批评指正&#xff0c;现已更新 preview Target&#xff1a;致力打造最赏心悦目Window下的终端&#xff0c;同时能够很接近Linux的使用习惯 key word&#xff1a;windows终端美化 windows terminal windows powershell 类似Linux下的Window终端 Window也能用ll windows…

让Excel 365 Excel 2021快速转化为生产力,创造价值!

文章目录 每日一句正能量前言关键点内容简介作者简介读者赞誉后记购买链接赠书活动 每日一句正能量 人的一生&#xff0c;好不好只有自己知道&#xff0c;乐不乐只有自己明白。快乐是一种心情&#xff0c;一种自然、积极向上的心态。在平凡之中寻求快乐&#xff0c;在磨难之中寻…

AI智能伪原创工具:原创文章自动生成的革新

随着人工智能技术的迅猛发展&#xff0c;AI智能伪原创工具正逐渐改变着我们的日常生活和工作方式。其中&#xff0c;原创文章自动生成技术的出现&#xff0c;为内容创作者、企业和学术界带来了全新的可能性和便利。这项技术的引入不仅提高了内容创作的效率&#xff0c;还为用户…

【查找算法】插值查找

一&#xff1a;插值查找 代码公式&#xff1a;int mid left (right - left) * (findVal - arr[left]) / (arr[right] - arr[left]); 1.1 基本概念 插值查找&#xff0c;有序表的一种查找方式。插值查找是根据查找关键字与查找表中最大最小记录关键字比较后的查找方法。插值…

什么样的跨网数据交换产品 能实现数据摆渡和数据同步?

首先&#xff0c;为什么会产生跨网数据摆渡的需求和场景呢&#xff1f;那是因为做了网络隔离&#xff0c;企业进行网络隔离的原因主要包括以下几点&#xff1a; 1、提高安全性&#xff1a;网络隔离是防止未授权访问和网络攻击的有效手段。通过将网络划分为多个独立的子网&…