经典算法:Fenwick Tree

news/2024/10/21 23:15:01/
  • 经典算法: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] = valreturn changedef update_tree(self, idx, change):idx = idx + 1while idx <= self.length:self.tree[idx] += changeidx += idx & (-idx)returndef update(self, idx, val):change = self.update_vals(idx, val)self.update_tree(idx, change)returndef query(self, idx):_sum = 0idx = idx + 1while idx > 0:_sum += self.tree[idx]idx -= idx & (-idx)return _sumdef 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] = valreturn changedef update_tree(self, idx, change):idx = idx + 1while idx <= self.length:self.tree[idx] += changeidx += idx & (-idx)returndef update(self, idx, val):change = self.update_vals(idx, val)self.update_tree(idx, change)returndef query(self, idx):_sum = 0idx = idx + 1while idx > 0:_sum += self.tree[idx]idx -= idx & (-idx)return _sumdef 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 = 0res = 0for idx in index:if idx >= prev:res += fenwick_tree.query_range(prev, idx) - 1else:res += fenwick_tree.query_range(0, idx) + fenwick_tree.query_range(prev, n-1) - 1prev = idxfenwick_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.ppmy.cn/news/209049.html

相关文章

SpringMVC第十一阶段:SpringMVC 拦截器执行源码解析

SpringMVC 拦截器执行源码解析&#xff1a; 1、执行doDispatcher做请求分发处理 1.1、调用getHandler()获取请求处理器&#xff0c;处理器中包含请求的方法和拦截器信息 getHandlerInternal() 根据请求地址获取对应的目标方法getHandlerExecutionChain() 获取请求地址对…

shell编程-02-变量作用域

作用域 局部变量&#xff1a;变量只能在函数内部使用 全局变量&#xff1a;变量可以在当前 Shell 进程中使用 环境变量&#xff1a;变量还可以在子进程中使用 局部变量 函数中定义的变量默认是全局变量&#xff0c;在定义时加上local命令&#xff0c;此时该变量就成了局部变…

VScode远程连接虚拟机(ubuntu系统)

文章目录 1. Windows端安装VScode2. 安装远程登录插件3. 配置Remote-SSH插件关于关闭后如何打开该配置文件 4. 测试ubuntu与windows可否ping通5. 在Ubuntu中安装 SSH1.检查是否安装ssh-server2.安装openssh-server3.查看ssh服务是否启动4.Ubuntu中配置openssh-server开机自动启…

从美图手机跨界颐和园,看智能手机如何玩IP定制才能C位出道?

6月27日美图手机在北京颐和园发布美图T9标准版&#xff0c;并首次与颐和园合作推出美图T9颐和园限量版&#xff0c;其“机身图案灵感源自中国传统色彩——黛绿与碧绿&#xff0c;缀以朱砂&#xff0c;山为黛&#xff0c;水为碧&#xff0c;朱砂为红&#xff0c;宛如一幅山水墨&…

莱卡TG0020-M8读卡器单片机开发

1.1 概述 本模块属 RFID 超小型 IC(Mifare1/TypeA/14443A)卡读卡模块&#xff0c;可单独使用&#xff0c;也可二次开发。可选接口有&#xff08;miniUSB 接口&#xff0c;RS232 串口&#xff0c;TTL串口&#xff09;&#xff0c;用户无须了解任何 RC523 等射频芯片的复杂控制命…

Dell服务器iDrac口默认账号密码和IP

账号&#xff1a;root 密码&#xff1a;calvin IP&#xff1a;192.168.0.120

华为云终端被他人设置密码,如何解除?可以恢复出厂设置,消除密码!

当我们需要对控制中心进行操作时&#xff0c;会弹出“密码验证”对话框&#xff0c;见下图&#xff1a; 面对这样的情况&#xff0c;肯定是有人莫名设置了密码&#xff0c;我们可以恢复出厂设置&#xff0c;消除密码&#xff01;具体以下步骤&#xff1a; 1、云终端注销&#…

Dell 服务器如何在BIOS 下清除iDRAC 日志

1.开机自检&#xff0c;根据屏幕右上角提示&#xff0c;按F2 进入system setup 2.出现以下界面&#xff0c; 选择 "iDRAC Settings" 3. 选择System Event Log 4. 如图选择YES 5. 保存设置&#xff0c;选择Finish&#xff0c; 退出重启后&#xff0c;日志清除完毕