数据结构--树状数组

树状数组(Fenwick Tree)

概述

树状数组是一种用于高效处理动态数组中前缀和查询的数据结构。它能够在 O ( l o g n ) O(log n) O(logn) 时间复杂度内完成以下操作:

  • 更新数组中的元素O(logn)
  • 查询数组前缀和O(logn)

数组O(1) 更新,O(n) 前缀和

前缀和数组O(n) 更新,O(1) 前缀和

如果问题同时要大量更新和求前缀和,上述两种数据结构均会寄掉

树状数组则采取折中思路,把整体复杂度降低至O(logn)

数据结构

先放张整体结构图:

核心思想:二进制

对任意数x可将其二进制分解
x = 2 i k + 2 i k − 1 + 2 i k − 2 + ⋯ + 2 i 1 x = 2^{i_k} + 2^{i_{k-1}} + 2^{i_{k-2}} + \cdots + 2^{i_1} x=2ik+2ik−1+2ik−2+⋯+2i1

其中 i k > i k − 1 > i k − 2 > ⋯ > i 1 \text{其中}i_k>i_{k-1}>i_{k-2}>\cdots>i_1 其中ik>ik−1>ik−2>⋯>i1

从而将区间(0, x]分为以下几个部分:

(x - 2\^{i_1}, x\] \\longrightarrow \\text{长度 } 2\^{i_1}

( x − 2 i 1 − 2 i 2 , x − 2 i 1 ] ⟶ 长度 2 i 2 (x - 2^{i_1} - 2^{i_2}, x - 2^{i_1}] \longrightarrow \text{长度 } 2^{i_2} (x−2i1−2i2,x−2i1]⟶长度 2i2

( x − 2 i 1 − 2 i 2 − 2 i 3 , x − 2 i 1 − 2 i 2 ] ⟶ 长度 2 i 3 (x - 2^{i_1} - 2^{i_2} - 2^{i_3}, x - 2^{i_1} - 2^{i_2}] \longrightarrow \text{长度 } 2^{i_3} (x−2i1−2i2−2i3,x−2i1−2i2]⟶长度 2i3

( 0 , 2 i k ] ⟶ 长度 2 i k (0, 2^{i_k}] \longrightarrow \text{长度 } 2^{i_k} (0,2ik]⟶长度 2ik

容易发现,对于任意一段区间(L,R]

区间长度为lowbit(x),区间左端点L = R - lowbit(R)

则在上述规则下,只要确定右端点,左端点的信息也唯一确定


树状数组用一个数组来存储序列的信息:

tr[x]:存储序列在[x - lowbit(x) + 1, x]之间的数的片段和

则按照前面的区间划分规则

∑ i = 1 x a [ i ] = ∑ i = x − 2 i 1 + 1 x a [ i ] + ∑ i = x − 2 i 1 − 2 i 2 + 1 x − 2 i 1 a [ i ] + ⋯ + ∑ i = 1 2 i k a [ i ] \sum_{i = 1}^x{a[i]} = \sum_{i=x-2^{i_1}+1}^x{a[i]} + \sum_{i=x-2^{i_1}-2^{i_2}+1}^{x-2^{i_1}}{a[i]} + \cdots + \sum_{i=1}^{2^{i_k}}{a[i]} ∑i=1xa[i]=∑i=x−2i1+1xa[i]+∑i=x−2i1−2i2+1x−2i1a[i]+⋯+∑i=12ika[i]
= t r [ x ] + t r [ x − 2 i 1 ] + ⋯ + t r [ 2 i k ] \qquad \qquad = tr[x] + tr[x-2^{i_1}] + \cdots + tr[2^{i_k}] =tr[x]+tr[x−2i1]+⋯+tr[2ik]
= t r [ x ] + t r [ x − lowbit ( x ) ] + t r [ ( x − lowbit ( x ) ) − lowbit ( x − lowbit ( x ) ) ] + ⋯ \qquad \qquad = tr[x] + tr[x-\text{lowbit}(x)] + tr[(x-\text{lowbit}(x))-\text{lowbit}(x-\text{lowbit}(x))] + \cdots =tr[x]+tr[x−lowbit(x)]+tr[(x−lowbit(x))−lowbit(x−lowbit(x))]+⋯

看到公式的第三行,很容易想到可以用递归来实现,只需每层往下不断-lowbit(t)就行

x最多只有logx位1,所以树状数组求前缀和的操作复杂度是 O(logn)

