线段树

线段树是一个十分好用的数据结构,很多的高级数据结构都是基于线段树的,所以说把线段树的根基打好了,后面才能学的好一些,首先来说一下这个数据结构在干些什么,这个数据结构包含了2种操作就是修改和查询,每次修改和查询的时间复杂度是 O ( l o g n ) O(logn) O(logn),朴素的做法一般修改的时间的复杂度是 O ( 1 ) O(1) O(1),但是查询的时间基本上是 O ( n ) O(n) O(n)

然后,我来介绍一下这个线段树的构成,并且说一下这些东西都在具体干些什么。

我们就以洛谷上线段树1模板为基础,因为这个模板是基本上可以套在很多东西上的。

题目传送门

线段树,顾名思义,就是一个个线段组成的树。

那么我们就看一下这个题,如果我们想要使用朴素办法来解决这道题,显然每次修改的时间复杂度是 O ( n ) O(n) O(n),每次查询的时间复杂度也是 O ( n ) O(n) O(n)的,显然这种时间复杂度是完全不能满足题目的要求的,所以说我们就需要一个更加高端的数据结构来解决这样一类问题,那就是线段树。

首先我们要知道怎么样把这个树存下来,我们就使用了结构体来存线段树。

代码如下:

struct node
{
	int l,r;
	long long sum,tag;
}t[400010];

这个 l l l, r r r分别表示的是这个区间的左端点和右端点的位置,sum表示的是区间和,tag是区间修改的标记。

然后我们来说一下如何建树。

void build(long long tr,long long l,long long r)
{
	t[tr].l=l;
	t[tr].r=r;
	if(l==r)
	{
		t[tr].sum=a[l];
		return;
	}
	long long mid;
	mid=(l+r)>>1;
	build(tr<<1,l,mid);
	build(tr<<1|1,mid+1,r);
	pushup(tr);
	return;
}

这个就是在进行建树的这一个操作,每次记录下来这个区间的左端点和右端点,然后建左子树和右子树,最后使用pushup更新一下答案。

然后来说一下这个更新的pushup操作

void pushup(long long tr)
{
	t[tr].sum=t[tr<<1].sum+t[tr<<1|1].sum;
	return;
} 

这个操作就是在更新目前已经知道的答案,把左右儿子目前的区间和更新到父亲的区间和上。

然后我们来说一下区间修改的操作。

void change(long long rt,long long l,long long r,long long c)
{
	if(l<=t[rt].l&&t[rt].r<=r)
	{
		t[rt].tag+=c;
		t[rt].sum+=c*(t[rt].r-t[rt].l+1);
		return;
	}
	pushdown(rt);
	long long mid=(t[rt].l+t[rt].r)>>1;
	if(l<=mid) change(rt<<1,l,r,c);
	if(mid<r) change(rt<<1|1,l,r,c);
	pushup(rt);
	return;
}

这个上面的意思就是在 [ l , r ] [l,r] [l,r]的区间上增加c,具体的意思就是,如果这个目前区间是在修改的范围内的话,我们就进行修改,并且将这个结点打上标记,方便以后把标记下传,以为要把这个区间里的所有数都加上c,所以就可以临时记录一下,那么增加的量就是

c*(t[rt].r-t[rt].l+1)

因为这里面是有

(t[rt].r-t[rt].l+1)

个数的。

接下来说一下怎么 p u s h d o w n pushdown pushdown这个标记

void pushdown(long long tr)
{
	if(t[tr].tag)
	{
		t[tr<<1].tag+=t[tr].tag;
		t[tr<<1].sum+=(t[tr<<1].r-t[tr<<1].l+1)*t[tr].tag;
		t[tr<<1|1].tag+=t[tr].tag;
		t[tr<<1|1].sum+=(t[tr<<1|1].r-t[tr<<1|1].l+1)*t[tr].tag;
		t[tr].tag=0;		
	}
}

这个就很好理解。

那么我们就来说说一下如何求区间的和了。

