继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

线段树——操作格子(蓝桥杯试题集)

九日王朝
关注TA
已关注
手记 180
粉丝 42
获赞 185

题目链接:

http://lx.lanqiao.cn/problem.page?gpid=T18

题目描述:

有n个格子,从左到右放成一排,编号为1-n。
共有m次操作,有3种操作类型:
1.修改一个格子的权值,
2.求连续一段格子权值和,
3.求连续一段格子的最大值。
对于每个2、3操作输出你所求出的结果。
输入格式
第一行2个整数n,m。
接下来一行n个整数表示n个格子的初始权值。
接下来m行,每行3个整数p,x,y,p表示操作类型,p=1时表示修改格子x的权值为y,p=2时表示求区间[x,y]内格子权值和,p=3时表示求区间[x,y]内格子最大的权值。
输出格式
有若干行,行数等于p=2或3的操作总数。
每行1个整数,对应了每个p=2或3操作的结果。
样例输入
4 3
1 2 3 4
2 1 3
1 4 3
3 1 4
样例输出
6
3
数据规模与约定
对于20%的数据n <= 100,m <= 200。
对于50%的数据n <= 5000,m <= 5000。
对于100%的数据1 <= n <= 100000,m <= 100000,0 <= 格子权值 <= 10000。


解题思路:

乍一看,不怎么难。然而写一波循环超时之后。

https://img2.mukewang.com/5b504bd600011ea401250118.jpg


本题求和、最大值操作显然不能循环计算求取,那么就要想办法再此之前保存这些值。那么问题就来了,我们要怎样保存呢?

先介绍一下线段树:

线段树是一种二叉搜索树,与区间树相似,线段树是建立在线段的基础上,每个结点都代表了一条线段[a,b]。
使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)。而未优化的空间复杂度为2N(这个我表示呵呵)
对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。因此线段树是平衡二叉树,最后的子节点数目为N,即整个线段区间的长度。
线段树至少支持下列操作:
Insert(t,x):将包含在区间 int 的元素 x 插入到树t中;
Delete(t,x):从线段树 t 中删除元素 x;
Search(t,x):返回一个指向树 t 中元素 x 的指针。


好,我们把其代入本题:

首先说一下建树:void build(int i,int l,int r)//引入节点标和左右标 

假设输入n=8,那我们需要构造的线段即[1,8],即我们的建树操作为:build(1,1,8);  //首先从节点1开始,左极限为1,右极限为8

我们的build函数有以下几个操作:

①如果l==r,也就是说,当前引入点左右相等,那么就是一个单独的数,我们就不再需要向下递归

②如果l不等于r,我们需要创造他的2个子节点:

        build(2*i,l,(l+r)/2);       //我们知道二叉树子节点的编号分别为2i,2i+1

        build(2*i+1,(l+r)/2+1,r);   //我们递归构造子节点时,对于奇数线段比如说[1-3],我们有两种选择,[1-2][3]或者[1-2][2-3] 这里我们采取前者。

③递归回归时将其有用的权值返回父节点,父节点针对于子节点的权值构造自身权值

比如这道题的sum与max:

         /* 回归时得到当前i节点的max信息 */    
        LineTree[i].Node_Max=fmax(LineTree[2*i].Node_Max,LineTree[2*i+1].Node_Max);
        /* 回归时得到当前i节点的sum信息 */  
LineTree[i].Node_Sum=LineTree[2*i].Node_Sum+LineTree[2*i+1].Node_Sum;  


接下来我们讲更新:

①当叶子节点更新的时候,所有其父路径直至根节点走要进行一次更新操作,即时间复杂度为O(logN)

②找叶子节点也需要花费O(logN)从根节点递归查找至叶子节点


最后,我们讲查询:

①查询分为点查询,线段查询(即线段左右点是否相等)

②线段查询,该线段可能在树中完全包含,也可能是半包含。例如构造线段[1,2,3,4]

即:

        [1,2,3,4]

   [1,2]        [3,4]

[1]    [2]    [3]    [4]


如果我们想搜寻[1,3]线段,那么可以看到,该树是不存在这样的线段的,那么我们就需要通过[1,2]和[3]来计算[1,2,3]所返回的权值


下面贴出完整代码:


