高效区间查询与更新:二维Fenwick树的实现与优化

285 阅读9分钟

Fenwick树(也称为树状数组)是一种高效的数据结构,用于处理前缀和与区间查询问题。相比于直接求解,Fenwick树可以在 O(log n) 时间复杂度内完成更新和查询操作。二维Fenwick树是该结构的扩展,适用于二维平面数据,常用于处理矩阵区域的查询和更新问题。

在这篇文章中,我们将介绍二维Fenwick树的基本概念、实现方法,并提供代码示例来展示如何在矩阵上进行高效的区间查询与更新操作。

Fenwick树的基本概念

Fenwick树的核心思想是通过构建一个树状结构,将每个节点表示为某一区域内元素的前缀和。Fenwick树支持以下两种操作:

  • 更新:更新某个位置的值,并更新受影响的前缀和。
  • 查询:查询从起点到某一位置的前缀和。

在二维情况下,我们使用一个二维树状数组来实现矩阵上前缀和的快速查询与更新。

image-20240917175551005

二维Fenwick树的实现原理

二维Fenwick树的核心思想是通过维护一个二维树状数组,每个节点表示一个区域的和。操作时会根据更新或查询的矩阵区域调整或计算对应的前缀和。

二维Fenwick树的操作

  1. 查询矩阵的前缀和 对于矩阵中的一个点 (x, y),我们可以通过累加从起点 (1, 1)(x, y) 区域的前缀和。
  2. 更新矩阵的值 当我们更新某个点 (x, y) 的值时,我们需要更新所有包含该点的区域前缀和。这通过调整二维树状数组中的相关节点值完成。

代码实现

下面是一个完整的二维Fenwick树实现,支持矩阵上的前缀和查询和单点更新操作。

class FenwickTree2D:
    def __init__(self, rows, cols):
        # 初始化一个二维树状数组,大小为(rows + 1) x (cols + 1)
        self.rows = rows
        self.cols = cols
        self.tree = [[0] * (cols + 1) for _ in range(rows + 1)]
​
    def update(self, x, y, value):
        # 更新点(x, y)的值,更新所有受影响的区域
        i = x
        while i <= self.rows:
            j = y
            while j <= self.cols:
                self.tree[i][j] += value
                j += j & -j  # j的二进制最低位加1
            i += i & -i  # i的二进制最低位加1
​
    def prefix_sum(self, x, y):
        # 查询从(1, 1)到(x, y)的前缀和
        result = 0
        i = x
        while i > 0:
            j = y
            while j > 0:
                result += self.tree[i][j]
                j -= j & -j  # j的二进制最低位减1
            i -= i & -i  # i的二进制最低位减1
        return result
​
    def range_sum(self, x1, y1, x2, y2):
        # 查询矩形区域(x1, y1)到(x2, y2)的区间和
        return (self.prefix_sum(x2, y2)
                - self.prefix_sum(x1 - 1, y2)
                - self.prefix_sum(x2, y1 - 1)
                + self.prefix_sum(x1 - 1, y1 - 1))
​
# 测试二维Fenwick树
if __name__ == "__main__":
    # 创建一个5x5的二维Fenwick树
    fenwick_tree = FenwickTree2D(5, 5)
    
    # 更新矩阵中的值
    fenwick_tree.update(2, 3, 4)
    fenwick_tree.update(4, 4, 5)
    fenwick_tree.update(3, 3, 6)
​
    # 查询前缀和与区间和
    print("前缀和 (2, 3):", fenwick_tree.prefix_sum(2, 3))  # 输出: 4
    print("前缀和 (4, 4):", fenwick_tree.prefix_sum(4, 4))  # 输出: 15
    print("区域和 (2, 2) 到 (4, 4):", fenwick_tree.range_sum(2, 2, 4, 4))  # 输出: 15

代码分析

初始化

我们创建了一个大小为 (rows + 1) x (cols + 1) 的二维数组 tree,用于存储前缀和。额外的一行一列用于处理索引的便捷性,因为树状数组的索引从1开始。

更新操作