long long query(long long rt,long long l,long long r)
{
	if(t[rt].l>=l&&t[rt].r<=r)
	{
		return t[rt].sum;
	}
	pushdown(rt);
	long long mid;long long ret=0;
	mid=(t[rt].l+t[rt].r)>>1;
	if(l<=mid)ret+=query(rt<<1,l,r); 
	if(mid<r)ret+=query(rt<<1|1,l,r);
	pushup(rt);
	return ret;
}

在查询这个答案的时候,如果我们到的这个区间是在查询的区间里面的话,我们就直接返回这段区间的权值。

但是有一点需要注意的是在往下走的时候一定要把标记都 p u s h d o w n pushdown pushdown掉。

然后我们这个代码就写完了。

我觉得这个 p u s h d o w n pushdown pushdown这件事,正是区间修改的精髓之处,把多次修改的东西累计起来,一次最终修改,可以省去很多很多的时间。

我放一下全部的代码

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cstring>
using namespace std;
struct node
{
	int l,r;
	long long sum,tag;
}t[400010];
long long a[100010];
inline long long read(){
    long long x = 0, sgn = 1;
    char ch = getchar();
    while (ch < '0' || ch > '9'){
        if (ch == '-') sgn = -1;
        ch = getchar();
    }
    while (ch >= '0' && ch <= '9'){
        x = x * 10 + ch - '0';
        ch = getchar();
    }
    return sgn * x;
}
void pushup(long long tr)
{
	t[tr].sum=t[tr<<1].sum+t[tr<<1|1].sum;
	return;
} 
void pushdown(long long tr)
{
	if(t[tr].tag)
	{
		t[tr<<1].tag+=t[tr].tag;
		t[tr<<1].sum+=(t[tr<<1].r-t[tr<<1].l+1)*t[tr].tag;
		t[tr<<1|1].tag+=t[tr].tag;
		t[tr<<1|1].sum+=(t[tr<<1|1].r-t[tr<<1|1].l+1)*t[tr].tag;
		t[tr].tag=0;		
	}
}
void build(long long tr,long long l,long long r)
{
	t[tr].l=l;
	t[tr].r=r;
	if(l==r)
	{
		t[tr].sum=a[l];
		return;
	}
	long long mid;
	mid=(l+r)>>1;
	build(tr<<1,l,mid);
	build(tr<<1|1,mid+1,r);
	pushup(tr);
	return;
}
long long query(long long rt,long long l,long long r)
{
	if(t[rt].l>=l&&t[rt].r<=r)
	{
		return t[rt].sum;
	}
	pushdown(rt);
	long long mid;long long ret=0;
	mid=(t[rt].l+t[rt].r)>>1;
	if(l<=mid)ret+=query(rt<<1,l,r); 
	if(mid<r)ret+=query(rt<<1|1,l,r);
	pushup(rt);
	return ret;
}
void change(long long rt,long long l,long long r,long long c)
{
	if(l<=t[rt].l&&t[rt].r<=r)
	{
		t[rt].tag+=c;
		t[rt].sum+=c*(t[rt].r-t[rt].l+1);
		return;
	}
	pushdown(rt);
	long long mid=(t[rt].l+t[rt].r)>>1;
	if(l<=mid) change(rt<<1,l,r,c);
	if(mid<r) change(rt<<1|1,l,r,c);
	pushup(rt);
	return;
}
int main()
{
	long long n,m;
	n=read();
	m=read(); 
	for(long long i=1;i<=n;i++)
	{
		a[i]=read();
	}
	build(1,1,n);
	for(long long i=1;i<=m;i++)
	{
		long long x;
		x=read();
		if(x==1)
		{
			long long y,z,k;
			y=read();
			z=read();
			k=read();
			change(1,y,z,k);
		}
		if(x==2)
		{
			long long y,z;
			y=read();
			z=read();
			cout<<query(1,y,z)<<endl;
		}
	}
	return 0;
}