类似的,若要实现在原数组 x位上添加 c
t r [ x ] , t r [ x + l o w b i t ( x ) ] , t r [ ( x + l o w b i t ( x ) ) + l o w b i t ( t r [ x ] + l o w b i t ( x ) ) ] , ⋯ tr[x], tr[x + lowbit(x)], tr[(x + lowbit(x)) + lowbit(tr[x]+lowbit(x))],\cdots tr[x],tr[x+lowbit(x)],tr[(x+lowbit(x))+lowbit(tr[x]+lowbit(x))],⋯

均需添加c(可能这个结论不是那么明显,读者可自行思考其中的原理,后续笔者将补充上证明)

当然这里不会无穷往后面加,我们只需用到1~n的数据,当加到超过n就可以停了,故整该操作的复杂度仍旧为O(logn)(分析同求和

一个更容易理解的视频讲解

操作

1. lowbit运算

复杂度:O(1)

代码如下,大家可以自行找几个数验证一下

C++ 复制代码
int lowbit(int x)
{
    // 取出x的最后一位1
    return x & -x;
}

2.添加

复杂度:O(logn)

C++ 复制代码
int add(int x, int c)
{
	// 向第x位添加c,c可正可负
	// 对所有含第x位的树节点均加上c
	for (int i = X; i <= n; i += lowbit(i)) tr[i] += c;
}

3.前缀和

复杂度:O(logn)

C++ 复制代码
int sum(int x)
{
	// 对第1~x位求和
	// 计算当前数存的值,然后迭代求剩余节点的值
	if (!x) return 0;
	return tr[x] + sum(x - lowbit(x));
}

例题

洛谷 P10589 楼兰图腾

题目描述

在完成了分配任务之后,西部 314 来到了楼兰古城的西部。相传很久以前这片土地上(比楼兰古城还早)生活着两个部落,一个部落崇拜尖刀(V),一个部落崇拜铁锹(),他们分别用 V 的形状来代表各自部落的图腾。

西部 314 在楼兰古城的下面发现了一幅巨大的壁画,壁画上被标记出了 N N N 个点,经测量发现这 N N N 个点的水平位置和竖直位置是两两不同的。西部 314 认为这幅壁画所包含的信息与这 N N N 个点的相对位置有关,因此不妨设坐标分别为 ( 1 , y 1 ) , ( 2 , y 2 ) , ⋯   , ( n , y n ) (1,y_1),(2,y_2),\cdots,(n,y_n) (1,y1),(2,y2),⋯,(n,yn),其中 y 1 ∼ y n y_1\sim y_n y1∼yn 是 1 1 1 到 n n n 的一个排列。

如图,图中的 y 1 = 1 y_1=1 y1=1, y 2 = 5 y_2=5 y2=5, y 3 = 3 y_3=3 y3=3, y 4 = 2 y_4=2 y4=2, y 5 = 4 y_5=4 y5=4。

西部 314 打算研究这幅壁画中包含着多少个图腾,其中 V 图腾的定义如下(注意:图腾的形式只和这三个纵坐标的相对大小排列顺序有关) 1 ≤ i < j < k ≤ n 1\le i<j<k\le n 1≤i<j<k≤n 且 y i > y j y_i>y_j yi>yj, y j < y k y_j<y_k yj<yk;

而崇拜 的部落的图腾被定义为 1 ≤ i < j < k ≤ n 1\le i<j<k\le n 1≤i<j<k≤n 且 y i < y j y_i<y_j yi<yj, y j > y k y_j>y_k yj>yk;

西部 314 想知道,这 n n n 个点中两个部落图腾的数目。因此,你需要编写一个程序来求出 V 的个数和 的个数。

输入

第一行一个正整数 n n n;

第二行是 n n n 个正整数,分别代表 y 1 , y 2 , ⋯   , y n y_1,y_2,\cdots,y_n y1,y2,⋯,yn​。

n ≤ 200000 n\le 200000 n≤200000,答案不超过 2 63 − 1 2^{63} - 1 263−1。

输出

输出两个数,中间用空格隔开,依次为 V 的个数和 的个数

样例输入 #1

复制代码
5
1 5 3 2 4

样例输出 #1

复制代码
3 4

思路

对V字形图腾,只需要知道每个点前后各有多少个比它高的 ; ∧ 字形反之 对 \text{V} 字形图腾,只需要知道每个点前后各有多少个比它高的; ∧ 字形反之 对V字形图腾,只需要知道每个点前后各有多少个比它高的;∧字形反之

朴素思路是枚举到第 i 个点,再一一枚举前后比它高的元素 , 朴素思路是枚举到第i个点,再一一枚举前后比它高的元素, 朴素思路是枚举到第i个点,再一一枚举前后比它高的元素,
复杂度 O ( n 2 ) ∼ 1 0 10 , 需要优化 复杂度O(n^2)\sim 10^{10},需要优化 复杂度O(n2)∼1010,需要优化

想象有这么一个 1 ∼ n 的数轴,从左往右一一读取每个点 , 读完一个点就在数轴上标记 1 想象有这么一个1 \sim n的数轴,从左往右一一读取每个点,读完一个点就在数轴上标记1 想象有这么一个1∼n的数轴,从左往右一一读取每个点,读完一个点就在数轴上标记1

则每个点左边比自己高的点数量其实就是前缀和 s u m ( n ) − s u m ( y ) 则每个点左边比自己高的点数量其实就是前缀和sum(n) - sum(y) 则每个点左边比自己高的点数量其实就是前缀和sum(n)−sum(y)

而右边的只需从右往左一一读取再来一遍就行 而右边的只需从右往左一一读取再来一遍就行 而右边的只需从右往左一一读取再来一遍就行

然而这个前缀数组需要一直修改 然而这个前缀数组需要一直修改 然而这个前缀数组需要一直修改
尽管我们可以用 O ( 1 ) 的时间读出高点 , 但是需要 O ( n ) 的时间去维护它 尽管我们可以用O(1)的时间读出高点,但是需要O(n)的时间去维护它 尽管我们可以用O(1)的时间读出高点,但是需要O(n)的时间去维护它

复杂度仍旧是 O ( n 2 ) , 这就让我们想到了树状数组 → 更新和求和复杂度均为 O ( l o g n ) 复杂度仍旧是O(n^2),这就让我们想到了树状数组 \to 更新和求和复杂度均为O(logn) 复杂度仍旧是O(n2),这就让我们想到了树状数组→更新和求和复杂度均为O(logn)

树状数组优化后复杂度变为 O ( n l o g n ) , 满足要求 树状数组优化后复杂度变为O(nlogn),满足要求 树状数组优化后复杂度变为O(nlogn),满足要求

代码

cpp 复制代码
// 利用树状数组存储某个数左/右 大于/小于它自己的数的数量
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

typedef long long LL;

const int N = 2e5 + 10;

int n;
int a[N];
int tr[N];
int gre[N], low[N]; // 存储
LL res1, res2;

inline int lowbit(int x)
{
    return x & (-x);
}

inline int sum(int x)
{
    // 求前x项的和
    if (!x) return 0;
    return tr[x] + sum(x - lowbit(x));
}

inline void add(int x, int c)
{
    for (int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}

int main()
{
    scanf("%d", &n);
    
    for (int i = 1; i <= n; i ++ )
        scanf("%d", &a[i]);
        
    // 从左到右来一遍
    for (int i = 1; i <= n; i ++ )
    {
        int y = a[i];
        gre[i] = sum(n) - sum(y); // 统计在i左边y + 1到n的数的数量
        low[i] = sum(y - 1); // 统计在i左边1到y - 1的数的数量
        add(y, 1); // 插入这个数
    }
    
    memset(tr, 0, sizeof tr);
    
    // 从右到左再来一遍
    for (int i = n; i; i --)
    {
        int y = a[i];
        res1 += (LL)gre[i] * (sum(n) - sum(y));
        res2 += (LL)low[i] * (sum(y - 1));
        add(y, 1);
    }
        
    printf("%lld %lld", res1, res2);
    
    return 0;
    
}

:::

POJ 2182 迷路的奶牛 ⇒ 从后往前慢慢确定每头牛高度 , 树状数组前缀和 + 二分 \Rightarrow 从后往前慢慢确定每头牛高度,树状数组前缀和+二分 ⇒从后往前慢慢确定每头牛高度,树状数组前缀和+二分

POJ 3468 A Simple Problem with Integers ⇒ 维护两个前缀和数组的树状数组 \Rightarrow 维护两个前缀和数组的树状数组 ⇒维护两个前缀和数组的树状数组

相关推荐
iFulling43 分钟前
【数据结构】第八章:排序
数据结构·算法
一只鱼^_1 小时前
力扣第448场周赛
数据结构·c++·算法·leetcode·数学建模·动态规划·迭代加深
良木林1 小时前
数据结构小扫尾——栈
c语言·数据结构
Magnum Lehar1 小时前
GameEditor的Platform的核心实现
数据结构
白露秋483 小时前
数据结构——算法复杂度
数据结构·算法·哈希算法
冉佳驹3 小时前
C语言 ——— 函数
c语言·数据结构·学习·递归·函数·嵌套调用·链式访问
?!7145 小时前
数据结构之哈夫曼树
c语言·数据结构·c++·算法
xin007hoyo5 小时前
算法笔记.约数个数
数据结构·笔记·算法
CodeWithMe6 小时前
【中间件】bthread_数据结构_学习笔记
数据结构·学习·中间件