Algorithm 位:使用二叉索引树?

Algorithm 位:使用二叉索引树?,algorithm,language-agnostic,tree,Algorithm,Language Agnostic,Tree,与其他数据结构相比,二叉索引树很少或相对没有理论可供研究。这是唯一一个能简明扼要地教授它的地方。虽然本教程的所有解释都很完整,但我无法理解这样一棵树背后的直觉是什么?如何证明它的正确性 我认为这个证据很难解释。那么当使用BIT时,您会采用什么方法?我发现@templatetypedef非常清楚地解释了二叉索引树的直觉和证明: 答案 直观地说,您可以将二叉索引树视为二叉树的压缩表示形式,二叉树本身就是标准数组表示形式的优化。这个答案有一个可能的推论 例如,让我们假设您要存储总共7个不同元素的累积频

与其他数据结构相比,二叉索引树很少或相对没有理论可供研究。这是唯一一个能简明扼要地教授它的地方。虽然本教程的所有解释都很完整,但我无法理解这样一棵树背后的直觉是什么?如何证明它的正确性

我认为这个证据很难解释。那么当使用BIT时,您会采用什么方法?

我发现@templatetypedef非常清楚地解释了二叉索引树的直觉和证明: 答案

直观地说,您可以将二叉索引树视为二叉树的压缩表示形式,二叉树本身就是标准数组表示形式的优化。这个答案有一个可能的推论

例如,让我们假设您要存储总共7个不同元素的累积频率。您可以从写下七个桶开始,将数字分配到其中:

[   ] [   ] [   ] [   ] [   ] [   ] [   ]
  1     2     3     4     5     6     7
现在,让我们假设累积频率如下所示:

[ 5 ] [ 6 ] [14 ] [25 ] [77 ] [105] [105]
  1     2     3     4     5     6     7
使用此版本的数组,可以通过增加存储在该点的数字的值,然后增加随后出现的所有元素的频率来增加任何元素的累积频率。例如,要将3的累积频率增加7,我们可以在位置3处或之后向数组中的每个元素添加7,如下所示:

[ 5 ] [ 6 ] [21 ] [32 ] [84 ] [112] [112]
  1     2     3     4     5     6     7
问题是这样做需要O(n)时间,如果n很大,速度会很慢

我们可以考虑改进此操作的一种方法是更改存储在桶中的内容。您可以考虑只存储当前频率相对于前一个桶增加的量,而不是存储到给定点的累积频率。例如,在我们的例子中,我们将重写上述存储桶,如下所示:

Before:
[ 5 ] [ 6 ] [21 ] [32 ] [84 ] [112] [112]
  1     2     3     4     5     6     7

After:
[ +5] [ +1] [+15] [+11] [+52] [+28] [ +0]
  1     2     3     4     5     6     7
现在,我们可以在时间O(1)内增加一个桶内的频率,只需向该桶添加适当的量。但是,执行查找的总成本现在变为O(n),因为我们必须通过将所有较小存储桶中的值相加来重新计算存储桶中的总成本

我们需要从这里了解到的关于二叉索引树的第一个主要见解如下:与其连续重新计算特定元素之前的数组元素之和,不如预先计算序列中特定点之前所有元素的总和?如果我们能做到这一点,那么我们就可以通过对这些预先计算的总和的正确组合求和来计算出某一点的累积总和

一种方法是将表示形式从桶数组更改为节点的二叉树。每个节点都将使用一个值进行注释,该值表示该给定节点左侧所有节点的累积和。例如,假设我们从这些节点构造以下二叉树:

             4
          /     \
         2       6
        / \     / \
       1   3   5   7
现在,我们可以通过存储包括该节点及其左子树在内的所有值的累积和来扩充每个节点。例如,给定我们的值,我们将存储以下内容:

Before:
[ +5] [ +1] [+15] [+11] [+52] [+28] [ +0]
  1     2     3     4     5     6     7

After:
                 4
               [+32]
              /     \
           2           6
         [ +6]       [+80]
         /   \       /   \
        1     3     5     7
      [ +5] [+15] [+52] [ +0]
                 4
               [+32]
              /     \
           2           6
         [ +6]       [+80]
         /   \       /   \
      > 1     3     5     7
      [ +5] [+15] [+52] [ +0]
              (empty)
               [+37]
              /     \
           0           1
         [+11]       [+80]
         /   \       /   \
        00   01     10   11
      [+10] [+15] [+52] [ +0]
考虑到这种树结构,很容易确定累积到某一点的总和。其思想如下:我们维护一个计数器,初始值为0,然后执行常规的二进制搜索,直到找到相关节点。当我们这样做的时候,我们还需要做以下工作:任何时候我们向右移动,我们也会将当前值添加到计数器中

例如,假设我们要查找3的和。为此,我们采取以下措施:

Before:
[ +5] [ +1] [+15] [+11] [+52] [+28] [ +0]
  1     2     3     4     5     6     7

After:
                 4
               [+32]
              /     \
           2           6
         [ +6]       [+80]
         /   \       /   \
        1     3     5     7
      [ +5] [+15] [+52] [ +0]
                 4
               [+32]
              /     \
           2           6
         [ +6]       [+80]
         /   \       /   \
      > 1     3     5     7
      [ +5] [+15] [+52] [ +0]
              (empty)
               [+37]
              /     \
           0           1
         [+11]       [+80]
         /   \       /   \
        00   01     10   11
      [+10] [+15] [+52] [ +0]
  • 从根开始(4)。计数器是0
  • 向左转到节点(2)。计数器是0
  • 向右转到节点(3)。计数器为0+6=6
  • 查找节点(3)。计数器为6+15=21