update(x, y, value) 方法更新矩阵中 (x, y) 位置的值,并根据Fenwick树的规则,调整所有受影响的区域的前缀和。使用i += i & -ij += j & -j 来找到下一个需要更新的节点。

查询操作

prefix_sum(x, y) 方法返回从 (1, 1)(x, y) 区域的前缀和。通过递归减去Fenwick树中的节点值,最终得到结果。

区间查询

range_sum(x1, y1, x2, y2) 方法返回矩阵中 (x1, y1)(x2, y2) 矩形区域内的所有元素的和。我们利用包含排除原理,组合前缀和来计算任意矩形区域的和。

时间复杂度分析

二维Fenwick树的更新和查询操作的时间复杂度均为 O(log(rows) * log(cols)) 。这种效率使其非常适用于频繁进行矩阵区域查询与更新的应用场景。

image-20240917175610207

应用场景

二维Fenwick树广泛应用于图像处理、矩阵区域查询、动态区域求和等需要频繁处理二维数据的领域。例如:

  • 热图分析:可以高效统计任意区域的数值和。
  • 游戏地图:在动态更新游戏地图时,可以快速查询某个区域的总分或状态。

Fenwick树的扩展与优化

在实际应用中,我们可以对二维Fenwick树进行进一步的扩展与优化,以应对不同场景的需求。

1. 支持区间更新

目前的二维Fenwick树只支持单点更新,但在某些应用场景下,我们可能需要对整个区域进行批量更新(即对矩形区域内的所有元素加上某个值)。为了实现这一点,我们可以通过引入差分数组的概念,对Fenwick树进行扩展,使其支持区域更新与区域查询。

区间更新实现思路

假设我们要对矩阵中 (x1, y1)(x2, y2) 之间的所有元素加上某个值 v,我们可以将该操作转换为对四个顶点的操作:

  • (x1, y1) 加上 v
  • (x1, y2 + 1) 减去 v
  • (x2 + 1, y1) 减去 v
  • (x2 + 1, y2 + 1) 加上 v

这样,每次我们在查询某个区域时,通过前缀和计算得到该区域实际更新后的值。

代码实现

class FenwickTree2DRangeUpdate:
    def __init__(self, rows, cols):
        self.rows = rows
        self.cols = cols
        self.tree = [[0] * (cols + 1) for _ in range(rows + 1)]
​
    def update_single(self, x, y, value):
        # 单点更新,与普通Fenwick树类似
        i = x
        while i <= self.rows:
            j = y
            while j <= self.cols:
                self.tree[i][j] += value
                j += j & -j
            i += i & -i
​
    def update_range(self, x1, y1, x2, y2, value):
        # 对矩形区域(x1, y1)到(x2, y2)进行更新
        self.update_single(x1, y1, value)
        self.update_single(x1, y2 + 1, -value)
        self.update_single(x2 + 1, y1, -value)
        self.update_single(x2 + 1, y2 + 1, value)
​
    def prefix_sum(self, x, y):
        # 查询前缀和,与普通Fenwick树类似
        result = 0
        i = x
        while i > 0:
            j = y
            while j > 0:
                result += self.tree[i][j]
                j -= j & -j
            i -= i & -i
        return result
​
    def range_sum(self, x1, y1, x2, y2):
        # 查询矩形区域的和
        return (self.prefix_sum(x2, y2)
                - self.prefix_sum(x1 - 1, y2)
                - self.prefix_sum(x2, y1 - 1)
                + self.prefix_sum(x1 - 1, y1 - 1))
​
# 测试区间更新二维Fenwick树
if __name__ == "__main__":
    # 创建一个5x5的二维Fenwick树
    fenwick_tree = FenwickTree2DRangeUpdate(5, 5)
​
    # 更新矩阵中的区域
    fenwick_tree.update_range(2, 2, 4, 4, 10)
​
    # 查询前缀和与区间和
    print("区域和 (2, 2) 到 (4, 4):", fenwick_tree.range_sum(2, 2, 4, 4))  # 输出: 90
    print("区域和 (1, 1) 到 (5, 5):", fenwick_tree.range_sum(1, 1, 5, 5))  # 输出: 90

