-
题目链接
前言:
- 最近在做线段树的练习,对于区间合并问题不是很清楚,花了好久才把线段树的区间合并问题理清楚,所以把学习的过程记录下来,建议手动建树并模拟测试用例 题目大意:
- 有一个数组,求这个数组中最长的单调连续递增序列的长度 题解:
- 见一下注释
/*
树结点的定义:
有该结点的左端点、右端点
有该结点对应区间的最左端点的值,最右端点的值
有该结点对应区间的从第一个元素开始、从左到右的最长连续递增子序列
有该结点对应区间的从最后一个元素开始、从右到左的最长连续递减子序列
线段树的定义:
有该线段树的树结构
有定义树结构的函数
有更新树结构的函数
有查询区间最长长度的函数
有向上更新父结点的函数
对于树结构的实现:
使用node tree[N*4]来定义
对于定义树结构的函数:
从0-n-1根据数组a元素值来建树
叶节点的左右端点、左右端点的值、左递增右递减的长度 显然是已知的
对于非叶节点,需要根据左右子树的情况来确定该结点的左递增右递减以及总连续递增的长度,在pushUp函数中封装该功能
对于更新树结构的函数:
在理解了树结构定义的函数之后,树的更新很好理解
只是在建树的基础上,只需要修改树上的一条线就可以
对于向上更新父结点的函数:(比较难理解,划重点)
这里可以分为好几种情况
首先,这个父结点的左右端点和左右端点的值很容易判定,直接使用左子树的最左端点和最左端点的值以及右子树的最右端点和最右端点的值
其次,这个父结点的左递增长度和右递减长度的判定:
这个长度肯定是等于左子树的左递增的长度
(这里需要明确,左递增的长度的含义:就是从该结点对应区间的最左端点开始,向右,连续的递增的子序列的长度)
(再次需要明确,右递减的长度的含义:就是从该结点对应区间的最右端点开始,向左,连续的递减的子序列的长度)
(例如:对于数组[3,4,2,5] 左递增的长度为2(由3,4组成,2不算,因为2<4,使递增结束)
右递减的长度为2(由2,5组成,4不算,因为4>2,使得递减结束)
另外,如果左子树的最右端点的值是小于右子树的最左端点的值,
而且如果左子树的最长的连续递增自学列的长度和左子树的左递增的长度相同(说明是左子树的所有元素满足时连续单调递增的)
同样,如果右子树的最长的连续递增自学列的长度和右子树的右递减的长度相同(说明是右子树的所有元素满足时连续单调递增的)
对于查询区间最长长度的函数:
不可以像其他线段树那样直接合并查询,需要在合并查询之前,判断左子树的最右端点值是否小于右子树的最左端点值
*/
#include<iostream>
using namespace std;
const int N = 100000;
int n,m;
int a[N];
struct node{
int l;
int r;
int lv;
int rv;
int lm;//从左到右递增的最大长度
int rm;//从右到左递减的最大长度
int m;
int mid(){
return (l+r)/2;
}
int len(){
return r-l+1;
}
};
struct Seg{
node tree[N*4];
void pushUp(int i){
//关键是父结点怎么建立呢
tree[i].lm = tree[i*2].lm;
tree[i].rm = tree[i*2+1].rm;
//因为左子树的从左边第一个元素到右边(一定是从第一个开始),看有多少个连续递增的
tree[i].lv = tree[i*2].lv;//同样一定是从右边最后一个开始
tree[i].rv = tree[i*2+1].rv;
tree[i].m = max(tree[i*2].m,tree[i*2+1].m);
if(tree[i*2].rv < tree[i*2+1].lv){
//左子区间的最右端点值 < 右子区间最左端点的值
if(tree[i*2].len() == tree[i*2].lm){
tree[i].lm += tree[i*2+1].lm;//左子区间完全是递增的
}
if(tree[i*2+1].len() == tree[i*2+1].rm){
//右子区间的长度等于右子区间的从右向左最长的递增序列
tree[i].rm += tree[i*2].rm;//右子区间完全是递增
}
//不管左右子区间是不是完全递增,反正从左子区间到右子区间是递增的的
//那么父结点的最长连续递增的区间长度等于自身和左右长度加起来最大的一个
tree[i].m = max(tree[i].m,tree[i*2].rm+tree[i*2+1].lm);
}
//我不管从左到右是不是递增的,我就要最大的
//至此,对于父结点i,
//它的从左到右的最长的递增长度确定了 ,它的从右到左的最长的递减长度确定了
//说明:如果第一个if不满足条件
//那么从左到右的最长递增和从右到左的最长递减是不会变的
tree[i].m = max(tree[i].m,max(tree[i].rm,tree[i].lm));
}
void build(int i,int l,int r){
//对于叶子结点,它的长度都是1
tree[i].l = l;
tree[i].r = r;
if(l==r){
tree[i].lv = tree[i].rv = a[l];//设置左右子树结点得值为a[l]
tree[i].lm = tree[i].rm = tree[i].m = 1;//设置最大长度为1
} else{
int m = tree[i].mid();
build(i*2,l,m);
build(i*2+1,m+1,r);
pushUp(i);//两颗子树都做好工作之后,再向父结点交差
}
}
void update(int i,int pos,int v){
//既然建树的流程已经很清楚了,那么更新就很方便了
//更新的过程就是把建树的过程简化,只需要更新一条线
//至于那种不知道兄弟结点的最右边或者最左边的问题,这都不是事,因为有lm,rm,lv,rv担着呢,嘻嘻
if(tree[i].l == tree[i].r) tree[i].lv = tree[i].rv = v;//是叶子结点,那么就直接更新
else{
int m = tree[i].mid();
if(pos<=m) update(i*2,pos,v);
else update(i*2+1,pos,v);
pushUp(i);//把该更新的那条路更新好之后,那么他父亲结点和祖父结点的相关问题和建树的时候是一样的
//考虑的问题在pushUp函数中已经体现出来
}
}
//查询又是一个大问题
int query(int i,int l,int r){
//查询[l,r]区间内的最长的连续递增的子序列
int li = tree[i].l;
int ri = tree[i].r;
if(l<=li && ri<=r){
//那么这棵树的范围是在我要查询的区间之内的
//所以可以直接返回这个区间内的最长的连续递增序列长度
//但是要注意到
return tree[i].m;//返回最大的长度
}else{
//我要查找的区间是由两个子树组成
int m = tree[i].mid();
if(r<=m) return query(i*2,l,r);
else if(l>m) return query(i*2+1,l,r);//因为这里少一个else导致调试不对
//我不管左右有没有
//我还的看左子树的最右子结点是否小于右子树的最左子结点
else{
int sum1 = 0;
int sum2 = 0;
int m1 = query(i*2,l,r);
int m2 = query(i*2+1,l,r);
if(tree[i*2].rv < tree[i*2+1].lv){
sum1 = min(tree[i*2].rm,m-l+1);//防止左子树的右递减的长度大于左子树的区间长度
sum2 = min(tree[i*2+1].lm,r-m);//防止右子树的左递增的长度大于右子树的区间长度
//然后用两个最小值求和,就是一个可能的最大长度
}
return max(max(m1,m2),sum1+sum2);
}
}
}
}seg;
int main(){
int t;
cin>>t;
while(t--){
char str;
int l,r;
scanf("%d%d",&n,&m);
for(int i=0;i<n;i++) scanf("%d",&a[i]);
seg.build(1,0,n-1);
while(m--){
cin>>str>>l>>r;
if(str == 'Q'){
printf("%d\n",seg.query(1,l,r));
}else seg.update(1,l,r);
}
}
return 0;
}
下面再贴一个不带注释的,方便浏览
#include<iostream>
using namespace std;
const int N = 100000;
int n,m;
int a[N];
struct node{
int l;
int r;
int lv;
int rv;
int lm;//从左到右递增的最大长度
int rm;//从右到左递减的最大长度
int m;
int mid(){
return (l+r)/2;
}
int len(){
return r-l+1;
}
};
struct Seg{
node tree[N*4];
void pushUp(int i){
tree[i].lm = tree[i*2].lm;
tree[i].rm = tree[i*2+1].rm;
tree[i].lv = tree[i*2].lv;
tree[i].rv = tree[i*2+1].rv;
tree[i].m = max(tree[i*2].m,tree[i*2+1].m);
if(tree[i*2].rv < tree[i*2+1].lv){
if(tree[i*2].len() == tree[i*2].lm){
tree[i].lm += tree[i*2+1].lm;//左子区间完全是递增的
}
if(tree[i*2+1].len() == tree[i*2+1].rm){
tree[i].rm += tree[i*2].rm;//右子区间完全是递增
}
tree[i].m = max(tree[i].m,tree[i*2].rm+tree[i*2+1].lm);
}
tree[i].m = max(tree[i].m,max(tree[i].rm,tree[i].lm));
}
void build(int i,int l,int r){
tree[i].l = l;
tree[i].r = r;
if(l==r){
tree[i].lv = tree[i].rv = a[l];//设置左右子树结点得值为a[l]
tree[i].lm = tree[i].rm = tree[i].m = 1;//设置最大长度为1
} else{
int m = tree[i].mid();
build(i*2,l,m);
build(i*2+1,m+1,r);
pushUp(i);
}
}
void update(int i,int pos,int v){
if(tree[i].l == tree[i].r) tree[i].lv = tree[i].rv = v;//是叶子结点,那么就直接更新
else{
int m = tree[i].mid();
if(pos<=m) update(i*2,pos,v);
else update(i*2+1,pos,v);
pushUp(i);
}
}
int query(int i,int l,int r){
//查询[l,r]区间内的最长的连续递增的子序列
int li = tree[i].l;
int ri = tree[i].r;
if(l<=li && ri<=r){
return tree[i].m;//返回最大的长度
}else{
int m = tree[i].mid();
if(r<=m) return query(i*2,l,r);
else if(l>m) return query(i*2+1,l,r);
else{
int sum1 = 0;
int sum2 = 0;
int m1 = query(i*2,l,r);
int m2 = query(i*2+1,l,r);
if(tree[i*2].rv < tree[i*2+1].lv){
sum1 = min(tree[i*2].rm,m-l+1);//防止左子树的右递减的长度大于左子树的区间长度
sum2 = min(tree[i*2+1].lm,r-m);//防止右子树的左递增的长度大于右子树的区间长度
}
return max(max(m1,m2),sum1+sum2);
}
}
}
}seg;
int main(){
int t;
cin>>t;
while(t--){
char str;
int l,r;
scanf("%d%d",&n,&m);
for(int i=0;i<n;i++) scanf("%d",&a[i]);
seg.build(1,0,n-1);
while(m--){
cin>>str>>l>>r;
if(str == 'Q'){
printf("%d\n",seg.query(1,l,r));
}else seg.update(1,l,r);
}
}
return 0;
}
-
注
- 个人拙见,如有更好算法还望指出,谢谢!
- 参考博客: