[洛谷]P3372 【模板】线段树 1 (#线段树)

31 篇文章 0 订阅
17 篇文章 0 订阅

题目描述

如题,已知一个数列,你需要进行下面两种操作:

1.将某区间每一个数加上x

2.求出某区间每一个数的和

输入输出格式

输入格式:

第一行包含两个整数N、M,分别表示该数列数字的个数和操作的总个数。

第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。

接下来M行每行包含3或4个整数,表示一个操作,具体如下:

操作1: 格式:1 x y k 含义:将区间[x,y]内每个数加上k

操作2: 格式:2 x y 含义:输出区间[x,y]内每个数的和

输出格式:

输出包含若干行整数,即为所有操作2的结果。

输入输出样例

输入样例#1

5 5
1 5 4 2 3
2 2 4
1 2 3 2
2 3 4
1 1 5 1
2 1 4

输出样例#1

11
8
20

说明

时空限制:1000ms,128M

数据规模:

对于30%的数据:N<=8,M<=10

对于70%的数据:N<=1000,M<=10000

对于100%的数据:N<=100000,M<=100000

(数据已经过加强^_^,保证在int64/long long数据范围内)

样例说明:


思路

让我们从这道板子题开始,详解线段树入门。

目录

1.1 概述/前言

1.2 实现线段树

1.3 模板

1.1 概述/前言

现在有3道题。

1.题目描述:有n个数和一个序列a(范围1~n)。求在a[i]中某些区间在范围数的和。(n<=10^6)

A.pro:这他喵的还不简单,前缀和,搞定。

2.题目描述:有n个数,一个序列a(范围1~n),以及k个操作。在区间[l,r]全部内加一个数,并且求区间[l,r]的和。(n<=10^4)

A.pro:emm好吧,前缀和肯定不行了。

前缀和:求和操作时间复杂度:O(1);区间修改时间复杂度:O(n)。这复杂度显然是我们不希望看到的。

3.题目描述:有n个数,一个序列a(范围1~n),以及k个操作。在区间[l,r]全部内加一个数,并且求区间[l,r]的和。(n<=10^6)

A.pro:艹。

所以,我们需要一种强大的数据结构,使得完美潇洒地完成题目3的问题。

其实可以引入分块的做法,即把数组分成若干个块求解问题。

线段树!!!

线段树,之所以称之为树,是因为它本身是二叉树。它本质上是把分块数组树形化。线段树,在各个节点保存一个区间,可以高效地解决区间修改问题。由于二叉结构的特性,它的每次操作都使得时间复杂度近似于O(logN)。由于是一棵二叉树,每个节点的信息都会被logN左右的节点记录。空间消耗较大,一般是4n。所以线段树是一个典型的空间换时间的数据结构。

下图是一个区间[1,10]的线段树。

当我们需要对一个区间进行操作时,同样要将其化成若干个小区间,这个就是分块思想。正如前面所说,线段树就是把分块思想树形化,对任何一个信息都可以达到logN的速度。(但其实常数较大)

对于每一个子节点而言,都表示整个序列中的一段子区间;对于每个叶子节点而言,都表示序列中的单个元素信息;子节点不断向自己的父亲节点传递信息,而父节点存储的信息则是他的每一个子节点信息的整合。

1.2 实现线段树

1.2.1 存储

由于线段树是一棵二叉树,所以存储结构完全可以借鉴那样存。

由于二叉树的自身特性,对于每个父亲节点的编号i,他的两个儿子的编号分别是2i和2i+1,所以我们考虑写两个O(1)的取儿子函数:(这里用的位运算)

inline ll int leftnode(ll int p) {return p<<1;}//左节点(左儿子) 
inline ll int rightnode(ll int p) {return p<<1|1;}//右节点(右儿子) 

相反,如果我们要找父亲节点,就可以把左儿子除以2就能得到。

1.2.2 建树

接下来我们考虑建立一棵线段树。

建树的操作就是不断二分,直到遍历到叶子节点。到了叶子节点后,可以干我们想维护的事情。比如求区间和,RMQ,RSQ等。之后我们再回溯上去,其他节点都得跟着更新。

inline void push_up(ll int node)//维护父子节点之间的逻辑关系(合并2个儿子节点) 
{
	tree[node]=tree[leftnode(node)]+tree[rightnode(node)];//区间和 
	//tree[node]=min(tree[leftnode(node)],tree[rightnode(node)]);//最小值
}
void build(ll int node,ll int start,ll int end)//建树,node是当前节点,start和end是范围(是指a数组的范围)
{//线段树自底向上回溯,所以线段树的叶子节点在会被赋值,如果左右区间相同(start==end),说明这是叶子节点 
	if(start==end)
	{
		tree[node]=a[start];//区间和
		minx[node]=a[start];//区间最小值 
		maxx[node]=a[start];//区间最大值 
		return; 
	}
	else
	{
		register ll int mid=(start+end)>>1;
		build(leftnode(node),start,mid);//左子树
		build(rightnode(node),mid+1,end);//右子树,把当前根节点的儿子分别当成新节点,继续建立线段树
		push_up(node);//维护线段树(区间和) 
		//区间最小值与最大值代码类似 
	}
}

根据线段树的服务对象,就写出了push_up这一函数。push_up操作的目的是为了维护父子间的逻辑关系。当我们递归建树时,对于每一个节点我们都需要遍历一遍,并且电脑中的递归实际意义是先向底层递归,然后从底层向上回溯,所以开始递归之后必然是先去整合子节点的信息,再向它们的祖先回溯整合之后的信息。

所以push_up是在整合2个子节点的信息。

所以这也就说明建树时应该递归建树,这样才能保存父亲节点和子节点之间的信息。在建树的时候我们就应该维护父子节点的逻辑关系。

1.2.3 区间修改

在这个模版中,是要求我们累加。所以我们可以仿照建树的思路,不断二分,如果修改的区间包括了当前区间,就回溯,否则继续二分。

inline void lazy_ad(ll int node,ll int start,ll int end,ll int value)
{
	tag[node]+=value;//懒标记,后面会讲
	tree[node]+=value*(end-start+1);//由于是这个区间统一改变,所以要加上元素个数次 
}//被包括的区间进行操作
void update(ll int node,ll int start,ll int end,ll int cl,ll int cr,ll int value)//区间修改,node是当前节点,start和end是范围(是指a数组的范围),cl和cr是要修改的区间,value是改成哪个数值
{//其实与建树类似,我们这题维护的修改操作是区间累计,二分+回溯,如果修改的区间包含了目前遍历的区间就回溯,否则二分 
	if(cl<=start && end<=cr)//如果修改区间包含当前区间 
	{
		lazy_ad(node,start,end,value);//直接加
		return; 
	}
	else//如果不包含当前区间 
	{
		register ll int mid=(start+end)>>1; 
		push_down(node,start,end,mid);//标记下传,后面会讲
		if(cl<=mid)
		{
			update(leftnode(node),start,mid,cl,cr,value);
		}
		if(mid<cr)
		{
			update(rightnode(node),mid+1,end,cl,cr,value);
		}
		push_up(node);//维护线段树
	}
}

1.2.4 区间查询

模板是让我们求区间和,那么就和区间修改的思路一样,不断二分,如果修改区间包括目前遍历的区间,那就返回这一区间的区间和,然后回溯。把询问到的叶子节点或被包含的区间相加就是答案。

ll int query(ll int node,ll int start,ll int end,ll int cl,ll int cr)//node是当前节点,start和end是范围(是指a数组的范围),L和R是在区间[L,R]里计算和
{//和区间修改一样,不断二分,如果修改区间包括当前区间,就返回这一区间的区间和,然后回溯。把访问到的叶子节点或被包含的区间相加就是答案 
	//cout<<"start="<<start<<endl;
	//cout<<"end="<<end<<endl;
	//cout<<endl;
	if(start>=cl && end<=cr)//如果修改的区间包括当前遍历的区间 
	{
		return tree[node];//返回这一区间的区间和 
	}
	register ll int mid=(start+end)>>1,s(0);
	push_down(node,start,end,mid);//标记下传
	if(cl<=mid)
	{
		s=s+query(leftnode(node),start,mid,cl,cr);//把"s+"去掉也是等价的。因为s初值为0,我先遍历的是左子树,然后query是不断递归寻址,最后再给s的 
	}
	if(mid<cr)
	{
		s=s+query(rightnode(node),mid+1,end,cl,cr);//左右子树元素累加 
	}
	return s;//返回s总值 
}

1.2.5 懒标记下传

其实还有永久化标记,那样的话就是主席树了(可持久化线段树)。

线段树的优点不在于全记录,那样的话就是O(n)了,显然不是我们希望的。而在于传递式记录。

标记下传(push_down)的本质和前面的操作时一样的,整个区间都被操作,记录在公共祖先节点上;只修改了一部分,那么就记录在这部分的公共祖先上;如果四环以内只修改了自己的话,那就只改变自己。我们查询子节点的时候再进行更新,也就是我们不需要查询的时候就下传标记。再将使用的标记清0。

注意访问任何一个节点的时候,都需要保证该节点的祖先标记都被清空。

inline void push_down(ll int node,ll int start,ll int end,ll int mid)//标记下传,node是当前节点 
{//线段树的优点不在于全记录,那样的话复杂度达到O(n);而是在与传递式记录
//本质和build,update之类的一样,如果操作一段区间,那只要记录在这段区间的公共祖先上(单点也是区间,长度为1)
//当我们需要查询子节点的信息时我们再更新,不需要查询的时候就打下传标记,要用的时候使用标记,再将使用的标记清0
//但是访问任何一个节点的时候,都要保证该节点的祖先节点已经被清空
//这,就是懒标记(lazy tag) ,简单粗暴省时间。 
	if(tag[node]==0)
	{
		return;
	}
	lazy_ad(leftnode(node),start,mid,tag[node]);
	lazy_ad(rightnode(node),mid+1,end,tag[node]);
	tag[node]=0;
}

1.3 模板

#include <stdio.h>
#include <iostream>
#define ll long long 
#define maxn 100005
using namespace std;
ll int a[maxn],tree[maxn<<2],tag[maxn<<2],n,s,m;//a是序列,tree是区间和,tag是标记数组 
ll int minx[maxn<<2],maxx[maxn<<2];//区间最小值和最大值,需要时会用到 
inline ll int leftnode(ll int p) {return p<<1;}//左节点(左儿子) 
inline ll int rightnode(ll int p) {return p<<1|1;}//右节点(右儿子) 
inline void push_up(ll int node)//维护父子节点之间的逻辑关系(合并2个儿子节点) 
{
	tree[node]=tree[leftnode(node)]+tree[rightnode(node)];//区间和 
	//tree[node]=min(tree[leftnode(node)],tree[rightnode(node)]);//最小值 
}
inline void lazy_ad(ll int node,ll int start,ll int end,ll int value)//区间累加 
{
	tag[node]+=value;//懒标记 
	tree[node]+=value*(end-start+1);//由于是这个区间统一改变,所以要加上元素个数次 
}//被包括的区间进行操作 
inline void push_down(ll int node,ll int start,ll int end,ll int mid)//标记下传,node是当前节点 
{//线段树的优点不在于全记录,那样的话复杂度达到O(n);而是在与传递式记录
//本质和build,update之类的一样,如果操作一段区间,那只要记录在这段区间的公共祖先上(单点也是区间,长度为1)
//当我们需要查询子节点的信息时我们再更新,不需要查询的时候就打下传标记,要用的时候使用标记,再将使用的标记清0
//但是访问任何一个节点的时候,都要保证该节点的祖先节点已经被清空
//这,就是懒标记(lazy tag) ,简单粗暴省时间。 
	if(tag[node]==0)
	{
		return;
	}
	lazy_ad(leftnode(node),start,mid,tag[node]);
	lazy_ad(rightnode(node),mid+1,end,tag[node]);
	tag[node]=0;
}
void build(ll int node,ll int start,ll int end)//建树,node是当前节点,start和end是范围(是指a数组的范围)
{//线段树自底向上回溯,所以线段树的叶子节点在会被赋值,如果左右区间相同(start==end),说明这是叶子节点 
	if(start==end)
	{
		tree[node]=a[start];//区间和
		minx[node]=a[start];//区间最小值 
		maxx[node]=a[start];//区间最大值 
		return; 
	}
	else
	{
		register ll int mid=(start+end)>>1;
		build(leftnode(node),start,mid);
		build(rightnode(node),mid+1,end);//把当前根节点的儿子分别当成新节点,继续建立线段树
		push_up(node);//维护线段树(区间和) 
		//区间最小值与最大值代码类似 
	}
}
void update(ll int node,ll int start,ll int end,ll int cl,ll int cr,ll int value)//区间修改,node是当前节点,start和end是范围(是指a数组的范围),cl和cr是要修改的区间,value是改成哪个数值
{//其实与建树类似,我们这题维护的修改操作是区间累计,二分+回溯,如果修改的区间包含了目前遍历的区间就回溯,否则二分 
	if(cl<=start && end<=cr)//如果修改区间包含当前区间 
	{
		lazy_ad(node,start,end,value);//直接加 
		return; 
	}
	else//如果不包含当前区间 
	{
		register ll int mid=(start+end)>>1; 
		push_down(node,start,end,mid);//标记下传
		if(cl<=mid)
		{
			update(leftnode(node),start,mid,cl,cr,value);
		}
		if(mid<cr)
		{
			update(rightnode(node),mid+1,end,cl,cr,value);
		}
		push_up(node);//维护线段树
	}
}
ll int query(ll int node,ll int start,ll int end,ll int cl,ll int cr)//node是当前节点,start和end是范围(是指a数组的范围),L和R是在区间[L,R]里计算和
{//和区间修改一样,不断二分,如果修改区间包括当前区间,就返回这一区间的区间和,然后回溯。把访问到的叶子节点或被包含的区间相加就是答案 
	//cout<<"start="<<start<<endl;
	//cout<<"end="<<end<<endl;
	//cout<<endl;
	if(start>=cl && end<=cr)//如果修改的区间包括当前遍历的区间 
	{
		return tree[node];//返回这一区间的区间和 
	}
	register ll int mid=(start+end)>>1,s(0);
	push_down(node,start,end,mid);//标记下传
	if(cl<=mid)
	{
		s=s+query(leftnode(node),start,mid,cl,cr);//把"s+"去掉也是等价的。因为s初值为0,我先遍历的是左子树,然后query是不断递归寻址,最后再给s的 
	}
	if(mid<cr)
	{
		s=s+query(rightnode(node),mid+1,end,cl,cr);//左右子树元素累加 
	}
	return s;//返回s总值 
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	register ll int i;
	cin>>n>>m;//有n个数,m个操作 
	for(i=1;i<=n;i++)
	{
		cin>>a[i];
	}
	build(1,1,n);//我要建立一棵树,从1号节点出发,范围是a数组的1~n
	//for(i=1;i<=15;i++)//查看有没有建树成功,父亲节点与两个儿子节点的关系是2n和2n+1(当从1开始计数时)。
	//{
	//	cout<<"tree["<<i<<"]="<<tree[i]<<endl;
	//}
	//cout<<endl;
	for(i=1;i<=m;i++)//处理m条操作 
	{
		ll int f,start,end,k;
		cin>>f>>start>>end;
		if(f==1)
		{
			cin>>k;
			update(1,1,n,start,end,k);
		}
		if(f==2)
		{
			cout<<query(1,1,n,start,end)<<endl;
		}
	}
	return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值