#2020寒假集训#线段树入门(Segment Tree 算法思想)代码笔记

简述

在学习一个新的算法之时,我们首先要理解这个算法是用来解决什么样的问题的,那么对于线段树

  • 我们经常会遇到给出一组数据,让我们求各种值的问题
  • 比如给出10个数,问第5-8个数的和
  • 比如给出10个数,问第5-8个数的最大值
  • 再比如我要把第6个数改成100,问现在的和、最大值等
  • 简单来说就是解决一些数列求值问题(最大子段和问题的模板以后再说,此文暂不做详解)
  • 还有一个小区别就是,一个点更新值和一个区间更新值的处理方法也是不同的喔

为什么要有线段树的算法思想呢?直接for循环遍历求值不香嘛(大家应该都会叭)(๑╹っ╹๑)

  • 罪魁祸首是谁?!当然是时间复杂度啦!
  • 遍历的话如果给出1e6的数据量,进行1e6次的询问,时间复杂度就有1e12,一秒钟之内肯定无法完成
  • 但是如果用线段树的思想呢?折半之后时间复杂度大幅下降,询问次数的1e6不能改变,但一次只要log1e6啦
  • log1e6就是个10以内的个位数,乘以1e6肯定也能满足一秒钟之内运行完程序哒٩(๑>◡<๑)۶

课堂随笔小记

  1. 什么是树——树是不存在环的图
  2. 比较快的想法——折半、分治,logN的时间复杂度当然算是比较理想的啦
  3. 一棵树是有深度的,从跟结点开始,下面的结点一层一层,深度递增
  4. 编号顶上root,左边root2,右边root2+1
  5. 建树用build递归(build(rt2,l,mid)、build(rt2+1,mid+1,rt)),分别是编号,左、右边界
  6. 叶子结点赋完值,祖先结点要被传递
  7. 数据太大不要用线段树做,线段树范围要开4倍以上
  8. 每一个build都有一个PushUp的过程,但只有到出口才会有值,一层层回来PushUp
  9. 判断r的时候是<=mid因为1-10,左边是1-5,右边是6-10,中间值会放到左区间
  10. 每一次判断区间在左子树还是右子树,一层层递归
  11. 如果是左右都有,那就拆成两段递归求和,边界值有mid
  12. 更新数据的时候,函数类似于build,传入的有个k,是更新位置
  13. 函数里的rt是结点编号,初始传入一般是1,根结点位置
  14. 在main里面调用更新函数的时候,初始传入的rt是1吗?调用的时候rt传入的都是根结点编号吗?
    ——传入根结点,习惯都是1
  15. Lazy滞后标签标记区间更新,只推到询问范围,向下更新
  16. PushUp是都要做;PushDown是能不做就不做
    但其实在代码里也是必不可少的,只是PushDown是为了省时间复杂度
  17. 线段树最大连续子段和,可以用模板,也可以用二分求和判断是否等于getsum(后续再做研究)

现在我们用图解来形象化理解线段树叭❥(ゝω・✿ฺ)

建树

建树主要是利用折半的思想,用结构体储存一个区间的左边界和右边界,用下标表示结点编号
然后还要根据题意储存一些 【最大值、和值、最小值、计算值、层数值】 等一系列 权值
这些权值在建树的build函数中都会调用PushUp函数向上传值,并以叶子结点左右边界相等为出口
储存什么权值就写一个怎样的PushUp,要因题而变灵活运用喔
在这里插入图片描述

更新

对于单点更新

主要是要找到更新点的位置赋值,然后靠PushUp一层一层传回根结点
注意要看清题意是更改值还是加减值,区别就在于是直接赋value值还是加上正或负的value值
比如下文两份模板中分别包含的add函数和update函数

对于区间更新

为了不遍历区间进行单点更新(这样复杂度也会大幅上升,如果更新区间有1e6个数据,那就…自求多福叭呜呜呜)
我们需要一个lazy数组,对被要求更新权值的区间进行标记,但不急着更新
等到下一次更新或者询问到了这一结点所涉及的区间
再写一个PushDown函数从上往下靠lazy传递更新值
新的更新涉及的时候要传递PushDown是因为:需要避免更新至被覆盖
询问涉及了再传递(包括新的更新涉及)是因为:更新权值浪费时间复杂度,能不做就不做哈哈哈ヾ(゚∀゚ゞ)

下文所附例题的代码都有详细注释,可以再行仔细研究喔(◕ᴗ◕✿)
在这里插入图片描述

区间查询

区间查询其实就是写一些getsum、getmax或者Query之类的函数
比如求和的时候,每一个结点的权值都会记录下面连着的所有结点的权值和
再比如求最大值的时候,每一个点的权值也会记录下面连着的所有结点的最大值
这样只要对区间从根结点进行折半查找,在左子树还是右子树,或者是跨越两者?
对于跨越的情况,只要把它拆分到左右两部分,分别返回,再根据要求的问题,处理两部分答案即可
比如求和,就把左右返回值相加;求最大值,就用max找出左右返回值更大的那一个
在这里插入图片描述

单点更新模板(含add函数)

来看道例题练练手叭(๑>ڡ<)✿ main函数之前的部分都是模板可以套用的喔

敌兵布阵 HZNU19training题源

Background
C国的死对头A国这段时间正在进行军事演习,所以C国间谍头子Derek和他手下Tidy又开始忙乎了。A国在海岸线沿直线布置了N个工兵营地,Derek和Tidy的任务就是要监视这些工兵营地的活动情况。由于采取了某种先进的监测手段,所以每个工兵营地的人数C国都掌握的一清二楚,每个工兵营地的人数都有可能发生变动,可能增加或减少若干人手,但这些都逃不过C国的监视。
中央情报局要研究敌人究竟演习什么战术,所以Tidy要随时向Derek汇报某一段连续的工兵营地一共有多少人,例如Derek问:“Tidy,马上汇报第3个营地到第10个营地共有多少人!”Tidy就要马上开始计算这一段的总人数并汇报。但敌兵营地的人数经常变动,而Derek每次询问的段都不一样,所以Tidy不得不每次都一个一个营地的去数,很快就精疲力尽了,Derek对Tidy的计算速度越来越不满:"你个死肥仔,算得这么慢,我炒你鱿鱼!”Tidy想:“你自己来算算看,这可真是一项累人的工作!我恨不得你炒我鱿鱼呢!”无奈之下,Tidy只好打电话向计算机专家Windbreaker求救,Windbreaker说:“死肥仔,叫你平时做多点acm题和看多点算法书,现在尝到苦果了吧!”Tidy说:"我知错了。。。"但Windbreaker已经挂掉电话了。Tidy很苦恼,这么算他真的会崩溃的,聪明的读者,你能写个程序帮他完成这项工作吗?不过如果你的程序效率不够高的话,Tidy还是会受到Derek的责骂的.

Input
第一行一个整数T,表示有T组数据。
每组数据第一行一个正整数N(N<=50000),表示敌人有N个工兵营地,接下来有N个正整数,第i个正整数ai代表第i个工兵营地里开始时有ai个人(1<=ai<=50)。
接下来每行有一条命令,命令有4种形式:
(1) Add i j,i和j为正整数,表示第i个营地增加j个人(j不超过30)
(2)Sub i j ,i和j为正整数,表示第i个营地减少j个人(j不超过30);
(3)Query i j ,i和j为正整数,i<=j,表示询问第i到第j个营地的总人数;
(4)End 表示结束,这条命令在每组数据最后出现;
每组数据最多有40000条命令

Output
对第i组数据,首先输出“Case i:”和回车,
对于每个Query询问,输出一个整数并回车,表示询问的段中的总人数,这个数保持在int以内。

