c++树形数据结构——树状数组,算法必看哟!!!

c++树形数据结构——树状数组,算法必看哟!!!

目录

一,简介

二,区分与前缀和的区别和联系

三,基本步骤演示

1,lowbit操作

2,lowbit和树状数组t[]的联系

1,update函数

2,getprefix函数

四,例题详解

例题1:蓝桥杯官网——殷老师排队

问题描述

输入格式

输出格式

样例输入

样例输出

数据规模

代码详解!

方法一:正确方法,树状数组

方法二,普通前缀和差分方法,时间复杂度高

例题2:23年蓝桥杯真题——异或和

问题描述

输入格式

输出格式

样例输入

样例输出

评测用例规模与约定

代码详解!

方法一:树状数组

方法2:更加简单直观的方法

注:本文题目均来自蓝桥杯官网公开题目,仅用于算法学习和技术讨论。


一,简介

树状数组(Fenwick Tree)是一种高效的数据结构,主要用于解决区间查询单点更新问题,时间复杂度均为 O(logn),适用于数组长度固定且需要频繁进行这两种操作的场景。是一种可以“动态求区间和”的树形数据结构,但并没有真正的构造出边来,所以是“树状的”。

以下是其核心知识点:
1. 基本原理
(1)结构特点:树状数组通过将数组元素按照二进制低位的 “1” 进行分层存储,形成一个树形结构(非传统二叉树),每个节点负责存储特定区间的前缀和。
2,核心操作:
(1)单点更新:修改某个元素的值,并更新其所有相关父节点。
(2)前缀和查询:计算从数组第一个元素到指定位置的前缀和,进而推导出区间和。

二,区分与前缀和的区别和联系

1. 时间复杂度
操作    前缀和    树状数组
初始化    O(n)  O(nlogn)或 O(n)
单点修改O(n)     O(logn)
区间查询O(1)     O(logn)
区间修改O(n)(需配合差分) O(logn)
2. 空间复杂度
前缀和:
O(n) (需要额外存储前缀和数组)。
树状数组:
O(n) (直接在原数组基础上维护树状结构)。
3. 适用场景
前缀和:适合静态数组(不涉及修改),频繁查询区间和。
例如:多次查询数组中任意子数组的和。
树状数组:适合动态数组(需频繁修改和查询)。
例如:单点修改后快速查询前缀和、区间修改与查询(需配合差分)。
4. 实现灵活性
前缀和:仅支持区间和查询,难以扩展到其他操作(如区间最大值)。
树状数组:可扩展支持多种操作,如区间最大值 / 最小值(需调整更新策略)。

三,基本步骤演示

1,lowbit操作

是一种位运算操作,用于计算出数字的二进制表达中的最低位的1以及后面所有的0
int lowbit(int x){return x&-x}; o(1)

int lowbit(int x)//用于快速计算一个整数在二进制表示下最低位的 1 所对应的值 { return x & -x; } /*x = 6 → 二进制: 0000 0110 ~x → 二进制: 1111 1001 ~x + 1 = -x → 二进制: 1111 1010 x & (-x) → 二进制: 0000 0010 → 十进制: 2*/ //lowbit(x) 的值等于 2^k,其中 k 是 x 的二进制末尾连续 0 的个数