#include<stdio.h>#define N 100010   struct LineTree {	int Node_Max;//i点线段的最大值 	int Node_Sum;//i点线段的和 	int Node_Lpoint;//i点线段的左限		int Node_Rpoint;//i点线段的右限 }LineTree[N*4];              //不是2n-1!!!不是2n-1!!!不是2n-1!!!  int Box[N];    // 盒子 int _max;int _sum;int fmax(int a,int b){	return a>b?a:b;}void build(int i,int l,int r)//引入节点标和左右标 {	LineTree[i].Node_Lpoint=l,	LineTree[i].Node_Rpoint=r;	if(l==r)          //如果左右相等,表示只有一个元素 	LineTree[i].Node_Sum=LineTree[i].Node_Max=Box[l];  //节点记录该单元素 		else    {/* 递归构造左右子树 */   build(2*i,l,(l+r)/2);build(2*i+1,(l+r)/2+1,r);/* 回归时得到当前i节点的max信息 */    LineTree[i].Node_Max=fmax(LineTree[2*i].Node_Max,LineTree[2*i+1].Node_Max);/* 回归时得到当前i节点的sum信息 */  		LineTree[i].Node_Sum=LineTree[2*i].Node_Sum+LineTree[2*i+1].Node_Sum;}}void updata(int i,int x,int y)/*单节点更新    y替换x */   {if(LineTree[i].Node_Lpoint==x&&LineTree[i].Node_Rpoint==x)                  //如果为X格子 {LineTree[i].Node_Sum=LineTree[i].Node_Max=y;             //单格子更新 while(i)                              //递归更新其所有父级路径 {		if(i%2)	{LineTree[i/2].Node_Sum=LineTree[i-1].Node_Sum+LineTree[i].Node_Sum;	LineTree[i/2].Node_Max=fmax(LineTree[i-1].Node_Max,LineTree[i].Node_Max);}	else	{	LineTree[i/2].Node_Sum=LineTree[i+1].Node_Sum+LineTree[i].Node_Sum;	LineTree[i/2].Node_Max=fmax(LineTree[i+1].Node_Max,LineTree[i].Node_Max);}	i/=2;}}else                                 //递归遍历 {i*=2;if(x>LineTree[i].Node_Rpoint)updata(i+1,x,y);elseupdata(i,x,y);}}int readmax(int i,int x,int y)       //区间最大值查询 {if(LineTree[i].Node_Lpoint==x&&LineTree[i].Node_Rpoint==y)_max=fmax(_max,LineTree[i].Node_Max);        //只有一个数                else{   	i*=2;   	if(x<=LineTree[i].Node_Rpoint)            //右区间       	{   		if(y<=LineTree[i].Node_Rpoint)           //完全包含    	readmax(i,x,y);else                  //半包含    	readmax(i,x,LineTree[i].Node_Rpoint);   	}   	i++;if(y>=LineTree[i].Node_Lpoint)            //左区间    	{   		if(x>=LineTree[i].Node_Lpoint)          //完全包含    		readmax(i,x,y);   	else                  //半包含    	readmax(i,LineTree[i].Node_Lpoint,y);}}}int readsum(int i,int x,int y)       //区间和 遍历同上 {if(LineTree[i].Node_Lpoint==x&&LineTree[i].Node_Rpoint==y)_sum+=LineTree[i].Node_Sum;else                    {   	i*=2;   	if(x<=LineTree[i].Node_Rpoint)   	{   		if(y<=LineTree[i].Node_Rpoint)   	readsum(i,x,y);else   	readsum(i,x,LineTree[i].Node_Rpoint);   	}   	i++;if(y>=LineTree[i].Node_Lpoint)   	{   		if(x>=LineTree[i].Node_Lpoint)   		readsum(i,x,y);   	else   	readsum(i,LineTree[i].Node_Lpoint,y);}}}	   	   	int main(){	int i,n,m,p,x,y;	while(~scanf("%d%d",&n,&m))	{		for(i=1;i<=n;i++)		scanf("%d",&Box[i]);build(1,1,n);		while(m--)		{			scanf("%d%d%d",&p,&x,&y);			switch(p)			{				case 1:updata(1,x,y);break;				case 2:_sum= 0;readsum(1,x,y);printf("%d\n",_sum);break;				case 3:_max=-1;readmax(1,x,y);printf("%d\n",_max);break;			}}}}


注意!!这个问题也是困扰了我很久的一个问题,我们申请线段树时候,其空间到底是多少? 


对于线段树来说,N也就是单独的树最终会成为其叶子节点,二叉树所需要的节点是2n-1,那么我们能否认为线段树所需要的空间即2n-1?

然而实时证明,这是错误的。

我们假设构造(1,11)这个线段并对其节点进行标注

(1,11)【节点1】
(1,5)  【节点2】(6,11)【节点3】
(1,3)  【节点4】(4,5)  【节点5】 (6,8)【节点6】(9,11)【节点7】
(1,2)  【节点8】(3)       【节点9】(4)      【节点10】(5)     【节点11】(6,7)【节点12】(8)【节点13】(9,10)【节点14】(11)【节点15】

我们看,节点14也就是(9,10)还需要递归扩展子树

他的子树应该是2n,2n+1,也就是28,29

咦?11个数不应该是21个节点吗?   其实是这样的,在之前的叶子节点(比如8,9)虽然没有子节点了,但是他仍然占有16,17,18,19的位置

为了满足我们的查找规律。所以,我们申请资源的时候不应该设为2n-1


打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP