高级数据结构之线段树

好久没写博客了。。。。。

我登进CSDN。。。

开始写博客

线段树:一种神奇的的东东。。

可以进行区间的各种操作

是一种基于二分思想的二叉树结构,用于区间进行信息统计,比树状数组更加通用的一个东东

线段树的基本结构:

1,线段树每个节点表示一个区间

2,线段树具有唯一的根节点,代表的区间是整个统计范围,如[1,n]

3,线段树的每个叶节点都代表一个长度为1的元区间[x,x]

4,对于每个节点[l,r],其左子树节点为[l,mid],右子树节点为[mid+1,r](mid=(l+r)/2)

如图二叉树的视角

二叉树的建树

  在建树的时候可以维护很多东东,在下面这段代码是维护区间的累加和

struct cow
{
	int l,r;//区间为l到r这一段
	long long sum;//区间累加和
	#define l(x) tree[x].l
	#define r(x) tree[x].r
	#define sum(x) tree[x].sum
        //有点懒
}tree[4*N];
void build(int p,int l,int r)
{
        //l,r如上所述
        //p为所在的的这个区间编号
	l(p)=l;r(p)=r;
	if(l==r){sum(p)=a[l];return ;}
	int mid=(l+r)/2;
	build(p*2,l,mid);//分治开始
	build(p*2+1,mid+1,r);
	sum(p)=sum(p*2)+sum(p*2+1);
        //sum累加
}
build(1,1,n)

线段树的单点修改

先找到叶节点,修改之后一层层向上回溯

void change(int p,int x,int v)
{//x表示要修改的位置,v表示修改成的值
    if(r(p)==x&&l(p)==x){sum(p)=v;return ;}
    //找到位置,把要赋值的全赋一遍,跳出
    int mid=(l(p)+r(p))/2;//寻找中间位置
    if(x>mid)change(p*2+1,x,v);
    else change(p*2,x,v);//两遍递归
    sum(p)=sum(p*2)+sum(p*2+1);//回溯
}

其时间复杂度为O(log(n))

线段树区间查询

举个栗子:

要查询l~r的区间总和,操作顺序如下:

1,先从1~n开始递归

2,若l~r覆盖了当前的节点,回溯把这个加上

3,判断左子树是否存在,如果存在,递归进去,不在,say bye bye

4,判断右子树是否存在,如果存在,递归进去,不在,say bye bye

代码如下:

long long ask(int p,int l,int r)//查询l~r的区间和
{
	if(l<=l(p)&&r>=r(p))return sum(p);//完全覆盖
	long long o=0;
	int mid=(l(p)+r(p))/2;//折中
	if(l<=mid)o+=ask(p*2,l,r);//判断左子树是否存在
	if(r>mid)o+=ask(p*2+1,l,r);//判断右子树是否存在
	return o;//返回
}
aks(1,l,r);

其时间复杂度为O(log(n))

下面来道例题感受一下

题目描述

给定长度为N的数列A,以及M条指令 (N≤≤500000, M≤≤100000),每条指令可能是以下两种之一:

“2 x y”,把 A[x] 改成 y。

“1 x y”,查询区间 [x,y] 中的最大连续子段和,即maxx≤l≤r≤y∑ri=lA[i]maxx≤l≤r≤y∑i=lrA[i]。 对于每个询问,输出一个整数表示答案。

输入格式

第一行两个整数N,M

第二行N个整数Ai

接下来M行每行3个整数k,x,y,k=1表示查询(此时如果x>y,请交换x,y),k=2表示修改

输出格式

对于每个询问输出一个整数表示答案。

样例数据

input

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

output

2
-1

数据规模与约定

对于100%的数据: N≤≤500000, M≤≤100000, |Ai|≤≤1000

时间限制:10s10s

空间限制:256MB

这道题的主要难点是在于要求区间最大的连续段子和,因为是分治的思想,我们不妨假设l~mid和mid+1~r已经被求出

我们可以维护四个信息:

1,sum区间和