2,lowbit和树状数组t[]的联系

 int t[](tree),大小和我们需要维护的数组大小一样即可
 树状数组的结构:t[i]存储a[i]中一段区间和:
 定义t[i]存储以i结尾,且区间大小为lowbit(i)的区间和
 t[j]=sigma(j=(i-lowbit(i)+1)~~~i)a[j],可以将[i-lowbit(i)+1,i]称为“管辖区间”
 例子:a1 a2 a3 a4.....................an(分别为1 2 3 4 ..........n)
 t1-->1 t2-->[1,2]  t3-->3 t4-->[1,4] t5-->5 t6-->[5,6] t7-->7 t8-->[1,8]
 如何进行单点修改?举个例子:假设要修改a[3],包含a[3]的区间有t3 t4 t8,所以应该修改这3个节点
 只需进行+lowbit操作即可(二进制性质)
 3+lowbit(3)=4  4+lowbit(4)=8......就是原来的编号加上他所管辖的区域等于后面与之相连的编号!!!
 怎么进行区间查询?
 第一步:将其拆为两个区间的差,例子:假设要查询[3,7]的和,就要拆分为sum[1,7]-sum[1,2]
 由上图,我们要求sum[1,7],结果为t[7]+t[6]+t[4],通过lowbit即可得到:
 7-lowbit(7)=6  6-lowbit(6)=4  就是原来的编号减去他所管辖的区域等于前面与之相连的编号!!!
 于是可以直接得到树状数组最重要的两个函数update()和getpre()!

1,update函数

//给a[k]增加x void update(int k, int x) { for (int i = k; i <= n; i += lowbit(i))t[i] += x; //解释:把a[k]增加x,相当于把与a[k]相关的几个区间和都要加上x。所以t[i]要加上x }

2,getprefix函数

//返回区间[1,k]的和 int getprefix(int k) { int res = 0; for (int i = k; i >=1; i -= lowbit(i))res += t[i]; //以管辖区间为单位依次向前加上t[i]!!!就返回的是区间[1,k]的和! }

四,例题详解

例题1:蓝桥杯官网——殷老师排队

问题描述

体育课上,殷老师正在给学生们排队,排完队后他发现这些学生有些不同。每个学生都有一个魅力值 ai​ 和愉悦值 bi​,愉悦值的计算公式如下:

图片描述

作为一名优秀的教师他一眼就可以看出学生们的魅力值,但随着时间的推移魅力值可能会发生变化,所以愉悦值也会发生变化。在排队的过程中会有 n 个事件,这 n 个事件共分为两种情况:

  1. 第 x 个学生的魅力值变为 y。
  2. 殷老师想查看第 x 个学生的愉悦值。

殷老师太忙了,所以你可以帮他计算某个学生的愉悦值吗?

输入格式

第一行包含两个整数 n,m,表示学生的个数和事件的个数。

第二行包含 n 个数表示每个学生的魅力值。

之后每一个操作的开头都输入一个数字 op 表示事件的类型,如果 op=1 则输入两个整数 x,z 表示将第 x 个学生的魅力值修改为 z;若 op=2 则输入一个整数 x 表示询问从第 x 个学生的愉悦值。

输出格式

对于每个操作 2 输出一个数字 ans 表示学生的愉悦值。

样例输入

10 10 74 56 25 77 55 36 50 89 42 15 1 2 38 2 8 2 1 2 1 1 3 11 2 1 2 6 2 8 1 5 47 2 3 

样例输出

147 -239 -239 -253 -23 161 189 

数据规模

对于所有评测数据,1≤n,m≤10^5,1≤op≤2,1≤x≤n,0≤z≤10^9。

代码详解!

方法一:正确方法,树状数组
#include <iostream> using namespace std; using ll = long long; const int N = 1e5 + 9; ll a[N], t[N], n, m; //首先,可以将式子转换: //bi=(i-1-1+1)ai-sigma(j=1 j=i-1)aj+sigma(j=i+1 j=n)aj-(n-i-1+1)ai //bi=(2i-n-1)ai-sigma(j=1 j=i-1)aj+sigma(j=i+1 j=n)aj int lowbit(int x) { return x & -x; } void update(int k, ll x)//单点修改,将a[k]加上x { a[k] += x; for (int i = k; i <= n; i += lowbit(i))t[i] += x; } ll getprefix(int k)//求区间[1,k]的和 { ll res = 0; for (int i = k; i >= 1; i -= lowbit(i))res += t[i]; return res; } ll getsum(int l, int r)//求区间[l,r]的和 { return getprefix(r) - getprefix(l - 1); } ll b(int i)//求愉悦值 { return (2 * i - n - 1) * a[i] - getsum(1, i - 1) + getsum(i + 1, n); } int main() { ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); cin >> n >> m; for (int i = 1; i <= n; i++) { int x; cin >> x; update(i, x); //cin>>a[i];update(i,a[i]); } while (m--) { int op; cin >> op; if (op == 1) { ll x, y; cin >> x >> y; update(x, -a[x] + y);//将a[x]加上-a[x]+y,即将a[x]变成y } else { int x; cin >> x; cout << b(x) << '\n'; } } return 0; } ///若改用直接输入数组的形式,则太麻烦了,还不如前者方便 #include <iostream> using namespace std; using ll = long long; const int N = 1e5 + 9; ll a[N], t[N], n, m, g[N]; //首先,可以将式子转换: //bi=(i-1-1+1)ai-sigma(j=1 j=i-1)aj+sigma(j=i+1 j=n)aj-(n-i-1+1)ai //bi=(2i-n-1)ai-sigma(j=1 j=i-1)aj+sigma(j=i+1 j=n)aj int lowbit(int x) { return x & -x; } void update(int k, ll x) // 单点修改,将a[k]设为x { g[k] += x; // 更新a[k] for (int i = k; i <= n; i += lowbit(i)) t[i] += x; // 更新树状数组 } ll getprefix(int k)//求区间[1,k]的和 { ll res = 0; for (int i = k; i >= 1; i -= lowbit(i))res += t[i]; return res; } ll getsum(int l, int r)//求区间[l,r]的和 { return getprefix(r) - getprefix(l - 1); } ll b(int i)//求愉悦值 { return (2 * i - n - 1) * g[i] - getsum(1, i - 1) + getsum(i + 1, n); } int main() { ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); cin >> n >> m; for (int i = 1; i <= n; i++) { //int x;cin>>x; //update(i,x); cin >> a[i]; update(i, a[i]); } while (m--) { int op; cin >> op; if (op == 1) { ll x, y; cin >> x >> y; update(x, y - g[x]);//将a[x]加上-a[x]+y,即将a[x]变成y } else { int x; cin >> x; cout << b(x) << '\n'; } } return 0; }

大家可能会好奇,树状数组的相关操作和前缀和差分具有相似之处。那么这题可以用普通的前缀和和差分方法做吗?其实是可以的。只是时间复杂度较大,只能通过80%的案例。

请看代码:

方法二,普通前缀和差分方法,时间复杂度高
#include <iostream> #include<cstring> using namespace std; using ll = long long; const int N = 1e5 + 9; ll a[N], diff[N], prefix[N], n; //求前缀和 void pre_fix() { memset(prefix, 0, sizeof(prefix)); for (int i = 1; i <= n; i++) prefix[i] = prefix[i - 1] + a[i]; } //update(i,k)将a[i]加上k void update(int i, int k) { diff[i] += k; diff[i + 1] -= k; //前缀和还原 for (int i = 1; i <= n; i++) a[i] = a[i - 1] + diff[i]; pre_fix(); } //前缀和:求[1,k]的和 ll getprefix(int k) { return prefix[k]; } ll getsum(int l, int r) { return getprefix(r) - getprefix(l - 1); } ll b(int i) { return (2 * i - n - 1) * a[i] - getsum(1, i - 1) + getsum(i + 1, n); } int main() { ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int m; cin >> n >> m; for (int i = 1; i <= n; i++) cin >> a[i]; //先初始差分数组: for (int i = 1; i <= n; i++) diff[i] = a[i] - a[i - 1]; //初始前缀和数组 for (int i = 1; i <= n; i++) prefix[i] = prefix[i - 1] + a[i]; while (m--) { int op; cin >> op; if (op == 1) { int x, y; cin >> x >> y; update(x, y - a[x]); } else if (op == 2) { int x; cin >> x; cout << b(x) << '\n'; } } return 0; }

例题2:23年蓝桥杯真题——异或和

问题描述