您还可以想象以相反的方式运行此过程:从给定节点开始,将计数器初始化为该节点的值,然后沿着树走到根。任何时候,当您向上跟随右子链接时,在您到达的节点处添加值。例如,要查找3的频率,我们可以执行以下操作:

Before:
[ +5] [ +1] [+15] [+11] [+52] [+28] [ +0]
  1     2     3     4     5     6     7

After:
                 4
               [+32]
              /     \
           2           6
         [ +6]       [+80]
         /   \       /   \
        1     3     5     7
      [ +5] [+15] [+52] [ +0]
                 4
               [+32]
              /     \
           2           6
         [ +6]       [+80]
         /   \       /   \
      > 1     3     5     7
      [ +5] [+15] [+52] [ +0]
              (empty)
               [+37]
              /     \
           0           1
         [+11]       [+80]
         /   \       /   \
        00   01     10   11
      [+10] [+15] [+52] [ +0]
  • 从节点(3)开始。柜台是15号
  • 向上转到节点(2)。计数器是15+6=21
  • 向上转到节点(1)。柜台是21号
为了增加一个节点的频率(以及隐式地增加它后面的所有节点的频率),我们需要更新树中包含该节点的左子树中的节点集。为此,我们执行以下操作:增加该节点的频率,然后开始向上走到树的根。任何时候,当您跟随一个将您作为左孩子的链接时,通过添加当前值来增加您遇到的节点的频率

例如,要将节点1的频率增加5,我们将执行以下操作:

Before:
[ +5] [ +1] [+15] [+11] [+52] [+28] [ +0]
  1     2     3     4     5     6     7

After:
                 4
               [+32]
              /     \
           2           6
         [ +6]       [+80]
         /   \       /   \
        1     3     5     7
      [ +5] [+15] [+52] [ +0]
                 4
               [+32]
              /     \
           2           6
         [ +6]       [+80]
         /   \       /   \
      > 1     3     5     7
      [ +5] [+15] [+52] [ +0]
              (empty)
               [+37]
              /     \
           0           1
         [+11]       [+80]
         /   \       /   \
        00   01     10   11
      [+10] [+15] [+52] [ +0]
从节点1开始,将其频率增加5以获得

                 4
               [+32]
              /     \
           2           6
         [ +6]       [+80]
         /   \       /   \
      > 1     3     5     7
      [+10] [+15] [+52] [ +0]
现在,转到其父级:

                 4
               [+32]
              /     \
         > 2           6
         [ +6]       [+80]
         /   \       /   \
        1     3     5     7
      [+10] [+15] [+52] [ +0]
               > 4
               [+32]
              /     \
           2           6
         [+11]       [+80]
         /   \       /   \
        1     3     5     7
      [+10] [+15] [+52] [ +0]
我们跟随一个向上的左子链接,因此我们也增加了该节点的频率:

                 4
               [+32]
              /     \
         > 2           6
         [+11]       [+80]
         /   \       /   \
        1     3     5     7
      [+10] [+15] [+52] [ +0]
我们现在转到它的父级:

                 4
               [+32]
              /     \
         > 2           6
         [ +6]       [+80]
         /   \       /   \
        1     3     5     7
      [+10] [+15] [+52] [ +0]
               > 4
               [+32]
              /     \
           2           6
         [+11]       [+80]
         /   \       /   \
        1     3     5     7
      [+10] [+15] [+52] [ +0]
这是一个左子链接,因此我们也增加了该节点:

                 4
               [+37]
              /     \
           2           6
         [+11]       [+80]
         /   \       /   \
        1     3     5     7
      [+10] [+15] [+52] [ +0]
现在我们完成了

最后一步是将其转换为二叉索引树,这就是我们使用二进制数做一些有趣的事情的地方。让我们用二进制文件重写此树中的每个bucket索引:

                100
               [+37]
              /     \
          010         110
         [+11]       [+80]
         /   \       /   \
       001   011   101   111
      [+10] [+15] [+52] [ +0]
在这里,我们可以做一个非常非常冷静的观察。取这些二进制数中的任何一个,找到该数字中最后一个设置的1,然后删除该位以及其后的所有位。现在只剩下以下内容:

Before:
[ +5] [ +1] [+15] [+11] [+52] [+28] [ +0]
  1     2     3     4     5     6     7

After:
                 4
               [+32]
              /     \
           2           6
         [ +6]       [+80]
         /   \       /   \
        1     3     5     7
      [ +5] [+15] [+52] [ +0]
                 4
               [+32]
              /     \
           2           6
         [ +6]       [+80]
         /   \       /   \
      > 1     3     5     7
      [ +5] [+15] [+52] [ +0]
              (empty)
               [+37]
              /     \
           0           1
         [+11]       [+80]
         /   \       /   \
        00   01     10   11
      [+10] [+15] [+52] [ +0]
这里有一个非常非常酷的观察结果:如果你把0看作是“左”的意思,把1看作是“右”,那么每个数字上的剩余位都精确地说明了如何从根开始,然后向下走到那个数字。例如,节点5具有二进制模式101。最后一个1是最后一位,所以我们去掉它得到10。事实上,如果你开始