参考博客:膜拜dalao由此进
问题引入:RMQ问题:
题目概述
给定序列a,对于m次询问,每个询问给定x和y,求[x,y]的最大最小值
输入样例:
第一行:两个数n,m表示a的长度和m次询问
接下来一行,n个数,为序列n
接下来m行,每行两个数x,y表示区间范围
5 3
3 5 7 9 6
1 3
2 3
1 5
输出样例
共m行
每一行两个数,maxx和minn,分别表示该次询问的最大最小值
3 7
5 7
3 9
数据保证
0<x<y<=n在这里插入代码片
30% 数据:0<n<=50000
100% 数据:0<n<=107,0<y-x<=106
看到这里请自觉思考3~5分钟,然后继续向下看
解法:
1.暴力求解
直接用for循环模拟
int maxx=-1,minn=INT_MAX;
for(int i=x;i<=y;i++)
{
maxx=max(maxx,a[i]);
minn=min(minn,a[i]);
}
时间复杂度就是O(n)啦
2.分块算法
这个代码实现比较长,这里就不展示了
可以前往我的另一篇blog看看
戳我进入隧道
结构体里面存的信息就是每个块的最大最小值
切入正题:
我们还可以用线段树解决此类问题:
什么是线段树?
线段树就是一种树形结构,大概长这样:
图里面用k来表示每个节点的编号
图中的每个节点(用结构体来存)有两个参数:
l,r表示该节点的左右端点,就是这个节点在序列中的位置
线段树用来干什么?
当然是解决区间问题啦!
RMQ,区间和什么的都可以解决
当然后面还有更加深的算法,本蒟蒻能力有限,涉及不到,多多包涵~
接下来就来看看线段树有哪些基本操作吧!
1.建树
数据结构:结构体存结点
struct node{
int l,r,maxx,minn;
}tree[400000+10];
大小开4n+1
原因是对于每个节点都有左子2k和右子2*k+1
此处的节点信息maxx和minn则对应着例题最大最小值
因为线段树是个满二叉树,所以我们用递归来建树
而线段树的基础在于:二分
函数参数:当前节点的左右端点、结点编号k
对于一个节点的编号,定义它的左儿子编号为2k,右儿子编号为2k+1
建树先左后右(观察叶节点,我们递归到最底层,输入数据存到叶节点中,按照左到右的顺序读入)
对于每个编号下的左右端点,取父节点的mid,作为其子节点的左或右的端点
举个栗子:
当前节点为([l,r])[1,4]
其mid就是2
其左子节点就是([l,mid])[1,2],右子节点就是(mid+1,r)[3,4]
而判断是否为叶节点:l和r相等即为叶节点
代码如下:
void build(int l,int r,int k)
{
tree[k].l=l,tree[k].r=r;
if(l==r)
{
cin>>tree[k].maxx;
return ;
}
int m=(l+r)>>1;
build(l,m,2*k);
build(m+1,r,k*2+1);
tree[k].maxx=max(tree[2*k].maxx,tree[2*k+1].maxx);
}
2.单点更新
原理和建树一样,也是递归到目标叶节点,然后修改这个点的信息
如何递归到目标节点呢?
从根节点开始递归,每次二分取左右端点
如果x比该区间的mid小,就往左子树找,否则往右子树找
递归到x为止
别漏一步:递归回去的时候不断更新!
代码如下:(这里是把a[x]改为y的值)
void updata(int k,int x,int y)
{
if(tree[k].l==tree[k].r)
{
tree[k].maxx=y;
return ;
}
int m=(tree[k].l+tree[k].r)>>1;
if(x<=m) updata(2*k,x,y);
else updata(2*k+1,x,y);
tree[k].maxx=max(tree[2*k].maxx,tree[2*k+1].maxx);
}
注:有些题目里有单点查询的,那么单点查询就是单点修改的基础上,去掉更改节点信息和其他节点的更新
代码如下:
void ask(int k)
{
if(tree[k].l==tree[k].r)
{
ans=tree[k].w;
return ;
}
int m=(tree[k].l+tree[k].r)/2;
if(x<=m) ask(k*2);
else ask(k*2+1);
}
3.区间查询
这个是我认为理解起来比较有难度的一个操作
拿本章问题的引入来举例吧
查询区间[x,y],从根出发的递归查找有以下三种情况:
- 当前节点的区间信息[l,r]左右端都大于[x,y]:如下图
这种情况,就需要当前节点的左右子节点继续向下寻找,一直找到像情况二这样的节点
2.当前节点的区间信息[l,r]左右端只有一端大于x或y:如下图
对于这种情况,就要取[l,r]的mid,在这个基础下比较x是否大于mid,如果取mid还是比x小的话那就再取,一直取到情况三
还是取[l,r]的mid,在这个基础下比较y是否大于mid,如果取mid还是比y大的话那就再取,一直取到情况三
3.这是最理想的情况![l,r]完全等于或者被[x,y]包着,如下图
这时候就直接将所查到的信息记录进ans中
代码如下
void find(int k,int x,int y)
{
if(tree[k].l>=x && tree[k].r<=y)
{
ans=max(ans,tree[k].maxx);
ans2=min(ans2,tree[k].minn);
return ;
}
int mid=(tree[k].l+tree[k].r)>>1;
if(x<=mid) find_max(2*k,x,y);
if(y>mid) find_max(2*k+1,x,y);
}
下面放出例题的线段树代码:
#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;
int read()
{
int x=0,f=1; char ch=getchar();
while(ch<'0' || ch>'9'){ if(ch==-1) f=-1; ch=getchar();}
while(ch>='0' && ch<='9') {x=x*10+ch-'0'; ch=getchar();}
return x*f;
}
int n,m;
struct node{
int l,r,maxx,minn;
}tree[400000+10];
int ans=-1;
int ans2=INT_MAX;
void build(int l,int r,int k)
{
tree[k].l=l,tree[k].r=r;
if(l==r)
{
cin>>tree[k].maxx;
return ;
}
int m=(l+r)>>1;
build(l,m,2*k);
build(m+1,r,k*2+1);
tree[k].maxx=max(tree[2*k].maxx,tree[2*k+1].maxx);
tree[k].minn=min(tree[2*k].minn,tree[2*k+1].minn);
}
void find(int k,int x,int y)
{
if(tree[k].l>=x && tree[k].r<=y)
{
ans=max(ans,tree[k].maxx);
ans=min(ans,tree[k].minn);
return ;
}
int mid=(tree[k].l+tree[k].r)>>1;
if(x<=mid) find(2*k,x,y);
if(y>mid) find(2*k+1,x,y);
}
void updata(int k,int x,int y)
{
if(tree[k].l==tree[k].r)
{
tree[k].maxx=y;
return ;
}
int m=(tree[k].l+tree[k].r)>>1;
if(x<=m) updata(2*k,x,y);
else updata(2*k+1,x,y);
tree[k].maxx=max(tree[2*k].maxx,tree[2*k+1].maxx);
}
int main()
{
n=read();
build(1,n,1);
m=read();
for(int i=1;i<=m;i++)
{
int f=read(),x=read(),y=read();
ans=-1; find(1,x,y); cout<<ans<<" "<<ans2<<endl;}
}
return 0;
}
本篇就介绍到这里,多多练习,多打模板,自然就对线段树掌握了