2,ans区间最大段子和

3,lamx靠近左端的区间最大段子和

4,rmax靠近右端的区间最大段子和

我们只需要在build函数和chane上改一下就行了

    sum(p)=sum(p*2)+sum(p*2+1);
    lm(p)=max(lm(p*2),lm(p*2+1)+sum(p*2));
    rm(p)=max(rm(p*2+1),rm(p*2)+sum(p*2+1));
    ans(p)=max(max(ans(p*2),ans(p*2+1)),rm(p*2)+lm(p*2+1));

(注意:要在ask里用结构体下传)

代码如下:

#include<bits/stdc++.h>
using namespace std;
const int N=500001;
struct cow
{
	int l,r;
	long long sum,lmax,rmax,ans;
	#define l(x) tree[x].l
	#define r(x) tree[x].r
	#define sum(x) tree[x].sum
    #define ans(x) tree[x].ans
    #define lm(x) tree[x].lmax
    #define rm(x) tree[x].rmax
}tree[4*N];
int n,m,a[N];
void build(int p,int l,int r)
{
	l(p)=l;r(p)=r;
	if(l==r)
	{
	  sum(p)=a[l];
	  ans(p)=a[l];
	  lm(p)=a[l];
	  rm(p)=a[l];
	  return ;
	}
	int mid=(l+r)/2;
	build(p*2,l,mid);
	build(p*2+1,mid+1,r);
	sum(p)=sum(p*2)+sum(p*2+1);
    lm(p)=max(lm(p*2),lm(p*2+1)+sum(p*2));
    rm(p)=max(rm(p*2+1),rm(p*2)+sum(p*2+1));
    ans(p)=max(max(ans(p*2),ans(p*2+1)),rm(p*2)+lm(p*2+1));
}
cow ask(int p,int l,int r)
{
	if(l(p)>=l&&r(p)<=r) return tree[p];
    int mid=l(p)+r(p)>>1;
    if(mid<l)return ask(p<<1|1,l,r);
    if(mid>=r)return ask(p<<1,l,r);
    cow o1,o2,omax;
    o1=ask(p<<1,l,r);
    o2=ask(p<<1|1,l,r);
    omax.ans=max(max(o1.ans,o2.ans),o1.rmax+o2.lmax);
    omax.lmax=max(o1.lmax,o1.sum+o2.lmax);
    omax.rmax=max(o2.rmax,o2.sum+o1.rmax);
    return omax;
}
void change(int p,int x,int v)
{
	if(r(p)==x&&l(p)==x){sum(p)=ans(p)=lm(p)=rm(p)=v;return ;}
	int mid=(l(p)+r(p))/2;
	if(x>mid)change(p*2+1,x,v);
	else change(p*2,x,v);
	sum(p)=sum(p*2)+sum(p*2+1);
    lm(p)=max(lm(p*2),lm(p*2+1)+sum(p*2));
    rm(p)=max(rm(p*2+1),rm(p*2)+sum(p*2+1));
    ans(p)=max(max(ans(p*2),ans(p*2+1)),rm(p*2)+lm(p*2+1));
	return ;
}
int main()
{
	freopen("b.in","r",stdin);
	freopen("b.out","w",stdout);
	cin>>n;cin>>m;
	for(int i=1;i<=n;i++)cin>>a[i];
	build(1,1,n);
	for(int i=1;i<=m;i++)
	{
		int x,y,z;cin>>x>>y>>z;
		if(x==1)
		{
			if(y>z)swap(y,z);
			cow u=ask(1,y,z);
		    cout<<u.ans<<endl;
		}
	    else change(1,y,z);
	}
	return 0;
}

延迟标记

先来道例题:

题目描述

对于数列 A1, A2, ... , AN. 你要进行2个操作:将一个区间的数同加上某个数,输出一段区间的和。

输入格式

