基础的树状数组、线段树操作(区间求和,单点修改、区间修改)
目录
动态求连续区间和
给定 n 个数组成的一个数列,规定有两种操作,一是修改某个元素,二是求子数列 [a,b][a,b] 的连续和。
输入格式
第一行包含两个整数 n 和 m,分别表示数的个数和操作次数。
第二行包含 n 个整数,表示完整数列。
接下来 m 行,每行包含三个整数 k,a,b (k=0,表示求子数列[a,b]的和;k=1,表示第 a 个数加 b)。
数列从 1 开始计数。
输出格式
输出若干行数字,表示 k=0 时,对应的子数列 [a,b] 的连续和。
数据范围
1≤n≤100000,
1≤m≤100000,
1≤a≤b≤n,
数据保证在任何时候,数列中所有元素之和均在 int 范围内。输入样例:
输出样例:
对于求区间问题,最先想到的应该是前缀和
- 建立一个前缀和数组
- 区间查询:O(1)
- 元素修改:O(n)
显然元素修改可能存在过于复杂的情况
所以用到另一种方法——树状数组
- 区间查询:O(log n)
- 元素修改:O(log n)
在讲具体算法前,要先介绍一个具体的操作
- lowbit运算——找出一个数二进制的最低位1(例如9:1001 -> 1 12:1100 -> 100)
- 设数为x,x按位取反,设为~x
- x 按位与 ~x为0,当x按位与(~x+1)时,得到结果
- 计算机内的数字按照补码规则,负数的补码与对应正数的反码加一相等,所以简化为x & -x
- 以lowbit运算为基础,构建一个树状数组
-
元素修改
-
区间查询
实现这两个功能之后,就可以写出代码了
另一种方法——线段树
- 线段树的本质是一个二叉树,节点上存储的是两个端点和一些属性,所以像线段一样
像普通二叉树一样,对于节点x有
-
左子节点 x*2 x << 1
-
右子节点 x*2+1 x << 1 | 1
- 所以可以通过递归的方式,利用给定的一个数组,建立一个二叉树 build(1,1,n)
-
元素修改——递归查找到目标长度为1的节点,修改该节点的sum值,再回溯修改其母节点
- 区间查询
- 设带查找区间为[ L , R ],如果当前节点的两端点包含于[ L , R ],返回当前节点的sum值
- 如果当前节点有在[ L , R ]之外的部分,需要判断
mid = (L + R)/ 2
如果当前节点的区间有在mid左边的部分,就需要递归查询左节点;如果当前节点的区间有在mid右边的部分,就需要递归查询右节点
完整线段树代码
另一道与线段树相关的题目:
数列区间最大值
输入一串数字,给你 M 个询问,每次询问就给你两个数字 X,Y要求你说出 X 到 Y 这段区间内的最大数。
输入格式
第一行两个整数 N,M 表示数字的个数和要询问的次数;
接下来一行为 N 个数;
接下来 M 行,每行都有两个整数 X,Y。
输出格式
输出共 M 行,每行输出一个数。
数据范围
1≤N≤100000,
1≤M≤1000000,
1≤X≤Y≤N,
数列中的数字均不超过2^31-1输入样例:
输出样例:
分析:
这个题目和 线段树区间查询、元素修改 基本方法差不多,都是线段树
只需要把节点的 区间和 属性改成 区间最大值 即可
具体实现:
稍微难理解的一道题:
数星星
天空中有一些星星,这些星星都在不同的位置,每个星星有个坐标。
如果一个星星的左下方(包含正左和正下)有 k 颗星星,就说这颗星星是 k 级的。
例如,上图中星星 5 是 3 级的(1,2,4 在它左下),星星 2,4是 1 级的。
例图中有 1 个 0 级,2 个 1 级,1 个 2 级,1 个 3 级的星星。
给定星星的位置,输出各级星星的数目。
换句话说,给定 N 个点,定义每个点的等级是在该点左下方(含正左、正下)的点的数目,试统计每个等级有多少个点。
输入格式
第一行一个整数 N,表示星星的数目;
接下来 N 行给出每颗星星的坐标,坐标用两个整数 x,y 表示;
不会有星星重叠。星星按 y 坐标增序给出,y 坐标相同的按 x 坐标增序给出。
输出格式
N 行,每行一个整数,分别是 0 级,1 级,2 级,……,N−1 级的星星的数目。
数据范围
1≤N≤15000,
0≤x,y≤32000输入样例:
输出样例:
分析:
- 从题目里可得,星星按 y 坐标增序给出,y 坐标相同的按 x 坐标增序给出,那么每个星星左下角有多少个星星就与后续输入坐标无关了
- 星星坐标从 0 开始,可以调整一下,自加 1 ,然后建立一个树状数组
- 设输入星星横坐标 x ,他的左下角星星数量在树状数组里只等于1~x内有多少个星星,等价于区间查询操作
- 对星星的 x 位置元素修改,+1
第五届蓝桥杯省赛C++B/C组
小朋友排队
n 个小朋友站成一排。
现在要把他们按身高从低到高的顺序排列,但是每次只能交换位置相邻的两个小朋友。
每个小朋友都有一个不高兴的程度。
开始的时候,所有小朋友的不高兴程度都是 0。
如果某个小朋友第一次被要求交换,则他的不高兴程度增加 1,如果第二次要求他交换,则他的不高兴程度增加 2(即不高兴程度为 3),依次类推。当要求某个小朋友第 kk 次交换时,他的不高兴程度增加 k。
请问,要让所有小朋友按从低到高排队,他们的不高兴程度之和最小是多少。
如果有两个小朋友身高一样,则他们谁站在谁前面是没有关系的。
输入格式
输入的第一行包含一个整数 n,表示小朋友的个数。
第二行包含 n 个整数 H1,H2,…,Hn,分别表示每个小朋友的身高。
输出格式
输出一行,包含一个整数,表示小朋友的不高兴程度和的最小值。
数据范围
1≤n≤100000,
0≤Hi≤1000000输入样例:
输出样例:
样例解释
首先交换身高为3和2的小朋友,再交换身高为3和1的小朋友,再交换身高为2和1的小朋友,每个小朋友的不高兴程度都是3,总和为9。
分析:
- 问题简化:一个有 n 个元素的数组H,以冒泡排序的方式排成升序,每个元素交换(大小相同不交换)的次数cnt[ i ],最后累加cnt[ i ] * cnt [ i ] / 2
- 每个元素要交换的的次数,就是在数组中关于当前元素的逆序对数量
交换次数cnt[ i ] = H[ 0 , i - 1 ]中大于H[ i ]的数量 + H[ i+1 , n ]中小于H[ i ]的数量
- 为什么是逆序对呢?可以从反面来想。(递推)
假设 H数组 已经升序,那么任意移动一个元素,计算出来的逆序对数量是符合上述定义的;
如果再移动另一个,也可以推理得出逆序对数量是符合的
如此反复的操作可以得出,逆序对数量的计算是正确的
- 经过上述的分析,我们可以得到暴力做法(显然会超时)
代码实现(树状数组):
前面的问题都是围绕着区间查询和元素修改进行的,用树状数组和线段树两种方法来解决
元素查询可以通过查询区间长度为1的情况完成
那么对于区间修改该怎么做呢?
一个简单的整数问题2
给定一个长度为 N 的数列 A,以及 M 条指令,每条指令可能是以下两种之一:
C l r d
,表示把 A[l],A[l+1],…,A[r] 都加上 dd。Q l r
,表示询问数列中第 l∼r 个数的和。对于每个询问,输出一个整数表示答案。
输入格式
第一行两个整数 N,M。
第二行 N 个整数 A[i]。
接下来 M 行表示 M 条指令,每条指令的格式如题目描述所示。
输出格式
对于每个询问,输出一个整数表示答案。
每个答案占一行。
数据范围
1≤N,M≤100000
|d|≤10000,
|A[i]|≤1e9输入样例:
输出样例:
分析:
有了前面的经验后,从方便的角度可以想到,运用 差分 来实现数组的区间修改效率最高
建立一个差分数组 a[ i ] = b[ i ] - b[ i - 1 ]
- 先是初始化
-
关于区间修改的部分,按照差分的思想
区间查询照常 - 所以对应着query的实现也要发生改变,此时a数组对应的是差分的树状数组
所以操作为:累加各个元素的差分和
但是这样的操作显然效率过低——计算了大量的重复差分
所以需要改进
所以我们可以维护两个树状数组
a[ i ]
b[ i ] = i * a[ i ]
代码实现
最后贴一下
Code(分块)O(m√n) #include <iostream> #include <cstring> #include <algorithm> #include <cmath> using namespace std; typedef long long LL; const int N = 1e5 + 10, M = 350; int n, m, len; LL sum[M], flag[M]; //sum:块的总和;flag:块的懒标记 int w[N]; int get(int i) { return i / len; } void modify(int l, int r, int d) { if (get(l) == get(r)) //段内直接暴力 { for (int i = l; i <= r; i ++ ) w[i] += d, sum[get(i)] += d; } else { int i = l, j = r; while (get(i) == get(l)) w[i] += d, sum[get(i)] += d, i ++ ; while (get(j) == get(r)) w[j] += d, sum[get(j)] += d, j -- ; for (int k = get(i); k <= get(j); k ++ ) sum[k] += len * d, flag[k] += d; } } LL query(int l, int r) { LL res = 0; if (get(l) == get(r)) //段内直接暴力 { for (int i = l; i <= r; i ++ ) res += w[i] + flag[get(i)]; } else { int i = l, j = r; while (get(i) == get(l)) res += w[i] + flag[get(i)], i ++ ; while (get(j) == get(r)) res += w[j] + flag[get(j)], j -- ; for (int k = get(i); k <= get(j); k ++ ) res += sum[k]; } return res; } int main() { scanf("%d%d", &n, &m); len = sqrt(n); for (int i = 1; i <= n; i ++ ) { scanf("%d", &w[i]); sum[get(i)] += w[i]; } char op[2]; int l, r, d; while (m -- ) { scanf("%s%d%d", op, &l, &r); if (*op == 'C') { scanf("%d", &d); modify(l, r, d); } else printf("%lld\n", query(l, r)); } return 0; } 线段树 O(mlogn) #include <iostream> #include <cstdio> #include <cstring> #include <algorithm> typedef long long LL; const int N = 1e5 + 10; int n, m; int w[N]; struct Node { int l, r; LL sum, add; }tr[N * 4]; void pushup(int u) { tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum; } void pushdown(int u) { auto &root = tr[u], &left = tr[u << 1], &right = tr[u << 1 | 1]; if (root.add) { //传递懒标记,更新子树 left.add += root.add, left.sum += (LL) (left.r - left.l + 1) * root.add; right.add += root.add, right.sum += (LL) (right.r - right.l + 1) * root.add; //删除父结点懒标记 root.add = 0; } } void build(int u, int l, int r) { if (l == r) tr[u] = {l, r, w[l], 0}; else { tr[u] = {l, r}; int mid = l + r >> 1; build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r); pushup(u); } } void modify(int u, int l, int r, int v) { if (l <= tr[u].l && tr[u].r <= r) { tr[u].sum += (tr[u].r - tr[u].l + 1) * v; tr[u].add += v; } else { pushdown(u); int mid = tr[u].l + tr[u].r >> 1; if (l <= mid) modify(u << 1, l, r, v); if (r > mid) modify(u << 1 | 1, l, r, v); pushup(u); } } LL query(int u, int l, int r) { if (l <= tr[u].l && tr[u].r <= r) return tr[u].sum; pushdown(u); int mid = tr[u].l + tr[u].r >> 1; LL v = 0; if (l <= mid) v = query(u << 1, l, r); if (r > mid) v += query(u << 1 | 1, l, r); return v; } int main() { scanf("%d%d", &n, &m); for (int i = 1; i <= n; ++i) scanf("%d", &w[i]); build(1, 1, n); char op[2]; int l, r, t; while (m -- ) { scanf("%s%d%d", op, &l, &r); if (*op == 'Q') printf("%lld\n", query(1, l, r)); else { scanf("%d", &t); modify(1, l, r, t); } } return 0; }
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析