经典算法:Fenwick Tree

news2024/10/6 4:09:37
  • 经典算法:Fenwick Tree
    • 1. 算法简介
    • 2. 原理介绍
    • 3. 算法实现
    • 4. 例题说明
      • 1. 解题思路
      • 2. 代码实现
    • 5. 参考链接

1. 算法简介

Fenwick Tree又称为Binary Indexed Tree,也算是一种常见的数据结构了。

他其实某种意义上来说算是Segment Tree的一种变体,其主要的用途也是用于一些需要频繁对数组中元素进行变换以及范围内求和的数组的优化实现。

正如之前在Segment Tree的介绍博客(经典算法:Segment Tree)中说明的那样:数组的范围求和以及值更新事实上是两个比较难以兼顾的操作,往往在其中一个的时间复杂度是 O ( 1 ) O(1) O(1)时,另一个的时间复杂度就会变为 O ( N ) O(N) O(N)

因此,如果需要频繁地改变数组中的值以及对数组中某一范围内的元素进行求和或者其他操作的话,整体的时间复杂度就会变为 O ( N 2 ) O(N^2) O(N2)

而Segment Tree就是针对上述问题的一种解决方法,其核心思路是通过一个树结构来记录每一个分段当中元素的和/最大/最小值,从而使得元素的更新以及query的时间复杂度都退化为 O ( l o g N ) O(logN) O(logN),进而优化整体的算法复杂度为 O ( N l o g N ) O(NlogN) O(NlogN)

但是,如前述博客当中所述,Segment Tree实现所需的最小空间复杂度是 O ( 2 N ) O(2N) O(2N)

而基于这方面,Fenwick Tree,或者说Binary Indexed Tree则可以进一步地对这部分内容进行优化。

它同样可以使用一个数组来快速对其进行实现,且较之Segment Tree,它所需的空间复杂度仅为 O ( N ) O(N) O(N),且同样可以保持update以及query的单次时间复杂度为 O ( l o g N ) O(logN) O(logN)

但是,需要注意的是,在Segment Tree当中,任意一段区间都可以表示为树当中某一些子区间的合集,因此,我们可以任意变换来求取某一子区间的和/最大值/最小值,只要这个特征的计算是无序的,它总是可以用Segment Tree来实现的。

而在Fenwick Tree当中,我们却无法将任意子区间拆分为树上面某一些节点所表示的区间的合集。事实上,对于Fenwick Tree,我们只能够求得任意前 i i i个元素的特征。因此,对于求前序和或者求分段区间内和的问题,我们还是可以通过Fenwick Tree来替换实现的,但是如果要求得任意区间的最大值或者最小值时,Fenwick Tree就爱莫能助了……

综上,我们简单介绍了:

  1. Fenwick树是什么
  2. Fenwick树的主要用途
  3. 与Segment Tree相比较,Fenwick Tree的优点与缺点

下面,我们就来看一下Fenwick Tree的具体原理和实现。

2. 原理介绍

Fenwick Tree的原理,或者用它的另一个名字会更好理解一点,即Binary Indexed Tree。

本质上来说,它和Segment Tree的原理是接近的,都是通过将原数组进行切分,得到一系列小的区间,然后每次update和query操作时,就将两者都变成对于这些区间的操作,使得两者都变为 O ( l o g N ) O(logN) O(logN)的算法复杂度,而不会像传统的方法中那样一个是 O ( 1 ) O(1) O(1)和另一个是 O ( N ) O(N) O(N)

而具体的这个区分分割的方法就是Segment Tree和Fenwick Tree的唯一差别。

Segment Tree的方法直接trivial,就是通过二分法由顶向下地将原区间不断地拆分直到单元素。

而Fenwick Tree的思路则是相反的,它是由底向上地推过去,直到可以覆盖所有的数组区间。

具体来说,假设数组长度为 n n n,那么 n n n总可以用二进制进行表达,即: n = 2 i 1 + ⋯ 2 i k n = 2^{i_1} + \cdots 2^{i_k} n=2i1+2ik,其中,不妨设 i 1 > i 2 > ⋯ > i k i_1 > i_2 > \cdots > i_k i1>i2>>ik

此时,我们总可以将前 2 i 1 2^{i_1} 2i1个元素是为一组,然后其后的 2 i 2 2^{i_2} 2i2个元素视为一组,以此类推,然后分别进行表达。

具体我们可以用下图进行说明:

在这里插入图片描述

其中,每一个子节点的值都是其子树节点的元素之和。

特别地,我们给出一个13个元素的数组所构成的Fenwick Tree的结构如下:

在这里插入图片描述

其中,黑色节点为真实节点,灰色节点为虚节点,并不真实存在,但是为了方便理解还是画在了图中。

