初见安~这里是樱狸:)又来讲专题了呢。
一、线段树基本知识和操作
1.定义
线段树,是一种基于分治思想的二叉树结构,用于在区间上进行信息统计。基本用途是对序列进行维护,支持查询与修改的命令。与树状数组类似而更优。线段树最大的特点就是每个点为一个区间,最后形成一颗二叉树,大致就是这么个意思——(嘤嘤图丑勿喷,怪难画的QwQ
每一个节点就是一个区间,叶子结点就是各个点本身。有一点儿二分的感觉。每个节点 [ l , r ] 都有子节点 [ l , mid ] 和 [ mid + 1 , r ](mid向下取整)。当然也为了计算方便,我们用到树状数组的一点技巧来标记各个节点的编号:
父节点p的两个子节点的编号为:p * 2 ,p * 2 + 1。所以如图蓝字,我们就会发现一个问题——编号17~24中间没有编号了。这个其实不影响我们进行线段树的操作,空间开的足够大就行了:)当然中间有空位,在理解的时候自己要知道这一点。
所以通过以上的理解可以想到:我们在存这个二叉树的时候需要用到一个struct结构——来存储这个区间的l 和 r。后期我们会发现这个其实不用存,暂时先存下来便于理解。需要注意的是:线段树虽然优,但是比较耗内存,在开结构体数组的时候空间至少要开4N才不会越界。这就是线段树的一些基本概念。
2.建树
在了解了线段树包含的信息过后,我们可以很容易的根据这些信息来递归建树。
在[ 1 , n ]上建立一颗线段树,每个叶节点[ i , i ]存 a[ i ]的值以推出其父节点的值。这里我们都以求区间最大值为例,区间p的最大值为其两个子区间的最大值中的较大者。下方是核心代码:
struct node//后文的结构体均为这个
{
int l, r, maxx;
}t[maxn * 4];
void build(int p, int l, int r)//p为当前结点的编号,l、 r为传过来的范围
{
t[p].l = l,t[p].r = r;
if(l == r) //到了叶子结点
{
t[p].maxx = num[l];//l和r都可以
return;
}
int mid = (l + r) / 2;
build(p * 2, l, mid);
build(p * 2 + 1, mid + 1, r);//先求其子树
t[p].maxx = max(t[p * 2].maxx,t[p * 2 + 1].maxx);
}
入口:build(1, 1, n);
当然也可以不用结构体,不存l 和r,在递归时传值调用即可。
3.单点修改
一般会涉及到修改单点的题操作都是很明显的。一般都是形如“c,x,y”,将a[ x ]的值修改为y。
在线段树中,我们很明显需要思考如何找到叶子结点[ x , x ]并修改,而且还会涉及到可能会改变其祖先们的值。话已至此,我们可以想到——继续用递归的方法,找到该叶子结点后回溯更新。所以我们仍然从第一个节点开始搜索并计算出mid,与x作比较,判断是在其左边还是右边子树。当然,递归函数的开头要先特判是否已经到达了叶子结点。而这里因为之前的操作,已经保证了只要l == r,那么就是我们要找的点。
核心代码如下——
void change(int p, int x, int v)
{
int l = t[p].l ,r = t[p].r;
if(l == r)//到达叶子结点
{
t[p].maxx = v;
return;
}
int mid = (l + r) / 2;
if(x <= mid) change(p * 2, x, v);//确定左右
else change(p * 2 + 1, x, v);
t[p].maxx = max(t[p * 2].maxx, t[p * 2 + 1].maxx);//回溯更新
}
4.查询
查询的话一般是形如“q, l, r”的命令,求区间[ l , r ]的最大值。这里我们很容易的想到这么几种情况:
1)查询区间刚好为某一个子节点的区间,返回该节点的值即可。
2)查询区间小于子区间,取mid缩小范围查找即可。类似于二分查找。
3)查询区间横跨左右两个子树。这种的话仍旧是分开,左子树的部分和右子树的部分分开算,算了过后回溯得出答案。所以下方两种是同一个方法。
以上3种情况中,2)是1)的先前步骤。所以大体来看只有两种情况:跨区间的和不跨区间的。
我们可以来看看代码和详解理解一下——
int ask(int p, int l, int r)//l和r 为查询区间
{
if(l <= t[p].l && r >= t[p].r) return t[p].maxx;
//刚好就是这么个区间,或者完全包含就不用找了。完全包含的判定是一般情况的结束搜索判定。
int mid = (t[p].l + t[p].r) / 2;
int val = -(1 << 30);//要求最大值,先附极小值
if(l <= mid) val = max(val, ask(p * 2, l, r));
if(r > mid) val = max(val, ask(p * 2 + 1, l, r)); //由于有可能左右两区间均有涉及,不用else。
return val;
}
*5.求最大字段和(重难点)
不要以为求和 和 求最大值的思路一样,这个要复杂不少。其实建树都是差不多的,会随结构体里变量的改变而增减语句。我们先来分析一下求和的特别之处。
前文求最大值,我们可以直接用两个儿子的最大值来更新父亲的。但是最大连续字段和就不一样了,因为很容易遇到这种情况:两个儿子的最大连续字段并不连续,也就是下图的情况:
这样的话,能更新父亲的就只是左右的最大和,不能加起来。
那么我们转念一想:什么情况下是可以加起来更新的呢?很容易想到这么几种情况:
合起来说也就是左子树的包含右边界的最大和右子树包含左边界的最大合起来。是否包含这一边界,我们可以存入结构体,在回溯的时候就可以保证层层都是靠右 or 靠左的。所以我们试着添加两个变量——lmax和rmax,即以当前节点为根的最大靠左区间和(包含左边界)及最大靠右区间和(包含右边界)。因为我们不确定这个节点靠左还是靠右,也不需要去考虑这个,都存上就行了。而如上方下面两种情况的话,我们还会用到一个变量——sum存这个节点代表的区间的所有值的和。
需要定义的变量已经安排完毕,我们开始增减build的内容——在建树的同时,每一个变量都要不断更新。节点p的靠左最大区间和为其左儿子的靠左最大区间和或者左儿子全部加上右儿子的最大靠左区间和(上方左下角图),同理可得靠右最大区间和(上方右下角图)
这里文字描述太过繁琐,还是看代码理解一下吧——由于在change和build的时候都会有更新的操作(而且一模一样),为了方便我们就提取出来单独作为一个函数push:
struct node
{
int maxx,sum,lmax,rmax;//可以暂时不用存l,r,在查询的时候传值过去即可。
}t[maxn * 4];
void push(int p)//p为这一节点的编号
{
int lson = p * 2 , rson = p * 2 + 1;//两个儿子的编号
t[p].sum = t[lson].sum + t[rson].sum;//计和
t[p].lmax = max(t[lson].lmax, t[lson].sum + t[rson].lmax);
t[p].rmax = max(t[rson].rmax, t[lson].rmax + t[rson].sum);
t[p].maxx = max(t[lson].maxx, t[rson].maxx);
t[p].maxx = max(t[p].maxx, t[lson].rmax + t[rson].lmax);//两次比较可以合并。
}
看代码的时候可能会有一点儿问题,这种时候就自己手动模拟一下,还是很好理解的。并且要记住:进行这一步操作的时候,两个儿子的对应变量已经计算完毕了。
到了叶子结点的时候呢,就是把结构体内的4个变量全部更新为a [ l ]。原因很简单就不阐述了。
说到这里,我们大致可以说是已经处理好了build 和 change函数:
void push(int p)
{
int lson = p * 2 , rson = p * 2 + 1;
t[p].sum = t[lson].sum + t[rson].sum;
t[p].lmax = max(t[lson].lmax, t[lson].sum + t[rson].lmax);
t[p].rmax = max(t[rson].rmax, t[lson].rmax + t[rson].sum);
t[p].maxx = max(t[lson].maxx, t[rson].maxx);
t[p].maxx = max(t[p].maxx, t[lson].rmax + t[rson].lmax);
}
void build(int p, int l, int r)
{
if(l == r)
{
t[p].maxx = t[p].sum = t[p].lmax = t[p].rmax = num[l];
return;
}
int mid = (l + r) / 2;
build(p * 2, l, mid);
build(p * 2 + 1, mid + 1, r);
push(p);
}
void change(int p, int l, int r, int x, int v)//把l和r传过来用
{
if(l == r)
{
t[p].maxx = t[p].lmax = t[p].rmax = t[p].sum = v;
return;
}
int mid = (l + r) / 2;
if(x <= mid) change(p * 2, l, mid, x, v);
else change(p * 2 + 1, mid + 1, r, x, v);
push(p);
}
那么重头戏来了——ask查询怎么办??
为什么要问怎么办——因为我们由核心的那两行更新来看,如果这里还是这样划分情况的话,就无法确定所得出的maxx是不是在这个范围内的子树;如果要得出,又需要考虑到是在左边还是右边还是跨两边,跨两边的话单独考虑两边各自占的范围。所以——我们就只有分3种讨论了。并且还有个问题——我们这里得出的答案不一定是我们构建的线段树上的值,所以更新时调用的结构体得是自己另外开后传值过来的。
如果你晕了,没关系,看代码吧。
node ask(int p, int l, int r, int ls, int rs)//ls和rs为目标范围
{
if(ls <= l && rs >= r) return t[p];
int mid = (l + r) / 2;
if(rs <= mid) return ask(p * 2, l, mid, ls, rs);
else if(ls > mid) return ask2(p * 2 + 1, mid + 1, r, ls, rs);
else
{
node ln, rn, ans;
ln = ask(p * 2, l, mid, ls, rs);
rn = ask(p * 2 + 1, mid + 1, r, ls, rs);
ans.sum = ln.sum + rn.sum;
ans.lmax = max(ln.lmax, ln.sum + rn.lmax);
ans.rmax = max(rn.rmax, ln.rmax + rn.sum);
ans.maxx = max(ln.maxx, rn.maxx);
ans.maxx = max(ans.maxx, ln.rmax + rn.lmax);
return ans;
}
}
后面那一坨是不是很像我们之前的push函数!!!其实我们在找的过程同样类似于一个建树的过程——递归到底再回溯推值。所以其实也不过是过程有点难,基本思路都是一样的:)
最后一个要点这么难,那我们来看个例题吧:)
小白逛公园
这里是传送门:洛谷P4513
题目描述
在小新家附近有一条“公园路”,路的一边从南到北依次排着nn个公园,小白早就看花了眼,自己也不清楚该去哪些公园玩了。
一开始,小白就根据公园的风景给每个公园打了分-.-。小新为了省事,每次遛狗的时候都会事先规定一个范围,小白只可以选择第aa个和第bb个公园之间(包括aa、bb两个公园)选择连续的一些公园玩。小白当然希望选出的公园的分数总和尽量高咯。同时,由于一些公园的景观会有所改变,所以,小白的打分也可能会有一些变化。
那么,就请你来帮小白选择公园吧。
输入格式:
第一行,两个整数NN和MM,分别表示表示公园的数量和操作(遛狗或者改变打分)总数。
接下来NN行,每行一个整数,依次给出小白 开始时对公园的打分。
接下来MM行,每行三个整数。第一个整数KK,11或22。
- K=1K=1表示,小新要带小白出去玩,接下来的两个整数aa和bb给出了选择公园的范围(1≤a,b≤N1≤a,b≤N)。测试数据可能会出现a>ba>b的情况,需要进行交换;
- K=2K=2表示,小白改变了对某个公园的打分,接下来的两个整数pp和ss,表示小白对第pp个公园的打分变成了ss(1≤p≤N1≤p≤N)。
其中,1≤N≤500 0001≤N≤500000,1≤M≤100 0001≤M≤100000,所有打分都是绝对值不超过10001000的整数。
输出格式:
小白每出去玩一次,都对应输出一行,只包含一个整数,表示小白可以选出的公园得分和的最大值。
输入样例:
5 3
1 2 -3 4 5
1 2 3
2 2 -1
1 2 3
输出样例:
2
-1
题解
这个题其实就是一个求最大子区间和的问题,而且还要不断限定区间并涉及到修改。为了方便修改操作,我们就选用线段树的方法啦~(传送门:线段树)用线段树的话,这就是一个线段树求最大子区间和的模板题了。直接上代码:
#include<bits/stdc++.h>
#define maxn 500005
using namespace std;
int n,m,num[maxn];
struct node
{
int maxx,sum,lmax,rmax;//可以不用存l和r
}t[maxn * 4];
void push(int p)
{
int lson = p * 2 , rson = p * 2 + 1;
t[p].sum = t[lson].sum + t[rson].sum;
t[p].lmax = max(t[lson].lmax, t[lson].sum + t[rson].lmax);
t[p].rmax = max(t[rson].rmax, t[lson].rmax + t[rson].sum);
t[p].maxx = max(t[lson].maxx, t[rson].maxx);
t[p].maxx = max(t[p].maxx, t[lson].rmax + t[rson].lmax);
}
void build(int p, int l, int r)
{
if(l == r)
{
t[p].maxx = t[p].sum = t[p].lmax = t[p].rmax = num[l];
return;
}
int mid = (l + r) / 2;
build(p * 2, l, mid);
build(p * 2 + 1, mid + 1, r);
push(p);
}
node ask2(int p, int l, int r, int ls, int rs)//不要在意那个2,ls和lr为查询区间
{
if(ls <= l && rs >= r) return t[p];
int mid = (l + r) / 2;
if(rs <= mid) return ask2(p * 2, l, mid, ls, rs);
else if(ls > mid) return ask2(p * 2 + 1, mid + 1, r, ls, rs);
else
{
node ln, rn, ans;
ln = ask2(p * 2, l, mid, ls, rs);
rn = ask2(p * 2 + 1, mid + 1, r, ls, rs);
ans.sum = ln.sum + rn.sum;
ans.lmax = max(ln.lmax, ln.sum + rn.lmax);
ans.rmax = max(rn.rmax, ln.rmax + rn.sum);
ans.maxx = max(ln.maxx, rn.maxx);
ans.maxx = max(ans.maxx, ln.rmax + rn.lmax);
return ans;
}
}
void change(int p, int l, int r, int x, int v)
{
if(l == r)
{
t[p].maxx = t[p].lmax = t[p].rmax = t[p].sum = v;
return;
}
int mid = (l + r) / 2;
if(x <= mid) change(p * 2, l, mid, x, v);
else change(p * 2 + 1, mid + 1, r, x, v);
push(p);
}
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++)
{
scanf("%d", &num[i]);
}
build(1, 1, n);
int k,a,b;
for(int i = 1; i <= m; i++)
{
scanf("%d%d%d", &k, &a, &b);
if(k == 1)
{
if(b < a) swap(a, b);
printf("%d\n", ask2(1, 1, n, a, b).maxx);
}
else change(1, 1, n, a, b);
}
return 0;
}
线段树的模板题其实难度挺高的,而很多简单的可以用线段树的题又大材小用了。所以这个算法算是比较高级啦~希望大家能理解:)
迎评:)
——End——