第一行2个整数表示数列长度和操作次数. 1 ≤ N,Q ≤ 100000. 第二行为数列 A1, A2, ... , AN. -1000000000 ≤ Ai ≤ 1000000000. 接下来的Q行操作: "C a b c" 表示将 Aa, Aa+1, ... , Ab.加上c. -10000 ≤ c ≤ 10000. "Q a b" 输出区间[a,b]的和。

输出格式

输出所有询问的答案,每行1个。

样例数据

input

10 5
1 2 3 4 5 6 7 8 9 10
Q 4 4
Q 1 10
Q 2 4
C 3 6 3
Q 2 4

output

4
55
9
15

数据规模与约定

时间限制:5s5s

空间限制:128MB

这道题你们会说不就线段树暴力就行了吗?

我呵呵呵

又有人说树状数组

我们讲的是线段树啊

思路

如果暴力,超时妥妥的。。。

现在就要延迟标记了。

我们可以打一个延迟标记,很简单

在维护信息时用一个id表示这段区间要加的但是没加的数,递归道这的时候再跟新这个标记

像这样:

void add(int p)
{
	if(id(p))
	{
		sum(p*2)+=(r(p*2)-l(p*2)+1)*id(p);
		sum(p*2+1)+=(r(p*2+1)-l(p*2+1)+1)*id(p);
		id(p*2)+=id(p);
		id(p*2+1)+=id(p);
		id(p)=0;
	}
}

注意一定要随时想的更新标记,不少写一个就别想调出来了。。。

延迟标记可以优化很多无用的相加,很有用(主要是来用优化时间)

代码如下:

#include<bits/stdc++.h>
using namespace std;
const int N=100005;
struct cow
{
	int l,r;
	long long sum,id;
	#define l(x) tree[x].l
	#define r(x) tree[x].r
	#define sum(x) tree[x].sum
	#define id(x) tree[x].id
}tree[4*N];
int n,m,a[N];
void build(int p,int l,int r)
{
	l(p)=l;r(p)=r;
	if(l==r){sum(p)=a[l];return ;}
	int mid=(l+r)/2;
	build(p*2,l,mid);
	build(p*2+1,mid+1,r);
	sum(p)=sum(p*2)+sum(p*2+1);
}
void add(int p)
{
	if(id(p))
	{
		sum(p*2)+=(r(p*2)-l(p*2)+1)*id(p);
		sum(p*2+1)+=(r(p*2+1)-l(p*2+1)+1)*id(p);
		id(p*2)+=id(p);
		id(p*2+1)+=id(p);
		id(p)=0;
	}
}
long long ask(int p,int l,int r)
{
	if(l<=l(p)&&r>=r(p))return sum(p);
	add(p);long long o=0;
	int mid=(l(p)+r(p))/2;
	if(l<=mid)o+=ask(p*2,l,r);
	if(r>mid)o+=ask(p*2+1,l,r);
	return o;
}
void change(int p,int l,int r,int c)
{
	if(l<=l(p)&&r>=r(p))
	{
		id(p)+=c;
		sum(p)+=(long long)(r(p)-l(p)+1)*c;
		return ;
	}
	add(p);
	int mid=(l(p)+r(p))/2;
	if(r>mid)change(p*2+1,l,r,c);
	if(l<=mid)change(p*2,l,r,c);
	sum(p)=sum(p*2)+sum(p*2+1);
	return ;
}
int main()
{
	freopen("b.in","r",stdin);
	freopen("b.out","w",stdout);
	cin>>n>>m;
	for(int i=1;i<=n;i++)cin>>a[i];
	build(1,1,n);
	for(int i=1;i<=m;i++)
	{
		char z[2];cin>>z;
		if(z[0]=='Q')
		{
			int x,y;cin>>x>>y;
			cout<<(long long)ask(1,x,y)<<endl;
		}
		else
		{
			int x,y,c;
			cin>>x>>y>>c;
			change(1,x,y,c);
		}
	}
	return 0;
}

OK,线段树博大精深,还有扫描线等一些高深的操作,本蒟蒻还没弄懂,这里就不在赘述了。。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值