关于线段树,线段树还能解决一下区间最大最小值,区间最大字段和类的问题。

比如今年noip2018d1t1就是可以用线段树写的,具体做法就是每次查询一下区间最小值的位置,然后分开处理,这就是区间最值的比较简单的东西,然后放一下代码

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int a[100001];
struct node
{
	int l,r;
	int num;
	int d;
}t[100000<<6];

node merge(node x,node y)
{
	node czn;
	if(x.num<y.num)
	{
		czn.num=x.num;
		czn.d=x.d;
	}
	else
	{
		czn.num=y.num;
		czn.d=y.d;
	}
	return czn;
}

void pushup(int rt)
{
	if(t[rt<<1].num<t[rt<<1|1].num)
	{
		t[rt].num=t[rt<<1].num;
		t[rt].d=t[rt<<1].d;
	}
	else
	{
		t[rt].num=t[rt<<1|1].num;
		t[rt].d=t[rt<<1|1].d;
	}
	return;
}

void build(int l,int r,int rt)
{
	//cout<<l<<" "<<r<<endl;
	t[rt].l=l;
	t[rt].r=r;
	if(l==r)
	{
		t[rt].num=a[l];
		t[rt].d=l;
		return;
	}
	int mid=l+r>>1;
	build(l,mid,rt<<1);
	build(mid+1,r,rt<<1|1);
	pushup(rt);
	return;
}
node query(int l,int r,int rt)
{
	//cout<<l<<" "<<r<<" "<<t[rt].l<<" "<<t[rt].r<<endl;
	//Sleep(1000);
	if(t[rt].l>=l&&t[rt].r<=r)
	{
		return t[rt];
	}
	int mid=t[rt].l+t[rt].r>>1;
	if(mid<l)return query(l,r,rt<<1|1);
	if(mid>=r)return query(l,r,rt<<1);
	return merge(query(l,r,rt<<1),query(l,r,rt<<1|1)); 
}
int work(int l,int r,int h)
{
	int ans;
	if(l>r)return 0;
	if(l==r)return a[l]-h;
	node now=query(l,r,1);
	//cout<<now.d<<" "<<now.num<<" "<<l<<" "<<r<<endl;
	//cout<<now.d<<' '<<now.num<<" "<<l<<" "<<r<<'*'<<endl;
	ans=work(l,now.d-1,now.num)+now.num-h+work(now.d+1,r,now.num);
	//cout<<l<<" "<<r<<" "<<ans<<endl;
	return ans;
}
int main()
{
//	freopen("road.in","r",stdin);
//	freopen("road.out","w",stdout);
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
	}
	build(1,n,1);
	printf("%d\n",work(1,n,0));
	return 0;
}

这个应该不是太难,然后我们来说一下区间最大字段和的问题

GSS1就是一个不带修改的板子题。

题目链接:传送门

关于求最大字段和这类问题,思路比较简单,就是每次求出区间的最大前缀、最大后缀、最大字段和、区间和。

这个是一个怎么样的原理呢?并且是什么解决这个问题呢?

当我们每次更新这个区间的最大字段和的时候,先来更新这个区间的最大前缀和,我们知道了左儿子的最大前缀和区间和,并且知道了右儿子的最大前缀和,那么这个区间的最大前缀和就是 m a x ( t [ r t &lt; &lt; 1 ] . l m a x , t [ r t &lt; &lt; 1 ] . s u m + t [ r t &lt; &lt; 1 ∣ 1 ] . l m a x ) max(t[rt&lt;&lt;1].lmax,t[rt&lt;&lt;1].sum+t[rt&lt;&lt;1|1].lmax) max(t[rt<<1].lmax,t[rt<<1].sum+t[rt<<11].lmax)了,这样很好理解,要么就是左儿子的最大前缀和,要么就是左儿子的区间和加上右儿子的最大前缀和。那么求最大后缀和就是一个远离了。接下来就是最重要的求最大字段和的时候了,首先我们来讨论这个最大字段和的区间位置所在,这个很明显的就是这个最大字段和要么是在左儿子的这个区间内,要么是在右儿子的区间内,要么就是左右儿子都夸着,那么这样就很简单了,我们就只需要求出 t [ t r &lt; &lt; 1 ] . d a t t[tr&lt;&lt;1].dat t[tr<<1].dat t [ r t &lt; &lt; 1 ∣ ] . d a t t[rt&lt;&lt;1|].dat t[rt<<1].dat t [ r t &lt; &lt; 1 ] . r m a x + t [ r t &lt; &lt; 1 ∣ 1 ] . l m a x t[rt&lt;&lt;1].rmax+t[rt&lt;&lt;1|1].lmax t[rt<<1].rmax+t[rt<<11].lmax就可以了,最后求区间的最大字段和的时候注意返回的类型是结构体的类型,这样对于最后结果的查询方便很多了。

