树状数组、线段树
题目链接
P3374 【模板】树状数组 1
P3368 【模板】树状数组 2
P3372 【模板】线段树 1
P3373 【模板】线段树 2
P6242 【模板】线段树 3
P3374 【模板】树状数组 1
题目描述
如题,已知一个数列,你需要进行下面两种操作:
-
将某一个数加上 \(x\)
-
求出某区间每一个数的和
输入格式
第一行包含两个正整数 \(n,m\),分别表示该数列数字的个数和操作的总个数。
第二行包含 \(n\) 个用空格分隔的整数,其中第 \(i\) 个数字表示数列第 \(i\) 项的初始值。
接下来 \(m\) 行每行包含 \(3\) 个整数,表示一个操作,具体如下:
-
1 x k
含义:将第 \(x\) 个数加上 \(k\) -
2 x y
含义:输出区间 \([x,y]\) 内每个数的和
输出格式
输出包含若干行整数,即为所有操作 \(2\) 的结果。
输入
5 5
1 5 4 2 3
1 1 3
2 2 5
1 3 -1
1 4 2
2 1 4
输出
14
16
说明/提示
【数据范围】
对于 \(30\%\) 的数据,\(1 \le n \le 8,1\le m \le 10\);
对于 \(70\%\) 的数据,\(1\le n,m \le 10^4\);
对于 \(100\%\) 的数据,\(1\le n,m \le 5\times 10^5\) 。
- 时间复杂度:\(O(logn)\)
代码
#include<bits/stdc++.h>
using namespace std;
using LL=long long;
const int N=5e5+10;
int n,m;
int a[N],c[N],s[N];
LL ask(int x)
{
LL res=0;
for(;x;x-=x&-x)res+=c[x];
return res;
}
void add(int x,int y)
{
for(;x<=n;x+=x&-x)c[x]+=y;
}
int main()
{
scanf("%d%d",&n,&m);
int x;
for(int i=1;i<=n;i++)
{
scanf("%d",&x);
s[i]=s[i-1]+x;
}
while(m--)
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
if(x==1)add(y,z);
else
printf("%lld\n",s[z]-s[y-1]+ask(z)-ask(y-1));
}
return 0;
}
P3368 【模板】树状数组 2
题目描述
如题,已知一个数列,你需要进行下面两种操作:
-
将某区间每一个数加上 \(x\);
-
求出某一个数的值。
输入格式
第一行包含两个整数 \(N、M\),分别表示该数列数字的个数和操作的总个数。
第二行包含 \(N\) 个用空格分隔的整数,其中第 \(i\) 个数字表示数列第 \(i\) 项的初始值。
接下来 \(M\) 行每行包含 \(2\) 或 \(4\)个整数,表示一个操作,具体如下:
操作 \(1\): 格式:1 x y k
含义:将区间 \([x,y]\) 内每个数加上 \(k\);
操作 \(2\): 格式:2 x
含义:输出第 \(x\) 个数的值。
输出格式
输出包含若干行整数,即为所有操作 \(2\) 的结果。
输入
5 5
1 5 4 2 3
1 2 4 2
2 3
1 1 5 -1
1 3 5 7
2 4
输出
6
10
说明/提示
数据规模与约定
对于 \(30\%\) 的数据:\(N\le8,M\le10\);
对于 \(70\%\) 的数据:\(N\le 10000,M\le10000\);
对于 \(100\%\) 的数据:\(1 \leq N, M\le 500000,1 \leq x, y \leq n\),保证任意时刻序列中任意元素的绝对值都不大于 \(2^{30}\)。
- 时间复杂度:\(O(mlogn)\)
代码
#include<bits/stdc++.h>
using namespace std;
using LL=long long;
const int N=5e5+10;
int n,m;
int a[N],c[N];
LL ask(int x)
{
LL res=0;
for(;x;x-=x&-x)res+=c[x];
return res;
}
void add(int x,int y)
{
for(;x<=n;x+=x&-x)c[x]+=y;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
while(m--)
{
int op,x,y,k;
scanf("%d%d",&op,&x);
if(op==1)
{
scanf("%d%d",&y,&k);
add(x,k);
add(y+1,-k);
}
else
printf("%lld\n",a[x]+ask(x));
}
return 0;
}
P3372 【模板】线段树 1
题目描述
如题,已知一个数列,你需要进行下面两种操作:
- 将某区间每一个数加上 \(k\)。
- 求出某区间每一个数的和。
输入格式
第一行包含两个整数 \(n, m\),分别表示该数列数字的个数和操作的总个数。
第二行包含 \(n\) 个用空格分隔的整数,其中第 \(i\) 个数字表示数列第 \(i\) 项的初始值。
接下来 \(m\) 行每行包含 \(3\) 或 \(4\) 个整数,表示一个操作,具体如下:
1 x y k
:将区间 \([x, y]\) 内每个数加上 \(k\)。2 x y
:输出区间 \([x, y]\) 内每个数的和。
输出格式
输出包含若干行整数,即为所有操作 \(2\) 的结果。
输入
5 5
1 5 4 2 3
2 2 4
1 2 3 2
2 3 4
1 1 5 1
2 1 4
输出
11
8
20
说明/提示
对于 \(30\%\) 的数据:\(n \le 8,m \le 10\)。
对于 \(70\%\) 的数据:\(n \le {10}^3 ,m \le {10}^4\) 。
对于 \(100\%\) 的数据:\(1 \le n, m \le {10}^5\) 。
保证任意时刻数列中任意元素的和在 \([u-2^{63}, 2^{63})\) 内。
树状数组
- 解法
我们不妨用树状数组维护前缀和,\(\sum_{i=1}^xc[i]\) 就是 \(a[x]\) 增加的值,而序列 \(a\) 的前缀和 \(a[1\sim x]\) 增加的值为:
- 时间复杂度:\(O(mlogn)\)
代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
using LL=long long;
LL c[2][N],s[N];
int n,m;
LL ask(int x,int k)
{
LL res=0;
for(;x;x-=x&-x)res+=c[k][x];
return res;
}
void add(int x,int y,int k)
{
for(;x<=n;x+=x&-x)c[k][x]+=y;
}
int main()
{
scanf("%d%d",&n,&m);
LL x;
for(int i=1;i<=n;i++)
{
scanf("%lld",&x);
s[i]=s[i-1]+x;
}
while(m--)
{
int op,x,y;
LL k;
scanf("%d%d%d",&op,&x,&y);
if(op==1)
{
scanf("%lld",&k);
add(x,k,0);
add(y+1,-k,0);
add(x,x*k,1);
add(y+1,-(y+1)*k,1);
}
else
{
LL res=s[y]-s[x-1];
res+=(y+1)*ask(y,0)-ask(y,1)-(x*ask(x-1,0)-ask(x-1,1));
printf("%lld\n",res);
}
}
return 0;
}
线段树(懒标记)
- 时间复杂度:\(O(mlogn)\)
代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
using LL=long long;
struct seg
{
int l,r;
LL sum,add;
#define l(x) tr[x].l
#define r(x) tr[x].r
#define sum(x) tr[x].sum
#define add(x) tr[x].add
}tr[N*4];
int n,m;
int a[N];
void build(int p,int l,int r)
{
l(p)=l,r(p)=r;
if(l==r)sum(p)=a[l];
else
{
int mid=l+r>>1;
build(p*2,l,mid),build(p*2+1,mid+1,r);
sum(p)=sum(p*2)+sum(p*2+1);
}
}
void spread(int p)
{
if(add(p))
{
sum(p*2)+=add(p)*(r(p*2)-l(p*2)+1);
sum(p*2+1)+=add(p)*(r(p*2+1)-l(p*2+1)+1);
add(p*2)+=add(p);
add(p*2+1)+=add(p);
add(p)=0;
}
}
void change(int p,int l,int r,int d)
{
if(l<=l(p)&&r(p)<=r)
{
sum(p)+=1ll*d*(r(p)-l(p)+1);
add(p)+=d;
return ;
}
spread(p);
int mid=l(p)+r(p)>>1;
if(l<=mid)change(p*2,l,r,d);
if(r>mid)change(p*2+1,l,r,d);
sum(p)=sum(p*2)+sum(p*2+1);
}
LL ask(int p,int l,int r)
{
if(l<=l(p)&&r(p)<=r)
return sum(p);
spread(p);
LL res=0;
int mid=l(p)+r(p)>>1;
if(l<=mid)res+=ask(p*2,l,r);
if(r>mid)res+=ask(p*2+1,l,r);
return res;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
build(1,1,n);
while(m--)
{
int op,x,y;
LL k;
scanf("%d%d%d",&op,&x,&y);
if(op==1)
{
scanf("%lld",&k);
change(1,x,y,k);
}
else
printf("%lld\n",ask(1,x,y));
}
return 0;
}
P3373 【模板】线段树 2
题目描述
如题,已知一个数列,你需要进行下面三种操作:
-
将某区间每一个数乘上 \(x\)
-
将某区间每一个数加上 \(x\)
-
求出某区间每一个数的和
输入格式
第一行包含三个整数 \(n,m,p\),分别表示该数列数字的个数、操作的总个数和模数。
第二行包含 \(n\) 个用空格分隔的整数,其中第 \(i\) 个数字表示数列第 \(i\) 项的初始值。
接下来 \(m\) 行每行包含若干个整数,表示一个操作,具体如下:
操作 \(1\): 格式:1 x y k
含义:将区间 \([x,y]\) 内每个数乘上 \(k\)
操作 \(2\): 格式:2 x y k
含义:将区间 \([x,y]\) 内每个数加上 \(k\)
操作 \(3\): 格式:3 x y
含义:输出区间 \([x,y]\) 内每个数的和对 \(p\) 取模所得的结果
输出格式
输出包含若干行整数,即为所有操作 \(3\) 的结果。
输入
5 5 38
1 5 4 2 3
2 1 4 1
3 2 5
1 2 4 2
2 3 5 5
3 1 4
输出
17
2
说明/提示
【数据范围】
对于 \(30\%\) 的数据:\(n \le 8,m \le 10\)
对于 \(70\%\) 的数据:\(n \le 10^3 ,m \le 10^4\)
对于 \(100\%\) 的数据:\(n \le 10^5,m \le 10^5\)
除样例外,\(p = 571373\)
解题思路
本题的关键在于加法和乘法的顺序问题:
- 加法优先级低,不会对前面的操作有影响
- 乘法优先级高,对前面的操作有影响,有三种影响:\(sum\)、懒标记 \(mul\) 和 \(add\)。
所以,采取先乘后加的方式
- 时间复杂度:\(O(mlogn)\)
代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
using LL=long long;
struct seg
{
int l,r;
LL sum,add,mul;
#define l(x) tr[x].l
#define r(x) tr[x].r
#define add(x) tr[x].add
#define mul(x) tr[x].mul
#define sum(x) tr[x].sum
}tr[N*4];
int n,m,mod;
int a[N];
void build(int p,int l,int r)
{
l(p)=l,r(p)=r;
mul(p)=1;
if(l==r)
{
sum(p)=a[l]%mod;
return ;
}
int mid=l+r>>1;
build(p*2,l,mid),build(p*2+1,mid+1,r);
sum(p)=(sum(p*2)+sum(p*2+1))%mod;
}
void spread(int p)
{
sum(p*2)=(sum(p*2)*mul(p)+add(p)*(r(p*2)-l(p*2)+1))%mod;
sum(p*2+1)=(sum(p*2+1)*mul(p)+add(p)*(r(p*2+1)-l(p*2+1)+1))%mod;
add(p*2)=(add(p*2)*mul(p)+add(p))%mod;
add(p*2+1)=(add(p*2+1)*mul(p)+add(p))%mod;
mul(p*2)=mul(p*2)*mul(p)%mod;
mul(p*2+1)=mul(p*2+1)*mul(p)%mod;
add(p)=0;
mul(p)=1;
}
void change1(int p,int l,int r,int d)
{
if(l<=l(p)&&r(p)<=r)
{
add(p)=add(p)*d%mod;
mul(p)=mul(p)*d%mod;
sum(p)=(sum(p)*d)%mod;
return ;
}
spread(p);
int mid=l(p)+r(p)>>1;
if(l<=mid)change1(p*2,l,r,d);
if(r>mid)change1(p*2+1,l,r,d);
sum(p)=(sum(p*2)+sum(p*2+1))%mod;
}
void change2(int p,int l,int r,int d)
{
if(l<=l(p)&&r(p)<=r)
{
add(p)=(add(p)+d)%mod;
sum(p)=(sum(p)+(r(p)-l(p)+1)*d)%mod;
return ;
}
spread(p);
int mid=l(p)+r(p)>>1;
if(l<=mid)change2(p*2,l,r,d);
if(r>mid)change2(p*2+1,l,r,d);
sum(p)=(sum(p*2)+sum(p*2+1))%mod;
}
int ask(int p,int l,int r)
{
if(l<=l(p)&&r(p)<=r)return sum(p);
spread(p);
int res=0;
int mid=l(p)+r(p)>>1;
if(l<=mid)res+=ask(p*2,l,r)%mod;
if(r>mid)res+=ask(p*2+1,l,r);
return res%mod;
}
int main()
{
scanf("%d%d%d",&n,&m,&mod);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
build(1,1,n);
while(m--)
{
int op,x,y,k;
scanf("%d%d%d",&op,&x,&y);
if(op==1)
{
scanf("%d",&k);
change1(1,x,y,k);
}
else if(op==2)
{
scanf("%d",&k);
change2(1,x,y,k);
}
else
printf("%d\n",ask(1,x,y));
}
return 0;
}
P6242 【模板】线段树 3
题目描述
给出一个长度为 \(n\) 的数列 \(A\),同时定义一个辅助数组 \(B\),\(B\) 开始与 \(A\) 完全相同。接下来进行了 \(m\) 次操作,操作有五种类型,按以下格式给出:
1 l r k
:对于所有的 \(i\in[l,r]\)],将 \(A_i\) 加上 \(k\)(\(k\) 可以为负数)。
2 l r v
:对于所有的 \(i\in[l,r]\),将 \(A_i\) 变成 \(\min(A_i,v)\)。
3 l r
:求 \(\sum_{i=l}^{r}A_i\)。
4 l r
:对于所有的 \(i\in[l,r]\),求 \(A_i\) 的最大值。
5 l r
:对于所有的 \(i\in[l,r]\),求 \(B_i\) 的最大值。
在每一次操作后,我们都进行一次更新,让 \(B_i\gets\max(B_i,A_i)\)。
输入格式
第一行包含两个正整数 \(n,m\),分别表示数列 \(A\) 的长度和操作次数。
第二行包含 \(n\) 个整数 \(A_1,A_2,\cdots,A_n\) ,表示数列 \(A\)。
接下来 \(m\) 行,每行行首有一个整数 \(op\),表示操作类型;接下来两个或三个整数表示操作参数,格式见【题目描述】。
输出格式
对于 \(op\in\{3,4,5\}\) 的操作,输出一行包含一个整数,表示这个询问的答案。
输入
5 6
1 2 3 4 5
3 2 5
1 1 3 3
4 2 4
2 3 4 1
5 1 5
3 1 4
输出
14
6
6
11
解题思路
代码