目录
线段树之基本概念
学习笔记:由浅入深的线段树入门 - LinearExpectation的文章 - 知乎 https://zhuanlan.zhihu.com/p/350443545
算法学习笔记(14): 线段树 - Pecco的文章 - 知乎
https://zhuanlan.zhihu.com/p/106118909
难点
线段树的难点主要体现在以下几个方面:
1. 线段树的建立:需要对原始数组进行递归划分,同时还需要考虑边界问题、节点区间和等细节,容易出错。
2. 线段树的查询:针对不同的问题,查询方式和返回结果都有所不同,需要根据具体情况进行分析。
3. 线段树的修改:修改一个节点的值需要对其祖先节点进行更新操作,需要考虑多个节点之间的关系,可能会影响到整个线段树的结构。
4. 线段树的空间优化:当数据量很大时,线段树的空间复杂度也会很高,需要进行一些空间优化,如使用动态开销等技巧。
总的来说,线段树需要综合考虑多个因素,需要具备较强的数学思维和编程能力,因此相对来说较为复杂和难以掌握。但是,一旦掌握了其核心思想和常见应用,就可以在许多问题中运用线段树解决。
EG
下面举一个求区间最大值的线段树例子。
假设有一个长度为 $n$ 的数组 $a$,需要支持以下两种操作:
1. 单点修改:将 $a_i$ 的值修改为 $v$;
2. 区间查询:找到 $\max\limits_{i\in[l,r]} a_i$ 的值。
首先构建一棵满二叉树,每个叶子节点代表一个原数组的元素。每个非叶子节点对应区间 $[l,r]$,左儿子对应区间 $[l,mid]$,右儿子对应区间 $[mid+1,r]$。其中 $mid=\lfloor\frac{l+r}{2}\rfloor$。
在每个节点维护一个最大值 $max$,表示该区间中最大的数值。单点修改操作可以通过从根节点开始访问,沿着要修改的位置向下,更新每个节点的最大值。区间查询操作可以通过递归遍历线段树,如果查询区间和当前节点区间没有交集,则返回一个足够小的值;否则,将查询区间分解成两个子区间,分别递归查询子节点,将结果合并返回。
下面是一个用于查询区间最大值的线段树实现:
const int MAXN = 1e5 + 5;
int a[MAXN];
struct node {
int l, r, max;
} t[MAXN << 2];
void build(int p, int l, int r) {
t[p].l = l, t[p].r = r;
if (l == r) {
t[p].max = a[l];
return;
}
int mid = (l + r) >> 1;
build(p << 1, l, mid);
build(p << 1 | 1, mid + 1, r);
t[p].max = max(t[p << 1].max, t[p << 1 | 1].max);
}//建树
void modify(int p, int x, int v) {
if (t[p].l == t[p].r) {
t[p].max = v;
return;
}
int mid = (t[p].l + t[p].r) >> 1;
if (x <= mid) modify(p << 1, x, v);
else modify(p << 1 | 1, x, v);
t[p].max = max(t[p << 1].max, t[p << 1 | 1].max);
}//节点值
int query(int p, int l, int r) {
if (l > r) return INT_MIN;
if (l <= t[p].l && t[p].r <= r) return t[p].max;
int mid = (t[p].l + t[p].r) >> 1;
int res = INT_MIN;
if (l <= mid) res = max(res, query(p << 1, l, r));
if (r > mid) res = max(res, query(p << 1 | 1, l, r));
return res;
}//更新
以上代码实现了查询区间最大值、单点修改的功能,时间复杂度为 $\mathcal{O}(n\log n)$。
线段树1
Description
如题,已知一个数列,你需要进行下面两种操作:
- 将某区间每一个数加上 k。
- 求出某区间每一个数的和。
Input
第一行包含两个整数 n,m,分别表示该数列数字的个数和操作的总个数。
第二行包含 n 个用空格分隔的整数,其中第 i 个数字表示数列第 i 项的初始值。
接下来 m 行每行包含 33 或 4 个整数,表示一个操作,具体如下:
1 x y k
:将区间 [x,y] 内每个数加上 k。2 x y
:输出区间 [x,y] 内每个数的和。
Output
输出包含若干行整数,即为所有操作 2 的结果。
Sample 1
Inputcopy | Outputcopy |
---|---|
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 |
#include<iostream>
#include<cstdio>
#define MAXN 1000001
#define ll long long
using namespace std;
unsigned ll n,m,a[MAXN],ans[MAXN<<2],tag[MAXN<<2];
ll ls(ll x){return x<<1;}
ll rs(ll x){return x<<1|1;}
inline void push_up(ll p){
ans[p]=ans[ls(p)]+ans[rs(p)];
}
void build(ll p,ll l,ll r){
tag[p]=0;
if(l==r){ans[p]=a[l];return ;}
ll mid=(l+r)>>1;
build(ls(p),l,mid);
build(rs(p),mid+1,r);
push_up(p);
}
void f(ll p,ll l,ll r,ll k){
tag[p]=tag[p]+k;
ans[p]=ans[p]+k*(r-l+1);
}
void push_down(ll p,ll l,ll r){
ll mid=(l+r)>>1;
f(ls(p),l,mid,tag[p]);
f(rs(p),mid+1,r,tag[p]);
tag[p]=0;
}
void update(ll nl,ll nr,ll l,ll r,ll p,ll k){
if(nl<=l&&r<=nr)
{
ans[p]+=k*(r-l+1);
tag[p]+=k;
return ;
}
push_down(p,l,r);
ll mid=(l+r)>>1;
if(nl<=mid)update(nl,nr,l,mid,ls(p),k);
if(nr>mid) update(nl,nr,mid+1,r,rs(p),k);
push_up(p);
}
ll query(ll q_x,ll q_y,ll l,ll r,ll p){
ll res=0;
if(q_x<=l&&r<=q_y)return ans[p];
ll mid=(l+r)>>1;
push_down(p,l,r);
if(q_x<=mid)res+=query(q_x,q_y,l,mid,ls(p));
if(q_y>mid) res+=query(q_x,q_y,mid+1,r,rs(p));
return res;
}
int main(){
ll a1,b,c,d,e,f;
scanf("%lld%lld",&n,&m);
for(ll i=1;i<=n;i++)scanf("%lld",&a[i]);
build(1,1,n);
while(m--){
scanf("%lld",&a1);
switch(a1)
{
case 1:{
scanf("%lld%lld%lld",&b,&c,&d);
update(b,c,1,n,1,d);
break;
}
case 2:{
scanf("%lld%lld",&e,&f);
printf("%lld\n",query(e,f,1,n,1));
break;
}
}
}
return 0;
}
线段树2
Description
如题,已知一个数列,你需要进行下面三种操作:
- 将某区间每一个数乘上 x;
- 将某区间每一个数加上 x;
- 求出某区间每一个数的和。
Input
第一行包含三个整数 n,q,m,分别表示该数列数字的个数、操作的总个数和模数。
第二行包含 n 个用空格分隔的整数,其中第 i 个数字表示数列第 i 项的初始值。
接下来 q 行每行包含若干个整数,表示一个操作,具体如下:
操作 1: 格式:1 x y k
含义:将区间 [x,y] 内每个数乘上 k
操作 2: 格式:2 x y k
含义:将区间 [x,y] 内每个数加上 k
操作 3: 格式:3 x y
含义:输出区间 [x,y] 内每个数的和对 m 取模所得的结果
Output
输出包含若干行整数,即为所有操作 33 的结果。
Sample 1
Inputcopy | Outputcopy |
---|---|
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 |
#include <bits/stdc++.h>
#define MAXN 100010
#define ll long long
using namespace std;
int n,m,mod;
int a[MAXN];
struct Segment_Tree {
ll sum,add,mul;
int l,r;
}s[MAXN<<2];
void update(int pos) {
s[pos].sum=(s[pos<<1].sum+s[pos<<1|1].sum)%mod;
return;
}
void pushdown(int pos) {
s[pos<<1].sum=(s[pos<<1].sum*s[pos].mul+s[pos].add*(s[pos<<1].r-s[pos<<1].l+1))%mod;
s[pos<<1|1].sum=(s[pos<<1|1].sum*s[pos].mul+s[pos].add*(s[pos<<1|1].r-s[pos<<1|1].l+1))%mod;
s[pos<<1].mul=(s[pos<<1].mul*s[pos].mul)%mod;
s[pos<<1|1].mul=(s[pos<<1|1].mul*s[pos].mul)%mod;
s[pos<<1].add=(s[pos<<1].add*s[pos].mul+s[pos].add)%mod;
s[pos<<1|1].add=(s[pos<<1|1].add*s[pos].mul+s[pos].add)%mod;
s[pos].add=0,s[pos].mul=1;
return;
}
void build_tree(int pos,int l,int r) {
s[pos].l=l,s[pos].r=r,s[pos].mul=1;
if (l==r)return void(s[pos].sum=a[l]%mod);
int mid=(l+r)>>1;
build_tree(pos<<1,l,mid);
build_tree(pos<<1|1,mid+1,r);
update(pos);
return;
}
void ChangeMul(int pos, int x, int y, int k) {
if (x<=s[pos].l&&s[pos].r<=y){
s[pos].add=(s[pos].add*k)%mod;
s[pos].mul=(s[pos].mul*k)%mod;
s[pos].sum=(s[pos].sum*k)%mod;
return;
}
pushdown(pos);
int mid=(s[pos].l+s[pos].r)>>1;
if (x<=mid) ChangeMul(pos<<1,x,y,k);
if (y>mid) ChangeMul(pos<<1|1,x,y,k);
update(pos);
return;
}
void ChangeAdd(int pos,int x,int y,int k) {
if (x<=s[pos].l&&s[pos].r<=y){
s[pos].add=(s[pos].add+k)%mod;
s[pos].sum=(s[pos].sum+k*(s[pos].r-s[pos].l+1))%mod;
return;
}
pushdown(pos);
int mid =(s[pos].l+s[pos].r)>>1;
if (x<=mid) ChangeAdd(pos<<1,x,y,k);
if (y>mid) ChangeAdd(pos<<1|1,x,y,k);
update(pos);
return;
}
ll AskRange(int pos, int x, int y){
if (x<=s[pos].l&&s[pos].r<=y){
return s[pos].sum;
}
pushdown(pos);
ll val=0;
int mid=(s[pos].l+s[pos].r)>>1;
if (x<=mid) val=(val+AskRange(pos<<1,x,y)) % mod;
if (y>mid) val=(val+AskRange(pos<<1|1,x,y)) % mod;
return val;
}
int main() {
scanf("%d%d%d",&n,&m,&mod);
for (int i=1;i<=n;i++){
scanf("%d",&a[i]);
}
build_tree(1,1,n);
for (int i=1;i<=m;i++){
int opt,x,y;
scanf("%d%d%d",&opt,&x,&y);
if (opt == 1){
int k;
scanf("%d",&k);
ChangeMul(1,x,y,k);
}
if (opt==2) {
int k;scanf("%d",&k);
ChangeAdd(1,x,y,k);
}
if (opt==3) {
printf("%lld\n",AskRange(1,x,y));
}
}
return 0;
}
树状数组1
Description
如题,已知一个数列,你需要进行下面两种操作:
-
将某一个数加上 x
-
求出某区间每一个数的和
Input
第一行包含两个正整数 n,m,分别表示该数列数字的个数和操作的总个数。
第二行包含 n 个用空格分隔的整数,其中第 i 个数字表示数列第 i 项的初始值。
接下来 m 行每行包含 3 个整数,表示一个操作,具体如下:
-
1 x k
含义:将第 x 个数加上 k -
2 x y
含义:输出区间 [x,y] 内每个数的和
Output
输出包含若干行整数,即为所有操作 2 的结果。
Sample 1
Inputcopy | Outputcopy |
---|---|
5 5 1 5 4 2 3 1 1 3 2 2 5 1 3 -1 1 4 2 2 1 4 | 14 16 |
#include<iostream>
#include<cstdio>
#define MAXN 1000001
#define ll long long
using namespace std;
unsigned ll n,m,a[MAXN],ans[MAXN<<2],tag[MAXN<<2];
ll ls(ll x){return x<<1;}
ll rs(ll x){return x<<1|1;}
inline void push_up(ll p){
ans[p]=ans[ls(p)]+ans[rs(p)];
}
void build(ll p,ll l,ll r){
tag[p]=0;
if(l==r){ans[p]=a[l];return ;}
ll mid=(l+r)>>1;
build(ls(p),l,mid);
build(rs(p),mid+1,r);
push_up(p);
}
void f(ll p,ll l,ll r,ll k){
tag[p]=tag[p]+k;
ans[p]=ans[p]+k*(r-l+1);
}
void push_down(ll p,ll l,ll r){
ll mid=(l+r)>>1;
f(ls(p),l,mid,tag[p]);
f(rs(p),mid+1,r,tag[p]);
tag[p]=0;
}
void update(ll nl,ll nr,ll l,ll r,ll p,ll k){
if(nl<=l&&r<=nr)
{
ans[p]+=k*(r-l+1);
tag[p]+=k;
return ;
}
push_down(p,l,r);
ll mid=(l+r)>>1;
if(nl<=mid)update(nl,nr,l,mid,ls(p),k);
if(nr>mid) update(nl,nr,mid+1,r,rs(p),k);
push_up(p);
}
ll query(ll q_x,ll q_y,ll l,ll r,ll p){
ll res=0;
if(q_x<=l&&r<=q_y)return ans[p];
ll mid=(l+r)>>1;
push_down(p,l,r);
if(q_x<=mid)res+=query(q_x,q_y,l,mid,ls(p));
if(q_y>mid) res+=query(q_x,q_y,mid+1,r,rs(p));
return res;
}
int main(){
ll a1,b,c,d,e,f;
scanf("%lld%lld",&n,&m);
for(ll i=1;i<=n;i++)scanf("%lld",&a[i]);
build(1,1,n);
while(m--){
scanf("%lld",&a1);
switch(a1)
{
case 1:{
scanf("%lld%lld%lld",&b,&c,&d);
update(b,c,1,n,1,d);
break;
}
case 2:{
scanf("%lld%lld",&e,&f);
printf("%lld\n",query(e,f,1,n,1));
break;
}
}
}
return 0;
}
敌兵布阵
C国的死对头A国这段时间正在进行军事演习,所以C国间谍头子Derek和他手下Tidy又开始忙乎了。A国在海岸线沿直线布置了N个工兵营地,Derek和Tidy的任务就是要监视这些工兵营地的活动情况。由于采取了某种先进的监测手段,所以每个工兵营地的人数C国都掌握的一清二楚,每个工兵营地的人数都有可能发生变动,可能增加或减少若干人手,但这些都逃不过C国的监视。
中央情报局要研究敌人究竟演习什么战术,所以Tidy要随时向Derek汇报某一段连续的工兵营地一共有多少人,例如Derek问:“Tidy,马上汇报第3个营地到第10个营地共有多少人!”Tidy就要马上开始计算这一段的总人数并汇报。但敌兵营地的人数经常变动,而Derek每次询问的段都不一样,所以Tidy不得不每次都一个一个营地的去数,很快就精疲力尽了,Derek对Tidy的计算速度越来越不满:"你个死肥仔,算得这么慢,我炒你鱿鱼!”Tidy想:“你自己来算算看,这可真是一项累人的工作!我恨不得你炒我鱿鱼呢!”无奈之下,Tidy只好打电话向计算机专家Windbreaker求救,Windbreaker说:“死肥仔,叫你平时做多点acm题和看多点算法书,现在尝到苦果了吧!”Tidy说:"我知错了。。。"但Windbreaker已经挂掉电话了。Tidy很苦恼,这么算他真的会崩溃的,聪明的读者,你能写个程序帮他完成这项工作吗?不过如果你的程序效率不够高的话,Tidy还是会受到Derek的责骂的.
Input
第一行一个整数T,表示有T组数据。
每组数据第一行一个正整数N(N<=50000),表示敌人有N个工兵营地,接下来有N个正整数,第i个正整数ai代表第i个工兵营地里开始时有ai个人(1<=ai<=50)。
接下来每行有一条命令,命令有4种形式:
(1) Add i j,i和j为正整数,表示第i个营地增加j个人(j不超过30)
(2)Sub i j ,i和j为正整数,表示第i个营地减少j个人(j不超过30);
(3)Query i j ,i和j为正整数,i<=j,表示询问第i到第j个营地的总人数;
(4)End 表示结束,这条命令在每组数据最后出现;
每组数据最多有40000条命令
Output
对第i组数据,首先输出“Case i:”和回车,
对于每个Query询问,输出一个整数并回车,表示询问的段中的总人数,这个数保持在int以内。
Sample
Inputcopy | Outputcopy |
---|---|
1 10 1 2 3 4 5 6 7 8 9 10 Query 1 3 Add 3 6 uery 2 7 Sub 10 2 Add 6 3 Query 3 10 End | Case 1: 6 33 59 |
#include <cstdio>
using namespace std;
#define ll long long
int T,n,a,b;
char s[15];
ll aa[50005];
struct node {
ll l, r, sum;
} t[4 * 50005];
ll build(int l, int r, int u) {
t[u].l = l, t[u].r = r;
if (t[u].l == t[u].r) return t[u].sum = aa[l];
return t[u].sum = build(l, (r + l) >> 1, u << 1) +
build(((r + l) >> 1) + 1, r, (u << 1) + 1);
}
void add(int i, int num) {
int u = 1;
while (true) {
t[u].sum += num;
if (t[u].l == t[u].r && t[u].l == i) return;
((t[u].l + t[u].r) >> 1) >= i ? u = u << 1 : u = (u << 1) + 1;
}
}
void sub(int i, int num) {
int u = 1;
while (true) {
t[u].sum -= num;
if (t[u].l == t[u].r && t[u].l == i) return;
((t[u].l + t[u].r) >> 1) >= i ? u = u << 1 : u = (u << 1) + 1;
}
}
ll query(int l, int r, int u) {
if (l > r || t[u].l > r || t[u].r < l) return 0;
if (t[u].l >= l && t[u].r <= r) return t[u].sum;
return query(l, r, u << 1) + query(l, r, (u << 1) + 1);
}
int main() {
scanf("%d", &T);
int _case = 1;
while (T--) {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%lld", &aa[i]);
printf("Case %d:\n", _case++);
build(1, n, 1);
while (true) {
scanf("%s", s);
if (s[0] == 'E') break;
scanf("%d%d", &a, &b);
if (s[0] == 'Q') printf("%lld\n", query(a, b, 1));
if (s[0] == 'A') add(a, b);
if (s[0] == 'S') sub(a, b);
}
}
return 0;
}