2. 处理动态矩阵尺寸

有时我们在构建Fenwick树时,可能并不知道矩阵的大小,或者矩阵大小会随着时间动态调整。为了应对这种情况,可以使用动态分配或重新构建Fenwick树。

  • 动态扩展:通过动态调整树的大小,允许用户根据需要随时扩展矩阵。这通常是通过创建一个新的更大尺寸的树,并将旧树的值拷贝过来完成。
  • 分块处理:如果矩阵非常大,或者我们希望支持更复杂的操作,我们可以将矩阵划分为多个子块,每个子块内部使用Fenwick树来处理小范围的操作,这样可以减少树的维护成本。

image-20240917175631259

动态扩展的实现

class DynamicFenwickTree2D:
    def __init__(self, initial_rows=1, initial_cols=1):
        # 初始化二维树状数组,支持动态扩展
        self.rows = initial_rows
        self.cols = initial_cols
        self.tree = [[0] * (self.cols + 1) for _ in range(self.rows + 1)]
​
    def _resize(self, new_rows, new_cols):
        # 扩展树的尺寸
        new_tree = [[0] * (new_cols + 1) for _ in range(new_rows + 1)]
        for i in range(1, self.rows + 1):
            for j in range(1, self.cols + 1):
                new_tree[i][j] = self.tree[i][j]
        self.rows = new_rows
        self.cols = new_cols
        self.tree = new_tree
​
    def update(self, x, y, value):
        # 检查是否需要扩展
        if x > self.rows or y > self.cols:
            self._resize(max(x, self.rows), max(y, self.cols))
​
        # 更新操作
        i = x
        while i <= self.rows:
            j = y
            while j <= self.cols:
                self.tree[i][j] += value
                j += j & -j
            i += i & -i
​
    def prefix_sum(self, x, y):
        # 查询前缀和
        result = 0
        i = x
        while i > 0:
            j = y
            while j > 0:
                result += self.tree[i][j]
                j -= j & -j
            i -= i & -i
        return result
​
    def range_sum(self, x1, y1, x2, y2):
        # 查询矩形区域的和
        return (self.prefix_sum(x2, y2)
                - self.prefix_sum(x1 - 1, y2)
                - self.prefix_sum(x2, y1 - 1)
                + self.prefix_sum(x1 - 1, y1 - 1))
​
# 测试动态扩展的二维Fenwick树
if __name__ == "__main__":
    # 创建一个动态扩展的二维Fenwick树
    fenwick_tree = DynamicFenwickTree2D()
​
    # 更新矩阵中的值
    fenwick_tree.update(6, 6, 10)
    fenwick_tree.update(4, 4, 5)
​
    # 查询前缀和与区间和
    print("前缀和 (6, 6):", fenwick_tree.prefix_sum(6, 6))  # 输出: 15
    print("区域和 (1, 1) 到 (6, 6):", fenwick_tree.range_sum(1, 1, 6, 6))  # 输出: 15

在上面的代码中,_resize 函数允许我们动态扩展矩阵的大小,并将旧的数据复制到新的矩阵中。

image-20240917175703899

总结

二维Fenwick树(也称为二叉索引树)是处理二维矩阵中前缀和查询和更新操作的高效数据结构,尤其在频繁的区间查询和单点更新的场景下表现突出。本文详细介绍了二维Fenwick树的基本实现、区间查询的逻辑、以及对其进行扩展和优化的多种方法,包括:

  • 基本实现:通过树状数组,能够在 O(log n * log m) 的时间复杂度内完成前缀和查询和单点更新。
  • 支持区间更新:通过引入差分数组的思想,可以实现对矩形区域的批量更新,同时保持高效的查询性能。
  • 动态矩阵尺寸扩展:通过对树状数组的动态扩展,解决了矩阵大小不固定的场景,使得Fenwick树适应更多实际应用。

Fenwick树的高效性和灵活性使其在处理二维空间中的大规模数据时尤为适用,尤其是在涉及频繁查询和更新的场景下,如图像处理、地理信息系统和游戏开发中。