数据结构-线段树(一)

参考博客:膜拜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
原因是对于每个节点都有左子2
k和右子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],从根出发的递归查找有以下三种情况:

  1. 当前节点的区间信息[l,r]左右端都大于[x,y]:如下图
    这种情况,就需要当前节点的左右子节点继续向下寻找,一直找到像情况二这样的节点情况三

2.当前节点的区间信息[l,r]左右端只有一端大于x或y:如下图情况二1
对于这种情况,就要取[l,r]的mid,在这个基础下比较x是否大于mid,如果取mid还是比x小的话那就再取,一直取到情况三
情况二2
还是取[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;
}

本篇就介绍到这里,多多练习,多打模板,自然就对线段树掌握了

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值