目录
动态求连续区间和
给定 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 范围内。输入样例:
10 5 1 2 3 4 5 6 7 8 9 10 1 1 5 0 1 3 0 4 8 1 7 5 0 4 8
输出样例:
11 30 35
对于求区间问题,最先想到的应该是前缀和
- 建立一个前缀和数组
- 区间查询: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运算为基础,构建一个树状数组
- 编辑
-
元素修改
void add(int x,int w){ for(int i = x;i <= n;i += lowbit(i)){ t[i] += w; } }
-
区间查询
int query(int u){ int res = 0; for(int i = u;i > 0;i -= lowbit(i)){ res += t[i]; } return res; }
实现这两个功能之后,就可以写出代码了
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5+10;
int t[N];
int n,m;
int lowbit(int x) // 返回末尾的1
{
return x & -x; //正数的反码加一等于补码,即对应负数
}
void add(int x,int w){
for(int i = x;i <= n;i += lowbit(i)){ //修改x~n之内x的父节点的值
t[i] += w;
}
}
int query(int u){
int res = 0;
for(int i = u;i > 0;i -= lowbit(i)){ //找到u之前所有的子节点,输出累加和
res += t[i];
}
return res;
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++ ){
int t;cin >> t;
add(i,t); //初始化数组
}
for(int i = 1;i <= m;i ++){
int k,a,b;cin >> k >> a >> b;
if(k == 1){
add(a,b); //元素修改
}else{
cout << query(b) - query(a-1) << endl;//区间查询(以前缀和为基础)
}
}
return 0;
}
另一种方法——线段树
编辑
- 线段树的本质是一个二叉树,节点上存储的是两个端点和一些属性,所以像线段一样
struct node{ int l,r,sum;//这里存放线段上的元素和 }nodes[N*4];
像普通二叉树一样,对于节点x有
-
左子节点 x*2 x << 1
-
右子节点 x*2+1 x << 1 | 1
- 所以可以通过递归的方式,利用给定的一个数组,建立一个二叉树 build(1,1,n)
void pushup(int u){ nodes[u].sum = nodes[u << 1].sum + nodes[u << 1 | 1].sum; } void build(int u,int l,int r){ if(l == r)nodes[u] = {l,r,a[r]}; //结束:长度为1的线段作为终点 else{ nodes[u] = {l,r}; int mid = l + r >> 1; build(u << 1,l,mid); //左节点递归 build(u << 1 | 1,mid + 1,r); //右节点递归 pushup(u); //传递值 } }
-
元素修改——递归查找到目标长度为1的节点,修改该节点的sum值,再回溯修改其母节点
void change(int u,int x,int w){ if(nodes[u].l == nodes[u].r)nodes[u].sum += w;//找到目标节点 else{ int mid = nodes[u].l + nodes[u].r >> 1; if(mid >= x)add(u << 1,x,w); //类似于二分查找 else add(u << 1 | 1,x,w); pushup(u); //递归回溯修改母节点 } }
- 区间查询
- 设带查找区间为[ L , R ],如果当前节点的两端点包含于[ L , R ],返回当前节点的sum值
- 如果当前节点有在[ L , R ]之外的部分,需要判断
mid = (L + R)/ 2
如果当前节点的区间有在mid左边的部分,就需要递归查询左节点;如果当前节点的区间有在mid右边的部分,就需要递归查询右节点int query(int u,int l,int r){ if(l <= nodes[u].l && nodes[u].r <= r)return nodes[u].sum;//包含于目标区间之内,返回 int mid = nodes[u].l + nodes[u].r >> 1; int sum = 0; if(mid >= l)sum += query(u << 1,l,r); //递归左子节点 if(mid < r)sum += query(u << 1 | 1,l,r); //递归右子节点 return sum; }
完整线段树代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5+10;
int a[N];
int n,m;
struct node{
int l,r,sum;
}nodes[N*4];
void pushup(int u){
nodes[u].sum = nodes[u << 1].sum + nodes[u << 1 | 1].sum;
}
void build(int u,int l,int r){
if(l == r)nodes[u] = {l,r,a[r]};
else{
nodes[u] = {l,r};
int mid = l + r >> 1;
build(u << 1,l,mid);
build(u << 1 | 1,mid + 1,r);
pushup(u);
}
}
int query(int u,int l,int r){
if(l <= nodes[u].l && nodes[u].r <= r)return nodes[u].sum;
int mid = nodes[u].l + nodes[u].r >> 1;
int sum = 0;
if(mid >= l)sum += query(u << 1,l,r);
if(mid < r)sum += query(u << 1 | 1,l,r);
return sum;
}
void add(int u,int x,int w){
if(nodes[u].l == nodes[u].r)nodes[u].sum += w;
else{
int mid = nodes[u].l + nodes[u].r >> 1;
if(mid >= x)add(u << 1,x,w);
else add(u << 1 | 1,x,w);
pushup(u);
}
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++ )cin >> a[i];
build(1,1,n);
int k,a,b;
for(int i = 0;i < m; i++){
cin >> k >> a >> b;
if(k){
add(1,a,b);
}else{
printf("%d\n",query(1,a,b));
}
}
}
另一道与线段树相关的题目:1270. 数列区间最大值 - AcWing题库
数列区间最大值
输入一串数字,给你 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输入样例:
10 2 3 2 4 5 6 8 1 2 9 7 1 4 3 8
输出样例:
5 8
分析:
这个题目和 线段树区间查询、元素修改 基本方法差不多,都是线段树
只需要把节点的 区间和 属性改成 区间最大值 即可
struct node{
int l,r,maxx;//区间最大值
}nodes[N*4];
具体实现:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <climits> //使用INT_MIN需要添加这个库函数
//这里找出的最大值可能有负数
using namespace std;
const int N = 1e5+10;
int a[N];
int n,m;
struct node{
int l,r,maxx;
}nodes[N*4];
void pushup(int u){
nodes[u].maxx = max(nodes[u << 1].maxx , nodes[u << 1 | 1].maxx);//修改了“属性”计算方法
}
void build(int u,int l,int r){//初始化
if(l == r)nodes[u] = {l,r,a[r]};
else{
nodes[u] = {l,r};
int mid = l + r >> 1;
build(u << 1,l,mid);
build(u << 1 | 1,mid + 1,r);
pushup(u);
}
}
int query(int u,int l,int r){//查询方法
if(l <= nodes[u].l && nodes[u].r <= r)return nodes[u].maxx;
int mid = nodes[u].l + nodes[u].r >> 1;
int maxx = INT_MIN;
if(mid >= l)maxx = max(maxx,query(u << 1,l,r));
if(mid < r)maxx = max(maxx,query(u << 1 | 1,l,r));
return maxx;
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++ )scanf("%d",a+i);
build(1,1,n);
int x,y;
for(int i = 0;i < m; i++){
scanf("%d%d", &x, &y);
printf("%d\n",query(1,x,y));
}
}
稍微难理解的一道题:1265. 数星星 - AcWing题库
数星星
天空中有一些星星,这些星星都在不同的位置,每个星星有个坐标。
如果一个星星的左下方(包含正左和正下)有 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输入样例:
5 1 1 5 1 7 1 3 3 5 5
输出样例:
1 2 1 1 0
分析:
- 从题目里可得,星星按 y 坐标增序给出,y 坐标相同的按 x 坐标增序给出,那么每个星星左下角有多少个星星就与后续输入坐标无关了
- 星星坐标从 0 开始,可以调整一下,自加 1 ,然后建立一个树状数组
- 设输入星星横坐标 x ,他的左下角星星数量在树状数组里只等于1~x内有多少个星星,等价于区间查询操作
- 对星星的 x 位置元素修改,+1
编辑
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5+10;
int a[N],cnt[N];
int n,m;
int lowbit(int x)
{
return x & (-x);
}
void add(int x,int w){
for(int i = x;i < N;i += lowbit(i)){//这里读题要注意:坐标并没有在n以内,而是在整个的最大范围内
a[i] += w;
}
}
int query(int u){
int res = 0;
for(int i = u;i > 0;i -= lowbit(i)){
res += a[i];
}
return res;
}
int main()
{
cin >> n;
for(int i = 1;i <= n;i ++){ //要实时计算星星的等级,因为一旦y是递增排序的,所以后来的x不会对之前产生影响
int a,b;cin >> a >> b;
add(a+1,1); //跳过0,因为这个树状数组以0为结束终点
cnt[query(a+1)]++; //前面有多少个星星,就是多少级
}
for (int i = 1; i <= n; i ++ ){ //没有0
cout << cnt[i] << endl; //输出各各等级的数量
}
return 0;
}
第五届蓝桥杯省赛C++B/C组 1215. 小朋友排队 - AcWing题库
小朋友排队
n 个小朋友站成一排。
现在要把他们按身高从低到高的顺序排列,但是每次只能交换位置相邻的两个小朋友。
每个小朋友都有一个不高兴的程度。
开始的时候,所有小朋友的不高兴程度都是 0。
如果某个小朋友第一次被要求交换,则他的不高兴程度增加 1,如果第二次要求他交换,则他的不高兴程度增加 2(即不高兴程度为 3),依次类推。当要求某个小朋友第 kk 次交换时,他的不高兴程度增加 k。
请问,要让所有小朋友按从低到高排队,他们的不高兴程度之和最小是多少。
如果有两个小朋友身高一样,则他们谁站在谁前面是没有关系的。
输入格式
输入的第一行包含一个整数 n,表示小朋友的个数。
第二行包含 n 个整数 H1,H2,…,Hn,分别表示每个小朋友的身高。
输出格式
输出一行,包含一个整数,表示小朋友的不高兴程度和的最小值。
数据范围
1≤n≤100000,
0≤Hi≤1000000输入样例:
3 3 2 1
输出样例:
9
样例解释
首先交换身高为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数组 已经升序,那么任意移动一个元素,计算出来的逆序对数量是符合上述定义的;
如果再移动另一个,也可以推理得出逆序对数量是符合的
如此反复的操作可以得出,逆序对数量的计算是正确的
- 经过上述的分析,我们可以得到暴力做法(显然会超时)
for(int i=1;i<=n;i++){ long long res=0; for(int j=1;j<i;j++) if(a[j]>a[i]) res++;// i 前面比 a[i]高的数量 for(int j=i+1;j<=n;j++) if(a[j]<a[i]) res++;// i 后面比 a[i]低的数量 res=res*res/2; //不高兴值 num[i]=res; } //计算总和 for(int i=1;i<=n;i++) ans+=num[i];
代码实现(树状数组):
//每个小孩的移动次数=前面比它高的人数+后面比他低的人数
//不高兴值就是累加1到移动次数=(移动次数)*(移动次数+1)/2
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10,M = 1e6 + 10;
typedef long long LL;
int n;
int a[N],h[N];
int cnt[M];
int lowbit(int x) // 返回末尾的1
{
return x & -x;
}
void add(int u){
for(int i = u ;i < M;i += lowbit(i)){
cnt[i] += 1;
}
}
int query(int u){
int ans = 0;
for(int i = u; i > 0;i -= lowbit(i)){
ans += cnt[i];
}
return ans;
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i ++ )scanf("%d",a+i),a[i]++;
for(int i = 1;i <= n;i ++){ //注意顺序
h[i] += query(M-1) - query(a[i]);//找前面放进来的,比他高的人数
//cout << h[i] << endl;
add(a[i]); //放入
}
memset(cnt,0,sizeof cnt); //清空
for(int i = n;i >= 1;i --){
h[i] += query(a[i]-1); //找后面放进来的,比他矮的人数
add(a[i]);
}
LL ans = 0; //范围最大n*n
for (int i = 1; i <= n; i ++ )ans += (LL)(h[i] + 1) * h[i]/2;
cout << ans << endl;
return 0;
}
前面的问题都是围绕着区间查询和元素修改进行的,用树状数组和线段树两种方法来解决
元素查询可以通过查询区间长度为1的情况完成
那么对于区间修改该怎么做呢?
243. 一个简单的整数问题2 - AcWing题库下面个问题就是关于区间修改的
一个简单的整数问题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输入样例:
10 5 1 2 3 4 5 6 7 8 9 10 Q 4 4 Q 1 10 Q 2 4 C 3 6 3 Q 2 4
输出样例:
4 55 9 15
分析:
有了前面的经验后,从方便的角度可以想到,运用 差分 来实现数组的区间修改效率最高
建立一个差分数组 a[ i ] = b[ i ] - b[ i - 1 ]
- 先是初始化
for (LL i = 1; i <= n; i ++ ){ cin >> b[i]; a[i] = b[i]-b[i-1]; add(i,a[i]); }
-
关于区间修改的部分,按照差分的思想
区间查询照常for(LL i = 1;i <= m;i ++){ char x;cin >> x; if(x == 'Q'){ LL a,b;cin >> a >> b; cout << query(b) - query(a-1) << endl; }else{ LL a,b,c;cin >> a >> b >> c; add(a, c); add(b+1, -c); } }
- 所以对应着query的实现也要发生改变,此时a数组对应的是差分的树状数组
所以操作为:累加各个元素的差分和LL query(LL u){ LL res = 0; for(LL i = u;i > 0;/*i -= lowbit(i)*/i--){ //累加各个元素的差分和 for(LL j = i;j > 0;j -= lowbit(j)){ res += a[j]; } } return res; }
但是这样的操作显然效率过低——计算了大量的重复差分
所以需要改进
a[1]
a[1]+a[2]
...
...
a[1]+a[2]+..+a[n-1]
a[1]+a[2]+...... + a[n]//转化成一个正方体
a[1]+a[2]+...... + a[n]
a[1]+a[2]+...... + a[n]
...
...(总共n+1行,在最上面添加一行)
a[1]+a[2]+...... + a[n]
a[1]+a[2]+...... + a[n]
计算公式=>
(n+1)(a[1]+a[2]+......+a[n]) - (1*a[1]+2*a[2]+......+n*a[n])
所以我们可以维护两个树状数组
a[ i ]
b[ i ] = i * a[ i ]
代码实现
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
const LL N = 1e5+10;
LL a[N],b[N];
LL n,m;
LL lowbit(LL x) // 返回末尾的1
{
return x & -x;
}
void add(LL x,LL w){
for(LL i = x;i <= n;i += lowbit(i)){ //同时维护两个树状数组
a[i] += w;
b[i] += x*w;
}
}
LL query(LL u){
LL res = 0;
for(LL i = u;i > 0;i -= lowbit(i)){ //优化的区间查询方案
res += (u+1) * a[i] - b[i]; //计算公式
}
return res;
}
//-----------------------------------------------//朴素代码,对于此题超时
/*
void add(LL x,LL w){
for(LL i = x;i <= n;i += lowbit(i)){
a[i] += w;
}
}
LL query(LL u){
LL res = 0;
for(LL i = u;i > 0;i--){
for(LL j = i;j > 0;j -= lowbit(j)){
res += a[j];
}
}
return res;
}
*///------------------------------------------------
int main()
{
cin >> n >> m;
LL t1=0,t2=0;
for (LL i = 1; i <= n; i ++ ){
cin >> t2;
add(i,t2-t1);
t1=t2;
}
for(LL i = 1;i <= m;i ++){
char x;cin >> x;
if(x == 'Q'){
LL a,b;cin >> a >> b;
cout << query(b) - query(a-1) << endl;
}else{
LL a,b,c;cin >> a >> b >> c;
add(a, c);
add(b+1, -c);
}
}
return 0;
}
〔manim | 算法 | 数据结构〕 完全理解并深入应用树状数组 | 支持多种动态维护区间操作_哔哩哔哩_bilibili
树状数组 数据结构详解与模板(可能是最详细的了)_bestsort的博客-CSDN博客_树状数组
感谢阅读!
标签:星星,树状,int,LL,add,数组,操作,include From: https://www.cnblogs.com/cfddfc/p/17065029.html