Sample Input
1
10
1 2 3 4 5 6 7 8 9 10
Query 1 3
Add 3 6
Query 2 7
Sub 10 2
Add 6 3
Query 3 10
End

Sample Output
Case 1:
6
33
59

#include<stdio.h>
#include<iostream> 
#include<string>
#include<algorithm>
using namespace std;
const int maxn=500005*4;//线段树范围要开4倍

/*
	#小知识# 
	对于ACM竞赛中时间复杂度的要求很高
	所以在写代码的时候任何能压缩时间复杂度的地方都不要放过
	比如!!!
	几个数进行乘除法的时间复杂度就远比进行位运算的复杂度要大
	所以对于乘以2的n次方或者除以2的n次方
	我们更多地是用位移操作来表达
	就像:k*(2的n次方)==k<<n;k/(2的n次方)==k>>n
	左移是乘以,右移是除以
	对于加减运算也可以使用位运算表达
	比如x|1即x+1(这些就是下方模板所运用的一些位运算小技巧) 
*/

struct Tree
{
	int l,r,sum,maxx;
};
/*
	一颗结构体线段树,需要储存的信息如下
	l:线段左范围;r:线段右范围
	sum:线段树结点的求和权值;maxx:线段树结点的求最大值权值 
*/

//两数组对应关系就是某node的l==r的时候,l=r=a的下标 
Tree node[maxn];//node[maxn]为线段树处理数组
int a[maxn];//a[maxn]为原数组
/*
	node数组是结构体类型的,它的下标是结点编号
	存着左范围、右范围、这个结点以下连着的所有结点之和和最大值
	建树初始的时候,结点编号是1(自定义的)
	左范围是a数组的最小下标,右范围是a数组的最大标志
	所以a数组一般是用1-N做下标 
	a数组存输入的数据,它对于node数组来说,是这棵树的叶子结点
	也就是node左右范围相等时,就会分别等于1-N
	这个左右范围相等的时候,具有位移结点编号的这个结点就等于a数组对应下标的值
	也就是结点编号为rt,左右范围l=r=i,则node[rt].sum=a[i];node[rt].maxx=a[i]
	然后靠递归一层一层推到编号为1的根结点
	整棵树就构建完毕啦 
	但问题是如果纯靠递归,只有l==r的时候才有赋值过程
	建树是正着往下靠mid建左子树和右子树 
	然后为了赋值,就要有PushUp这个函数啦
	在l==r出口之后,靠PushUp就能给叶子结点的上一层赋值
	然后递归里面靠着PushUp倒回来就完成向上传值啦 
*/

void PushUp(int i)//向上传值 
{
	node[i].sum=node[i<<1].sum+node[(i<<1)|1].sum;//传和 
	node[i].maxx=max(node[i<<1].maxx,node[(i<<1)|1].maxx);//传最大值
	/*
		其实对值的传递进其他方式的操作也可以 
		关键就是更改这个函数,例如
		node[i].cen=node[i<<1].cen+1;
		if(node[i].cen%2==0) node[i].cal=node[i<<1].cal|node[(i<<1)|1].cal;
		else node[i].cal = node[i << 1].cal ^ node[(i << 1) | 1].cal;
		只要在建树和更新函数里面给叶子结点需要的变量进行初始化赋值就行啦
		但要注意喔,如果是奇偶层不同操作的
		计算node[i].cen时,这个函数是父结点的,所以它得先由子结点的node[i<<1].cen+1
		用左子树右子树都行,因为从下往上赋值的话它们会在同一层,虽然实际上可能某一支特别长的
		而且!!!千万别写成node[i<<1].cen++!!! 
		这个是赋值操作+自加操作,子结点的cen值也变了!!! 
	*/
}

void build(int i,int l,int r)//建树 
{
	node[i].l=l;node[i].r=r;
	if (l==r)
	{
		node[i].maxx=a[l];
		node[i].sum=a[l];
		return;
	}
	int mid=(l+r)/2;
	build(i<<1,l,mid);//递归建左子树 
	build((i<<1)|1,mid+1,r);//递归建右子树  
	//如1-10,mid=5,左边1-5,右边6-10,所以mid归左边 
	PushUp(i);//递归出口后赋值再一层层倒回来向上传值 
}

