当前位置: 首页 > news >正文

数据结构--树状数组

树状数组(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+2ik1+2ik2++2i1

其中 i k > i k − 1 > i k − 2 > ⋯ > i 1 \text{其中}i_k>i_{k-1}>i_{k-2}>\cdots>i_1 其中ik>ik1>ik2>>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} (x2i12i2,x2i1]长度 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} (x2i12i22i3,x2i12i2]长度 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=x2i1+1xa[i]+i=x2i12i2+1x2i1a[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[x2i1]++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[xlowbit(x)]+tr[(xlowbit(x))lowbit(xlowbit(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)

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

int lowbit(int x)
{// 取出x的最后一位1return x & -x;
}

2.添加

复杂度:O(logn)

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

3.前缀和

复杂度:O(logn)

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 y1yn 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 1i<j<kn 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 1i<j<kn 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 n200000,答案不超过 2 63 − 1 2^{63} - 1 2631

输出

输出两个数,中间用空格隔开,依次为 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 想象有这么一个1n的数轴,从左往右一一读取每个点,读完一个点就在数轴上标记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),满足要求

代码

// 利用树状数组存储某个数左/右 大于/小于它自己的数的数量
#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 维护两个前缀和数组的树状数组 维护两个前缀和数组的树状数组

相关文章:

  • opencv的contours
  • ABC404G 题解
  • 数据结构(4) 堆
  • Terraform 中的 external 数据块是什么?如何使用?
  • 软考-软件设计师中级备考 12、软件工程
  • Java 中使用 Callable 创建线程的方法
  • 【办公类-99-04】20250504闵豆统计表excle转PDF,合并PDF、添加中文字体页眉+边框下划线
  • postgresql数据库基本操作
  • JVM happens-before 原则有哪些?
  • 数字信号处理学习笔记--Chapter 1 离散时间信号与系统
  • AndroidLogger常用命令和搜索功能介绍
  • ESP32S3 多固件烧录方法、合并多个固件为单一固件方法
  • C语言实现数据结构:堆排序和二叉树_链式
  • 小土堆pytorch--tensorboard的使用
  • AI日报 · 2025年5月04日|Hugging Face 启动 MCP 全球创新挑战赛
  • 位置权限关掉还能看到IP属地吗?全面解析定位与IP的关系
  • nextjs+supabase vercel部署失败
  • 2025年第十六届蓝桥杯省赛B组Java题解【完整、易懂版】
  • GTID(全局事务标识符)的深入解析
  • better_fbx 下载
  • 专访|刘伟强:在《水饺皇后》里,我放进儿时全家福照片
  • 张求会谈陈寅恪的生前身后事
  • 日本来信|劳动者的书信④
  • 上海环球马术冠军赛开赛,一场体育与假日消费联动的狂欢
  • 永辉超市回应顾客结算时被“反向抹零”:整改并补偿
  • 马上评|启动最高层级医政调查,维护医学一方净土