下面,我们只需要考察其数组的update和query方法即可。

很显然,如果要update某一个元素,只需要以该元素为起点,自下而上地更新该元素以及其父节点当中的元素即可。以上述Fenwick Tree为例,如果要更新节点3的值,那么就要依次更新节点3、4、8。

同样的,如果要对query从起点到某一个位置为止的所有元素的前序和,只需要自右向左地累加所有子树的最上层父节点的值即可。同样以上述Fenwick Tree为例,如果要查询前11个元素的前序和,那么就要依次累加节点11、10、8当中的元素值。

因此,问题也就划归为怎么求取元素的父节点以及左邻接节点的值了。

而这个问题事实上也是Fenwick Tree当中最fancy的设计,我看了一下网上好多的博客,无论中文的还是英文的,感觉其实都没有说的很明白,但是代码不会唬人,看代码的话就还是挺明白的了。

首先,可以先定义一个叫做Last Set Bit(LSB)的东西,说白了就是任何一个数使用二进制表示之后最后一个1出现的位置。换用上述 n = 2 i 1 + ⋯ 2 i k n = 2^{i_1} + \cdots 2^{i_k} n=2i1+2ik的定义的话( i 1 > i 2 > ⋯ > i k i_1 > i_2 > \cdots > i_k i1>i2>>ik),其LSB就是 2 i k 2^{i_k} 2ik

更具体的例子来说, 5 ( 101 ) 5(101) 5(101)的LSB就是 1 ( 1 ) 1(1) 1(1) 10 ( 1010 ) 10(1010) 10(1010)的LSB就是 2 ( 10 ) 2(10) 2(10) 8 ( 1000 ) 8(1000) 8(1000)的LSB就是 8 ( 1000 ) 8(1000) 8(1000)

而LSB的具体代码实现则更加简单,由计算机负数存储方式的机制,我们很快就可以给出LSB的快速计算方式为:

def LSB(x):
    return x & (-x)

此时,我们不难发现:

  1. 求取一个元素的父节点:x = x + LSB(x)
  2. 求取一个元素的左邻接节点:x = x - LSB(x)

因此,上述问题也就搞定了,我们只需要将其翻译为代码语言即可得到Fenwick Tree的实现了。

3. 算法实现

基于上述原理介绍,我们可以给出python的算法实现如下:

class FenwickTree:
    def __init__(self, arr):
        self.length = len(arr)
        self.vals = [0 for _ in arr]
        self.tree = [0 for _ in range(self.length + 1)]
        for idx, val in enumerate(arr):
            self.update(idx, val)

    def update_vals(self, idx, val):
        change = val - self.vals[idx]
        self.vals[idx] = val
        return change

    def update_tree(self, idx, change):
        idx = idx + 1
        while idx <= self.length:
            self.tree[idx] += change
            idx += idx & (-idx)
        return

    def update(self, idx, val):
        change = self.update_vals(idx, val)
        self.update_tree(idx, change)
        return

    def query(self, idx):
        _sum = 0
        idx = idx + 1
        while idx > 0:
            _sum += self.tree[idx]
            idx -= idx & (-idx)
        return _sum

    def query_range(self, left, right):
        return self.query(right) - self.query(left-1)

4. 例题说明

最后,我们来用一道例题来给一个具体的使用例子:

  • Leetcode 2659. Make Array Empty

1. 解题思路

这一题的具体思路其实在之前Segment Tree当中我们已经给出了说明,这里就不再赘述了,可以直接参看之前的博客:

  • 经典算法:Segment Tree

2. 代码实现

我们直接给出python代码实现如下:

class FenwickTree:
    def __init__(self, arr):
        self.length = len(arr)
        self.vals = [0 for _ in arr]
        self.tree = [0 for _ in range(self.length + 1)]
        for idx, val in enumerate(arr):
            self.update(idx, val)

    def update_vals(self, idx, val):
        change = val - self.vals[idx]
        self.vals[idx] = val
        return change

    def update_tree(self, idx, change):
        idx = idx + 1
        while idx <= self.length:
            self.tree[idx] += change
            idx += idx & (-idx)
        return

    def update(self, idx, val):
        change = self.update_vals(idx, val)
        self.update_tree(idx, change)
        return

    def query(self, idx):
        _sum = 0
        idx = idx + 1
        while idx > 0:
            _sum += self.tree[idx]
            idx -= idx & (-idx)
        return _sum

    def query_range(self, left, right):
        return self.query(right) - self.query(left-1)

class Solution:
    def countOperationsToEmptyArray(self, nums: List[int]) -> int:
        n = len(nums)
        index = [i for i in range(n)]
        index = sorted(index, key=lambda x: nums[x])

        status = [1 for _ in range(n)]
        fenwick_tree = FenwickTree(status)
        prev = 0
        res = 0
        for idx in index:
            if idx >= prev:
                res += fenwick_tree.query_range(prev, idx) - 1
            else:
                res += fenwick_tree.query_range(0, idx) + fenwick_tree.query_range(prev, n-1) - 1
            prev = idx
            fenwick_tree.update(idx, 0)
        return res + n

提交代码评测得到:耗时4097ms,占用内存32.8MB。

5. 参考链接

  1. https://en.wikipedia.org/wiki/Fenwick_tree
  2. https://www.geeksforgeeks.org/binary-indexed-tree-or-fenwick-tree-2/
  3. https://cp-algorithms.com/data_structures/fenwick.html
  4. https://www.hackerearth.com/practice/notes/binary-indexed-tree-or-fenwick-tree/
  5. https://www.geeksforgeeks.org/binary-indexed-tree-range-update-range-queries/
  6. https://brilliant.org/wiki/fenwick-tree/

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

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

相关文章

克隆虚拟机

上一篇我们已经讲过了启动虚拟机并安装Linux系统&#xff0c;下面我们来讲一下如何通过已经创建好的虚拟机spark01克隆出spark02和spark03来&#xff0c;从而满足搭建大数据集群环境需要多台虚拟机的需求。 首先我们要理解两个概念&#xff1a; 1.完整克隆 完整克隆的虚拟机可…

【算法证明 三】计算顺序统计量的复杂度

计算顺序统计量&#xff0c;在 c 标准库中对应有一个函数&#xff1a;nth_element。其作用是求解一个数组中第 k 大的数字。常见的算法是基于 partition 的分治算法。不难证明这种算法的最坏复杂度是 Θ ( n 2 ) \Theta(n^2) Θ(n2)。但是其期望复杂度是 Θ ( n ) \Theta(n) …

从源码全面解析 dubbo 服务暴露的来龙去脉

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱敲代码的小黄&#xff0c;独角兽企业的Java开发工程师&#xff0c;CSDN博客专家&#xff0c;阿里云专家博主&#x1f4d5;系列专栏&#xff1a;Java设计模式、Spring源码系列、Netty源码系列、Kafka源码系列、JUC源码…

SpringBoot配置 -- SpringBoot快速入门保姆级教程(二)

文章目录 前言二、SpringBoot配置1. 了解配置文件的3种格式2.yaml格式语法规则3.读取yaml数据的3种方式4.多环境开发配置5.多环境命令行启动参数设置6. 多环境开发兼容问题7.配置文件分类 总结 前言 为了巩固所学的知识&#xff0c;作者尝试着开始发布一些学习笔记类的博客&am…

vcruntime140.dll如何修复

VCRUNTIME140.dll是Windows操作系统上一个非常重要的动态链接库文件&#xff0c;它是由Microsoft Visual C Runtime提供的运行时库文件之一&#xff0c;被许多应用程序用来进行编译和运行。如果该文件丢失或损坏&#xff0c;很多应用程序就无法正常运行&#xff0c;这可能会带来…

三分钟了解SpringBoot配置优先级底层源码解析

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是冰点&#xff0c;从业11年&#xff0c;目前在物流独角兽企业从事技术方面工作&#xff0c;&#x1f342;博主正在努力完成2023计划中&#xff1a;以梦为马&#xff0c;扬帆起航&#xff0c;2023追梦人&#x1f4dd;联系…

关于性能测试平台的一些想法,想跟大家聊一下

目录 一、任务管理 二、用例管理 三、环境管理 四、压测机管理 五、数据管理 六、监控管理 七、日志管理 八、报表管理 九、配置管理 十、系统管理 组织架构 这里我按照每个不同系统归属的项目组为横向&#xff0c;性能测试团队作为职能部门为纵向的矩阵式组织架构为…

JUC学习(二)

目录 Doug Lea — JUC并发包的作者锁框架Lock和Condition接口可重入锁公平锁与非公平锁读写锁锁降级和锁升级队列同步器AQS底层实现公平锁一定公平吗&#xff1f;Condition实现原理 ——————————————————————————————— 在前面&#xff0c;我们了解…

ICV报告:乘光伏新能源汽车之势,功率器件蓄势待发

前言&#xff1a; 电力电子器件&#xff08;Power Electronic Device&#xff09;&#xff0c;又称为功率半导体器件&#xff0c;用于电能变换和电能控制电路中的大功率(通常指电流为数十至数千安&#xff0c;电压为数百伏以上)电子器件。功率器件能够承受和控制较大电流、电压…

无限阳光、自动收集阳光CALL、阳光产生速度