int getsum(int i,int l,int r)
{
	if(node[i].l==l&&node[i].r==r) return node[i].sum;//某区间正好是询问区间 
	int mid=(node[i].l+node[i].r)/2;//靠折半的mid来找区间在哪 
	if(r<=mid) return getsum(i<<1,l,r);//区间在左子树上,注意mid归左子树 
	else if (l>mid) return getsum((i<<1)|1,l,r);//区间在右子树上 
	else return getsum(i<<1,l,mid)+getsum((i<<1)|1,mid+1,r);//区间横跨左右子树 
}

int getmax(int i, int l, int r)
{//注释同getsum函数 
	if(node[i].l==l&&node[i].r==r) return node[i].maxx;
	int mid=(node[i].l + node[i].r)/2;
	if(r<=mid) return getmax(i<<1,l,r);
	else if(l>mid) return getmax((i<<1)|1,l,r);
	else return max(getmax(i<<1,l,mid),getmax((i<<1)|1,mid+1,r));
}

void add(int i, int k, int v)//当前研究是否更新的结点的编号为i,初始传入的是根结点1 
//(一般是1为初始编号,具体得看建立树时使用的第一个编号是什么)
{//k为需要更新的点的位置,v为修改的值的大小 
	if (node[i].l==k&&node[i].r==k)//左右端点均和k相等,说明找到了k所在的叶子节点
	{
		node[i].sum+=v;
		node[i].maxx+=v;
		/*
			这儿是+v,v是从之前到现在这个结点的权值要变的△值
			不是改成什么值,而是加上v(正的),减去v(负的) 
		*/
		return;//找到了叶子节点就不需要在向下寻找了
	}
	int mid=(node[i].l+node[i].r)/2;
	if (k<=mid) add(i<<1,k,v);
	else add((i<<1)|1,k,v);
	PushUp(i);//一个结点更新完毕之后,勿忘向上传至值,上面的结点也要随之变动 
}

int main()
{
	int T,put;
	cin>>T;
	for(int j=1;j<=T;j++)
	{
		int N;
		cin>>N;
		getchar();
		for(int i=1;i<=N;i++)
		{
			cin>>a[i];
			/*
				输入的是叶子结点的值
				下标递增就行,输入的数据不强制递增 
				而下标只要按顺序这样输入是递增啦 
			*/
		}
		build(1,1,N);//设根结点编号为1,构建最小l为1,最大r为N的线段树 
		int key,value,left,right;
		string sign;
		cout<<"Case "<<j<<":"<<endl;
		while(cin>>sign&&sign!="End")
		{
			scanf("%d %d",&key,&value);
			if(sign=="Add") add(1,key,value);
			else if(sign=="Sub") add(1,key,value*(-1));
			else if(sign=="Query") cout<<getsum(1,key,value)<<endl;
			getchar();
		}
	}
	return 0;
}

区间更新模板(含update函数)

还是来看道例题练练手叭(๑>ڡ<)✿ main函数之前的部分依旧都是模板可以套用的喔

Just a HookHZNU19training题源

Background
In the game of DotA, Pudge’s meat hook is actually the most horrible thing for most of the heroes. The hook is made up of several consecutive metallic sticks which are of the same length.
Now Pudge wants to do some operations on the hook.
Let us number the consecutive metallic sticks of the hook from 1 to N. For each operation, Pudge can change the consecutive metallic sticks, numbered from X to Y, into cupreous sticks, silver sticks or golden sticks.
The total value of the hook is calculated as the sum of values of N metallic sticks. More precisely, the value for each kind of stick is calculated as follows:
For each cupreous stick, the value is 1.
For each silver stick, the value is 2.
For each golden stick, the value is 3.
Pudge wants to know the total value of the hook after performing the operations.
You may consider the original hook is made up of cupreous sticks.

