目录

一,简介

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

三,基本步骤演示

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了,制作不易,如果觉得不错,不要忘了点赞+关注+收藏哟!!!

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