简单实现无限阳光 本次实验内容&#xff1a;通过逆向分析植物阳光数量的动态地址找到阳光的基址与偏移&#xff0c;从而实现每次启动游戏都能够使用基址加偏移的方式定位阳光数据&#xff0c;最后我们将通过使用C语言编写通用辅助实现简单的无限阳光外挂&#xff0c;在教程开始…

Vue Router路由管理器

目录&#xff1a; 相关理解基本路由几个注意事项嵌套&#xff08;多级&#xff09;路由路由的query参数命名路由路由的params参数路由的props配置路由跳转的replace方法编程式路由导航缓存路由组件activated和deactivated路由守卫路由器的两种工作模式 相关理解 vue-route…

博学谷学习记录】超强总结,用心分享 | 架构师 敏捷开发 学习总结

文章目录 敏捷开发1. 概述2. 敏捷开发 敏捷开发 1. 概述 随着软件开发技术的不断发展&#xff0c;现在出现了很多种不同的开发模式&#xff0c;其实敏捷开发已经成为现在很多企业开发应用程序都想要选择的开发方案&#xff0c;那么什么是敏捷开发呢&#xff1f;1.1 四种开发模…

Linux 配置Java环境(一)

Linux 配置Java环境 一、配置Java环境1、查看系统是否有java环境2、卸载系统自带的jdk3、创建一个文件夹用于存放java的压缩包4、包下载好的jdk拖到java文件夹5、安装jdk6、配置环境变量7、让配置生效8、验证是否配置成功 一、配置Java环境 1、查看系统是否有java环境 输入指…

nginx中location和rewrite

常用的Nginx 正则表达式 ^ &#xff1a;匹配输入字符串的起始位置 $ &#xff1a;匹配输入字符串的结束位置 * &#xff1a;匹配前面的字符零次或多次。如“ol*”能匹配“o”及“ol”、“oll” &#xff1a;匹配前面的字符一次或多次。如“ol”能匹配“ol”及“oll”、“olll…

0-1背包问题:动态规划的经典应用

文章目录 引言背包问题简介0-1背包问题定义0-1背包问题的限制条件 动态规划解决思路状态定义状态转移方程 背包问题的Java实现示例与分析 总结 引言 背包问题是在给定一组物品和一个背包容量的情况下&#xff0c;如何选择物品放入背包&#xff0c;以使得放入背包的物品总价值最…

高边功率开关参数Load current(ISO)和Nominal current

1. IL(nom)是没有加散热片的情况下&#xff0c;考虑RON和BTS6133D与环境热阻计算得到的电流值&#xff1b; 2. IL(iso)是有散热片的情况下计算得到的电流值&#xff1b; 3. IL12(SC)是如果负载电流达到75A以上&#xff0c;BTS6143D会通过不断重启来来限制电流在75A以下。

【论文简述】GeoMVSNet: Learning Multi-View Stereo with Geometry Perception(CVPR 2023)

一、论文简述 1. 第一作者&#xff1a;Jie Zhu 2. 发表年份&#xff1a;2023 3. 发表期刊&#xff1a;CVPR 4. 关键词&#xff1a;MVS、级联结构、几何感知、频域增强、高斯混合模型 5. 探索动机&#xff1a;基于级联的结构以从粗到细的方式计算不同分辨率的深度图&#x…

数据结构与算法练习(二)数组模拟队列

文章目录 1、队列2、数组实现队列3、数组实现循环队列 1、队列 队列是一个有序列表&#xff0c;可以用数组或链表来实现 遵循先入先出的原则 在队尾插入元素叫做入队&#xff0c;对头删除元素叫做出队。2、数组实现队列 思路&#xff1a; 用front和rear记录队列前后的下标&a…

【MCS-51单片机汇编语言】期末复习总结⑤——定时器中断方式与查询方式程序设计(题型五)

文章目录 知识准备工作方式寄存器TMODTCON寄存器IE寄存器 定时/计数器的使用初始化 常考题型例题1题目描述题目解析题解 例题2题目描述题目解析题解 知识准备 工作方式寄存器TMOD D7D6D5D4D3D2D1D0TF1TR1TF0TR0IE1IT1IE0IT0 [注]&#xff1a;TMOD高4位与T1相关&#xff0c;低…

iptables和防火墙

文章目录 1.防火墙2.Iptables基本介绍2.1 什么是iptables2.2 什么是包过滤防火墙2.3 包过滤防火墙如何实现 1.防火墙 Linux防火墙主要工作在网络层&#xff0c;针对 TCP/IP 数据包实施过滤和限制&#xff0c;典型的包过滤防火墙&#xff0c;基于内核编码实现&#xff0c;具有非…