线段树主要用来维护复杂的区间信息.只要满足区间可加性,线段树基本都可以解决.
1.线段树基本操作(单点更新,区间求和等不涉及lazy标记问题)
先来讲建树问题,线段树建树有很多种方法,本文介绍的是把一个区间划分成为[l,mid],[mid+1,r]的建树方法.
我们会把一个大区间分成若干个小区间,tree[1]是表示整个大区间.把它分成两个小区间.用下标tree[2×father], tree[2×father+1]来储存信息.
最基本的信息有两个.区间的左端点和右端点.其他的附加信息首先一定要满足区间可加性. 即father区间的信息可以由两个子区间的信息推出来.最简单的如sum.max,min这几种信息都是很明显的具有区间可加性.
建树代码
void build(int i,int l,int r){
tree[i] = {
l,r};
if(l >= r){
tree[i].sum or max or min = a[l];
return;
}
int mid = l+r>>1;
build(i*2,l,mid);
build(i*2+1,mid+1,r);
push_up(i);
}
要注意递归的边界和区间的划分.一开始可能会敲出很多神奇的坑.敲多了就熟练了.
前面说了,满足区间可加性的信息可以用子区间的信息推出父亲区间的信息.这里用一个通用的push_up(i)操作来减少代码量和增加可调试性.这个函数在每次更新完子节点的信息之后就用一次.就可以了.
push_up代码
void push_up(int i){
tree[i].sum = tree[i*2].sum + tree[i*2+1].sum;
tree[i].max = max(tree[i*2].max,tree[i*2+1].max);
//min同上.
}
理解如何建树之后再引入最简单的一些操作.
单点更新(log(n)).
线段树一定要理解复杂度是如何来的.不然对后面的lazy标记的运用会很懵逼.单点更新的复杂度之所以是log(n)是因为我们只更新一个数.而这个更新的过程是不断二分的.故复杂度是log(n).虽然它很简单,但能对我们接下来理解lazy标记有帮助.
单点更新代码(在左边就往左找,在右边就往右找)
void add(int i,int p,int v){
if(tree[i].l == tree[i].r){
tree[i].sum or max or min = v; // 如果是表示增加v可以写成 += v;
return;
}
int mid = tree[i].l + tree[i].r >> 1;
if(p <= mid) add(i*2,p,v);
//这一步也很关键.因为区间是被划分成为[l,mid][mid+1,r]所以等于mid的时候要更新左边
else add(i*2+1,p,v);
push_up(i);//更新完别忘记push_up
}
区间求和(log(n))
区间求和的复杂度证明过程稍长.先给代码再简要的说一下.
int query(int i,int l,int r){
if(tree[i].l >= l && tree[i].r <= r) return tree[i].sum;
int mid = tree[i].l + tree[i].r >> 1;
if(l >= mid) return query(i*2,l,r);
else if(r < mid) return query(i*2+1,l,r);
else return query(i*2,l,mid)+query(i*2+1,mid+1,r);
}
我们来看这三条分支
第一条 虽然分成了两个小区间,但只用了一个,所以和单点更新是一样的.
第二条 同上
第三条 这一次两边都会递归. 但是有一个特点.只有在l,r位于tree[i].l,r中间的时候才会真正去递归两个子区间,而且这种情况只会出现至多一次(可以自己思考一下为什么,不理解也没关系,知道复杂度是log(n)就行)
所以总体来说复杂度是(2*log(n))的.
给几道例题感受一下线段树的强大.(不是基本的求和求最大值等问题,网上有很多基础的模板可以测试.我就不给题目了)
顺带提一嘴.不建议初学的时候敲了模板之后做题一直用模板.最好每一题都新敲一次.能更好的锻炼自己的代码能力.也能更深刻的理解线段树.
你能回答这些问题吗
题意很直接,就不当复读机了. 这题其实是由一个很经典的题目加了点新操作转化而来的.先引入一下这个问题的解法再回来看这题
即最大连续字段和这个问题.
给一个数组,求最大的区间和.有负数. 有很便捷的dp O(n)解法,但和线段树关系不大,讲另外一个没那么优秀但有启发性的分治算法.
我们把数组分为两段,可以知道的是.答案要么在左半边区间,要么在右半边区间,要么就是中间连着跨越了左右区间连续的一段. 具体一点说就是递归求完左右区间的最大值之后,从中点向两边延伸过程中得到的子序列也是可能的答案.
从这个做法我们可以联想到这题的解法.大区间和小区间就如同刚才分治时候分割的两段和被分割的区间的关系. 答案要么在左右两边要么在中间.为了方便得到中间的最大值,我们可以用两个变量 lmax表示贴着左边界向右延伸的最大值和rmax表示贴着右边界向左延伸的最大值.这样tree[L].rmax+tree[R].lmax(L和R表示2×i和2×i+1,代码里也用了一个宏去减少代码量)就是待选答案了.而lmax和rmax的更新方式可以自己独立思考一下.
顺带提一嘴的是线段树代码的出bug率是很高的.debug一下午甚至一天.两天都是很可能发生的事情.尽量在崩溃之前不要看题解.
具体代码
#pragma GCC optimize(3)
#define LL long long
#define pq priority_queue
#define ULL unsigned long long
#define pb push_back
#define mem(a,x) memset(a,x,sizeof a)
#define pii pair<int,int>
#define pll pair<long long,long long>
#define pdd pair<double,double>
#define db double
#define fir(i,a,b) for(int i=a;i<=b;++i)
#define afir(i,a,b) for(int i=a;i>=b;--i)
#define ft first
#define vi vector<int>
#define sd second
#define ALL(a) a.begin(),a.end()
#define L 2*i
#define R 2*i+1
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5+10;
const int mod = 9901;
inline int read(){
int x = 0,f=1;char ch = getchar();
while(ch<'0'||ch>'9'){
if(ch=='-')f=-1;ch=getchar();}
while(ch<='9'&&ch>='0'){
x=x*10+ch-'0';ch=getchar();}
return x*f;
}
struct Tree{
int l,r,v,sum,lmax,rmax;
}tree[N*4];
int a[N];
void push_up(int i){
tree[i].sum = tree[i*2].sum + tree[i*2+1].sum;
tree[i].lmax = max(tree[L].lmax,tree[L].sum+tree[R].lmax);
tree[i].rmax = max(tree[R].rmax,tree[R].sum+tree[L].rmax);
tree[i].v = max(tree[L].rmax + tree[R].lmax, max(tree[L].v, tree[R].v));
}
void build(int i,int l,int r){
tree[i] = {
l,r};
if(l >= r){
tree[i] = {
l,r,a[l],a[l],a[l],a[l]};
return;
}
int mid = l+r>>1;
build(i*2,l,mid);
build(i*2+1,mid+1,r);
push_up(i);
}
void add(int i,int p,int v){
if(tree[i].l == tree[i].r){
tree[i].sum = tree[i].lmax = tree[i].rmax = tree[i].v = v;
return;
}
int mid = tree[i].l + tree[i].r >> 1;
if(p <= mid) add(L,p,v);
else add(R,p,v);
push_up(i);
}
Tree query(int i,int l,int r){
if(tree[i].l >= l && tree[i].r <= r) return tree[i];
int mid = tree[i].l + tree[i].r >> 1;
if(r <= mid) return query(L,l,r);
if(l > mid) return query(R,l,r);
Tree t1,t2,t3;
t1 = query(L,l,mid);
t2 = query(R,mid+1,r);
t3.sum = t1.sum + t2.sum;
t3.lmax = max(t1.lmax,t1.sum+t2.lmax);
t3.rmax = max(t2.rmax,t2.sum+t1.rmax);
t3.v = max(t1.rmax + t2.lmax, max(t1.v, t2.v));
return t3;
}
int main(){
int n,m;
cin >> n >> m;
fir(i,1,n) cin >> a[i];
build(1,1,n);
while(m--){
int op,p,x;
cin >> op >> p >> x;
if(op == 1) cout << query(1,min(p,x),max(p,x)).v << endl;
else add(1,p,x);
}
return 0;
}
区间最大公约数
这题给出的两个操作.第二个操作显然是满足区间可加性的.可第一个操作的复杂度不如人意.可以看完下面的区间修改后再来思考这题为什么不能用lazy标记.
给区间的每一个数字都加x的话这个区间里面每一对数的gcd都会变化.所以要到跟节点修改每一个数的值.这个复杂度是O(n)的.不可以接受.所以需要转化.
数学知识:更相减损法. gcd(a,b) = gcd(a,b-a).还可以扩展到多个的情况.gcd(a,b,c) = gcd(a,b-a,c-b)… 这个式子是不是有点眼熟. 这不就是一个差分数组嘛. 再考虑区间加x其实就是在差分数组上d[l] += x, d[r+1] -= x,这样子我们就把修改区间的每一个数转化成了修改差分数组上的两个数.这样就是O(2×log(n))的操作了. 同时我们还需要用到a[i]的值. 这个可以用多一个线段树或者树状数组来维护.
顺带补充一下,这题会爆int需要用long long
具体代码:
#pragma GCC optimize(3)
#define LL long long
#define pq priority_queue
#define ULL unsigned long long
#define pb push_back
#define mem(a,x) memset(a,x,sizeof a)
#define pii pair<int,int>
#define pll pair<long long,long long>
#define pdd pair<double,double>
#define db double
#define fir(i,a,b) for(int i=a;i<=b;++i)
#define afir(i,a,b) for(int i=a;i>=b;--i)
#define ft first
#define vi vector<int>
#define sd second
#define ALL(a) a.begin(),a.end()
#define L 2*i
#define R 2*i+1
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5+10;
const int mod = 9901;
inline int read(){
int x = 0,f=1;char ch = getchar();
while(ch<'0'||ch>'9'){
if(ch=='-')f=-1;ch=getchar();}
while(ch<='9'&&ch>='0'){
x=x*10+ch-'0';ch=getchar();}
return x*f;
}
LL gcd(LL a,LL b){
return b == 0 ? a : gcd(b,a%b);}
struct Tree{
int l,r;
LL g;
} tree[N*4];
LL a[N],c[N],b[N];
int n,m;
void add(int x,LL v){
for(;x<=n;x+=x&-x) c[x] += v;
}
LL ask(int x){
LL res