那么我就在这里放一下代码:

#include<iostream> 
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;

int n,m;

int a[50030];

struct node
{
	int l,r;
	int lmax,rmax,sum;
	int tot;
}t[50001<<2];

inline int read()
{
  int x=0,f=1;char ch=getchar();
  while (!isdigit(ch)) {if (ch=='-') f=-1; ch=getchar();}
  while (isdigit(ch)) {x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
  return x*f;
}

void pushup(int rt)
{
	t[rt].sum=t[rt<<1].sum+t[rt<<1|1].sum;
	t[rt].lmax=max(t[rt<<1].lmax,t[rt<<1].sum+t[rt<<1|1].lmax);
	t[rt].rmax=max(t[rt<<1|1].rmax,t[rt<<1|1].sum+t[rt<<1].rmax);
	t[rt].tot=max(max(t[rt<<1].tot,t[rt<<1|1].tot),t[rt<<1].rmax+t[rt<<1|1].lmax);
	return;
}

void build(int rt,int l,int r)
{
	t[rt].l=l;
	t[rt].r=r;
	if(l==r)
	{
		t[rt].sum=t[rt].lmax=t[rt].rmax=t[rt].tot=a[l];
		return;
	}
	int mid=l+r>>1;
	build(rt<<1,l,mid);
	build(rt<<1|1,mid+1,r);
	pushup(rt);
	return;
}

node query(int l,int r,int rt)
{
	if(t[rt].l>=l&&t[rt].r<=r)
	{
		return t[rt];
	}
	int mid=t[rt].l+t[rt].r>>1;
	if(l>mid) return query(l,r,rt<<1|1);
	if(r<=mid)return query(l,r,rt<<1);
	node ans,x,y;
	x=query(l,r,rt<<1);y=query(l,r,rt<<1|1);
	ans.sum=x.sum+y.sum;
	ans.tot=max(max(x.tot,y.tot),x.rmax+y.lmax);
	ans.lmax=max(x.lmax,x.sum+y.lmax);
	ans.rmax=max(y.rmax,x.rmax+y.sum);
	return ans;
}

int main()
{
	int n,m;
	n=read();
	for(int i=1;i<=n;i++)
	  a[i]=read();
	build(1,1,n);	
	m=read();
	while(m--)
	{
		int x,y;
		x=read(),y=read();
		printf("%d\n",query(x,y,1).tot);
	}
	return 0;
}

然后GSS3也是一个最大字段和的模板题,只不过这个是带上了修改的功能,也是十分的简单,我就只放修改的代码了。
何况还是单点修改

题目传送门

代码:

void change(int l,int r,int rt,int x)
{
	if(t[rt].l==t[rt].r)
	{
		t[rt].sum=t[rt].tot=t[rt].lmax=t[rt].rmax=x;
		return;
	}
	int mid=t[rt].l+t[rt].r>>1;
	if(l<=mid)
	  change(l,r,rt<<1,x);
	else change(l,r,rt<<1|1,x);
	pushup(rt);
	return;
}

然后线段树基本上就这样了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值