Input
The input consists of several test cases. The first line of the input is the number of the cases. There are no more than 10 cases.
For each case, the first line contains an integer N, 1<=N<=100,000, which is the number of the sticks of Pudge’s meat hook and the second line contains an integer Q, 0<=Q<=100,000, which is the number of the operations.
Next Q lines, each line contains three integers X, Y, 1<=X<=Y<=N, Z, 1<=Z<=3, which defines an operation: change the sticks numbered from X to Y into the metal kind Z, where Z=1 represents the cupreous kind, Z=2 represents the silver kind and Z=3 represents the golden kind.

Output
For each case, print a number in a line representing the total value of the hook after the operations. Use the format in the example.

Sample Input
1
10
2
1 5 2
5 9 3

Sample Output
Case 1: The total value of the hook is 24.

#include<stdio.h>
#include<algorithm>
using namespace std;
typedef long long LL; 
const int maxn=100005;

/*
	#小知识# 
	对于ACM竞赛中时间复杂度的要求很高
	所以在写代码的时候任何能压缩时间复杂度的地方都不要放过
	比如!!!
	几个数进行乘除法的时间复杂度就远比进行位运算的复杂度要大
	所以对于乘以2的n次方或者除以2的n次方
	我们更多地是用位移操作来表达
	就像:k*(2的n次方)==k<<n;k/(2的n次方)==k>>n
	左移是乘以,右移是除以
	对于加减运算也可以使用位运算表达
	比如x|1即x+1(这些就是下方模板所运用的一些位运算小技巧) 
*/

struct Tree//树的结构体构造 
{
	int l,r;
	LL sum;
	int mid()//构建mid函数,实现折半,结构体内的函数名非结构体名就不是用来初始化0的啦 
	{
		return (l+r)>>1;
	}
	/*
		像之前这段代码同名就是用来初始化变量为0的啦~
		出自于Dijkstra迪杰特斯拉算法 
		struct Edge
		{
   			int to,w;
   			Edge(){}//后面别加分号 
    		Edge(int to,int w):to(to),w(w){}//后面别加分号 
		}edge[N<<1];
	*/
}tree[maxn<<2];//线段树范围要开4倍		
LL a[maxn];//a[N]储存原数组
LL lazy[maxn<<2];//lazy用来记录该节点的每个数值应该加多少 

void PushUp(int rt)//向上传值 
{
	tree[rt].sum=tree[rt<<1].sum + tree[rt<<1|1].sum;//这篇代码只做了求和,无求最值 
}
 
void PushDown(int rt,int m)
{
	if(lazy[rt])//判断有无滞后标签
	{//若有滞后标签则其中的值就是这个区间要更改成的新值 
		lazy[rt<<1]=lazy[rt];//滞后标签向左子树传递 
		lazy[rt<<1|1]=lazy[rt];//滞后标签向右子树传递 
		tree[rt<<1].sum=lazy[rt]*(m-(m>>1));
		//标记的是每个更新为多少,左子树的元素个数是要将传入结点的区间长度折半的
		tree[rt<<1|1].sum=lazy[rt]*(m>>1);//所以左右都是对半差1
		lazy[rt]=0;//滞后标签归零 
	}
	//主要思想就是【标记但不操作+询问到了再操作】以此来节省时间复杂度 
}

void build(int l,int r,int rt)
{//注意不同模板的参数顺序不同,比如这个模板的根结点rt最后输入 
	tree[rt].l=l;
	tree[rt].r=r;
	lazy[rt]=0;//初始化lazy标记为0,还未被要求区间更新 
	if (l==r)
	{
		tree[rt].sum=a[l];
		return;
	}
	int m=tree[rt].mid();//用结构体.函数名()进行结构体内部函数操作,此处实现了折半操作 
	build(l,m,(rt<<1));
	build(m+1,r,(rt<<1|1));
	PushUp(rt);//注意不同模板的参数顺序不同,比如这个模板的根结点rt最后输入 
}
 
void update(LL c,int l,int r,int rt)
{
	if(tree[rt].l==l&&tree[rt].r==r)
	{ 
		lazy[rt]=c;//找到了被要求区间更新的区间结点,将其标记,但还未更新 
		tree[rt].sum = c*(r-l+1);
		return;
	}
	if(tree[rt].l==tree[rt].r) return;//这句代码可删除 
	//这句话没啥用的亚子,单点更新的话也会在上一步return,只不过l和r的值一样罢了 
	int m=tree[rt].mid();
	PushDown(rt, tree[rt].r-tree[rt].l+1);
	/*
		第一遍更新的时候,显然用不到这句话
		因为其他点的lazy值都是0,找到了这个区间的结点update也推出了
		如果另外情况下也是这样,当然,用不到这个PushDown的调用
		但是!!!它会在下一次更新到相关区间的时候调用
		比如,我第一次更新了[4,8],第二次要更新[5,6]
		这个时候如果没有这个函数,标记了[4,8]之后,又标记[5,6]
		先进行一次对[6,6]的查询,这个时候会推一次,[5,5]也会被推到 
		但如果再查询[4,8],[5,6]的更新值就被覆盖啦
		[5,5]会变成之前更新的[4,8]的值,就出现bug啦
		所以现在这句话就比如更新了[4,8]之后要更新[5,6] 
		那么在更新[5,6]的时候,由于之前的[4,8]已经被标记,flag非零
		它就会开始往下推啦,推到这次更新的区间update退出为止
		[5,6]的兄弟结点都有了[4,8]的flag,而推过了的[4,8]已被归零 
		其实原本在下一次查询到比[4,8]深的区间的时候就要推的
		现在只是先顺便推好了,操作的层数也只是两次更新层数之差
		不会浪费多少时间复杂度哒 
	*/ 
	if(r<=m) update(c,l,r,rt<<1);
	else if(l>m) update(c,l,r,rt<<1|1);
	else 
	{
		update(c,l,m,rt<<1);
		update(c,m+1,r,rt<<1|1);
	}
	PushUp(rt);
}
 
LL Query(int l,int r,int rt)//这个Query是询问的意思,此模板的询问操作只询问求和 
{//注意不同模板的参数顺序不同,比如这个模板的根结点rt最后输入 
	if (l==tree[rt].l&&r==tree[rt].r)
	{
		return tree[rt].sum;
	}
	int m=tree[rt].mid();
	PushDown(rt,tree[rt].r-tree[rt].l+1);
	LL res=0;
	if(r<=m) res=Query(l,r,rt<<1);//得到的值会是函数递归出口的值 
	//也可以写成+=,因为每次递归的时候都有定义res=0 
	else if (l>m)res=Query(l,r,rt<<1|1);
	else
	{
		res=Query(l,m,rt<<1);//这里是else里的第一行,可以写成+=
		res+=Query(m+1,r,rt<<1|1);
		//但else的第二行必须+=,不然res就不是左子树加右子树,而是左子树换成右子树了 
	}
	return res;
}

int main()
{
	int T,N,Q;
	scanf("%d",&T);
	for(int k=1;k<=T;k++)
	{
		scanf("%d",&N);
		for(int i=1;i<=N;i++)
		{
			a[i]=1;
		}
		build(1,N,1);
		int left,right;
		long long value;
		scanf("%d",&Q);
		for(int i=1;i<=Q;i++)
		{
			scanf("%d %d %lld",&left,&right,&value);
			update(value,left,right,1); 
		}
		printf("Case %d: The total value of the hook is %lld.\n",k,Query(1,N,1));
	}
	return 0;
}
发布了22 篇原创文章 · 获赞 0 · 访问量 410

猜你喜欢

转载自blog.csdn.net/qq_46184427/article/details/104024847
今日推荐