给一棵含有 n 个结点的有根树,根结点为 1,编号为 i 的点有点权 ai​(i∈[1,n])。现在有两种操作,格式如下:

  • 1 x y:该操作表示将点 x 的点权改为 y。
  • 2 x:该操作表示查询以结点 x 为根的子树内的所有点的点权的异或和。

现有长度为 m 的操作序列,请对于每个第二类操作给出正确的结果。

输入格式

输入的第一行包含两个正整数 n,m,用一个空格分隔。

第二行包含 n 个整数 a1​,a2​,…,an​,相邻整数之间使用一个空格分隔。

接下来 n−1 行,每行包含两个正整数 ui​,vi​,表示结点 ui​ 和 vi​ 之间有一条边。

接下来 m 行,每行包含一个操作。

输出格式

输出若干行,每行对应一个查询操作的答案。

样例输入

4 4 1 2 3 4 1 2 1 3 2 4 2 1 1 1 0 2 1 2 2 

样例输出

4 5 6 

评测用例规模与约定

对于 30% 的评测用例,n,m≤1000;

对于所有评测用例,1≤n,m≤100000,0≤ai​,y≤100000,1≤ui​,vi​,x≤n。

代码详解!

方法一:树状数组
#include <iostream> #include <vector> using ll=long long; using namespace std; const int N=1e5+9; ll a[N],t[N],idx[N],dfn[N],sz[N]; vector<int>g[N]; int n,m,tot=0; int lowbit(int x) { return x & -x; } void update(int k,int x) { for(int i=k;i<=n;i+=lowbit(i)) t[i]^=x; } int query(int k) { int res=0; for(int i=k;i>0;i-=lowbit(i)) res^=t[i]; return res; } void dfs(int x,int fa) { dfn[x]=++tot; idx[dfn[x]]=x; sz[x]=1; for(const auto&y:g[x]) { if(y==fa) continue; dfs(y,x); sz[x]+=sz[y]; } } int main() { int q; cin >> n >> q; for (int i = 1; i <= n; i++)cin >> a[i]; for (int i = 1; i < n; i++) { int u, v; cin >> u >> v; g[u].push_back(v), g[v].push_back(u); } dfs(1, 0); for (int i = 1; i <= n; i++)update(dfn[i], a[i]); while (q--) { int op; cin >> op; if (op == 1)//更新x为y { int x, y; cin >> x >> y; int val = query(dfn[x]) ^ query(dfn[x] - 1); //在初始状态(未进行任何更新操作时),query(dfn[x]) ^ query(dfn[x] - 1) 等于 a[x]; // 但在经过更新操作后,它等于节点 x 当前的最新值(而非初始的 a[x])!!!!!! //val 表示的是 DFS 序中某一点的当前点权值 /*query(dfn[x]) 返回区间 [1, dfn[x]] 的异或和。 query(dfn[x]-1) 返回区间 [1, dfn[x]-1] 的异或和。 两者异或后,得到的 val 就是位置 dfn[x] 对应的点权(即节点 x 的当前值)。*/ update(dfn[x], val ^ y);//这一步的意思就是将dfn[x]所代表的点权(就是val)在和val^y进行异或运算 //val^vla^y=0^y=y,至此,val被替换成y!!! } else//求异或和 { int x; cin >> x; int l = dfn[x], r = dfn[x] + sz[x] - 1; int ans = query(r) ^ query(l - 1);//类似prefix //ans表示l到r的异或和 /*异或(XOR,符号为 ^)具有以下关键性质: 交换律:a ^ b = b ^ a 结合律:(a ^ b) ^ c = a ^ (b ^ c) 自反性:a ^ a = 0(任何数异或自身为 0) 单位元:a ^ 0 = a(任何数异或 0 等于自身)*/ //定义 prefix[i] 为前 i 个元素的异或和: //prefix[i] = a[1] ^ a[2] ^ ... ^ a[i] //则区间 [l, r] 的异或和可表示为: //a[l] ^ a[l+1] ^ ... ^ a[r] = prefix[r] ^ prefix[l-1] //前面的1到l-1部分由交换律a[i]^a[i]=0,都抵消了 cout << ans << '\n'; } } return 0; }
方法2:更加简单直观的方法
//////方法2:更加直观 //需要设置两个数组:in_[N]和out[N],in_[i]表示第i个结点的入序,out[i]表示第i个结点的出序 //对树进行一次dfs,在进入函数时更新入序,在退出函数时更新出序 //对于以结点x为根结点的子树,其所有子结点的dfs序都将位于[ in_[x],out[x] ]间 #include <bits/stdc++.h> using namespace std; const int N = 1e5 + 10; vector<int>edge[N];//edge[i]存储第i个结点的邻接点 int in_[N], out[N];//in_[i]存储第i个结点的入序,out[i]存储第i个结点的出序 int weight[N];//weight[i]存储的第i个结点的值(原始数据) int tot = 0;//记录dfs序的变量 int seq[N];//seq[i]为dfs序中第i个结点的权值 int n, m; void dfs(int u, int father)//对结点u进行dfs,其父结点为father { in_[u] = ++tot;//进入函数,计算入序,tot先自加 seq[tot] = weight[u];//第tot个结点的权值是weight[u] for (const auto& v : edge[u])//遍历u的邻接点v { if (v == father)continue; dfs(v, u); } out[u] = tot;//退出函数,更新u的出序,tot无需自加 } /*in_[u]:表示节点u在 DFS 遍历中首次被访问的时间戳(即进入节点u的时间)。 这个时间戳是唯一的,并且在整个 DFS 过程中严格递增。 out[u]:表示节点u的所有子树都被遍历完成后,退出节点u的时间戳。 由于 DFS 会递归遍历当前节点的所有子节点, 因此out[u]实际上等于当前节点及其所有子树的节点总数(从in_[u]开始的连续区间)。*/ /*入序(in_):在递归进入节点u时立即记录,确保每个节点的入序唯一且递增。 出序(out):在遍历完所有子节点后记录,此时的tot值恰好等于当前子树的最后一个节点的入序, 因此out[u]表示子树区间的右端点。*/ int main() { scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++)scanf("%d", &weight[i]);//输入n个结点的原始值 for (int i = 1; i < n; i++)//输入n-1条无向边 { int u, v; scanf("%d%d", &u, &v); edge[u].push_back(v); edge[v].push_back(u); } dfs(1, 0);//从1号结点开始进行一次dfs,求出所有结点的dfs序,填写in_数组和out数组 //此后我们只按照dfs序处理结点,原始的weight不再使用 while (m--) { int op, x, y; scanf("%d", &op); if (op == 1)//把第x个结点的权值改为y { scanf("%d%d", &x, &y); seq[in_[x]] = y;//转化为将第in_[x]个结点(dfs序下)的权值改为y } else if (op == 2) { scanf("%d", &x);//查询以x为根的子树的异或和 int ans = 0; for (int i = in_[x]; i <= out[x]; i++)//转化为求区间[ in_[x],out[x] ]的异或和 { ans = ans ^ seq[i]; } printf("%d\n", ans); //注意这里使用的是暴力求前缀和的方式而不是前缀和数组 //这是因为本题是一边修改一边查询,前缀和数组无法实现动态修改和查询 //如果要提高效率可以使用线段树,可在O(logn)时间复杂度内完成查询 //但对于本题,暴力求异或和即可通过全部测试点 } } return 0; }

okkkkkkkk了,制作不易,如果觉得不错,不要忘了点赞+关注+收藏哟!!!

Read more

【c++篇】:深入剖析vector--模拟实现属于自己的c++动态数组

【c++篇】:深入剖析vector--模拟实现属于自己的c++动态数组

✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨ ✨ 个人主页:余辉zmh–ZEEKLOG博客 ✨文章所属专栏:c++篇–ZEEKLOG博客 文章目录 * 前言 * 一.`vector`类的默认成员函数 * 整体框架 * 构造函数 * 析构函数 * 拷贝构造函数 * 赋值运算符重载函数 * 测试 * 二.`vector`类的访问和迭代器相关函数 * 访问函数 * 迭代器函数 * 测试 * 三.`vector`类的容量相关函数 * 容量大小函数 * 扩容函数 * 测试 * 四.`vector`类的修改相关函数 * 插入函数 * 删除函数 * 测试 * 五.迭代器失效问题 * 迭代器失效的原因 * 避免迭代器失效的方法 * 示例代码分析 * 总结 * 六.完整代码文件 * `vector.h`文件

By Ne0inhk
Java项目:Java脚手架项目的意义和环境搭建(一)

Java项目:Java脚手架项目的意义和环境搭建(一)

文章目录 * 前言 * 1、目的和意义 * 2、前置知识 * 一、框架 * 二、建立Git仓库并建立分支 * 三、环境的搭建 * 1. 创建后端工程 * 2. 中间件的部署 * 2.1 初始化数据库 * 2.2 配置 nacos 的配置 * 2.3 配置 redis 的配置 * 2.4 docker-compose的容器化编排 * 2.5 环境搭建 * 结果 * END 鸡汤: ● 世界或许嘈杂,但你的节奏独一无二。慢慢来,你奔赴的远方,正因你的坚持而闪闪发光。 ● 累了就深呼吸,对自己说:“我已经做得很好了。” 你值得被温柔以待,包括被你自己。 前言 1、

By Ne0inhk
你真的理解Java SPI吗?从源码到实战的深度思考 [特殊字符]

你真的理解Java SPI吗?从源码到实战的深度思考 [特殊字符]

目录 * 前言:重新认识SPI * 核心思考一:SPI的本质是什么? * 核心思考二:ServiceLoader的优与劣 * 核心思考三:Dubbo如何优化SPI? * 核心思考四:实战中的坑与最佳实践 * 总结与后续计划 前言:重新认识SPI 这篇文章《Java SPI机制初探》来自得物技术团队,系统介绍了Java SPI的概念、原理以及在JDBC、Spring、Dubbo等框架中的应用。文章从SPI的基础概念出发,深入分析了ServiceLoader的源码实现,并结合实际场景讲解了SPI的优缺点和解决方案。 说实话,SPI这个名词一直出现在我耳边,但从未真正了解过。这次正好借着这篇文章来学习一下,看看和自己印象中的是否一致。看完之后,发现SPI其实没有我想象中那么复杂,但背后的设计思想确实值得深入思考。 核心思考一:SPI的本质是什么? API vs SPI:控制权的反转 文章开篇就对比了API和SPI的区别,这个对比让我对SPI有了更清晰的认识: * API:接口实现方同时负责接口定义和接口实现,接口控制权在服务提供方 * SPI:服务调用方负

By Ne0inhk
Java外功基础(1)——Spring Web MVC

Java外功基础(1)——Spring Web MVC

1.前置知识 1.1 Tomcat 定义:Tomcat是一个开源的轻量级Web(Http)服务器和Servlet容器。它实现了Java Servlet等Java EE规范的核心功能,常用于部署和运行Java Web应用程序 。换言之,Tomcat就是一个严格遵循Servlet规范开发出来的、可以独立安装和运行的Java Web服务器/Servlet容器核心功能:Servlet容器:支持Servlet的执行,处理HTTP请求和响应Web服务器:提供静态资源(如HTML)的访问能力,支持基本的HTTP服务安装与版本对应: tomcat官网:Apache Tomcat®目录结构:bin:存放可执行文件,如startup.batconf:存放配置文件lib:存放Tomcat运行所需的jar文件logs:存储日志文件temp:存放临时文件,如上传的文件或缓存数据webapps:默认web应用部署目录work:服务器的工作目录,存放运行时生成的临时文件(编译文件) 1.2 Servlet 1.2.1 定义

By Ne0inhk