寒假笔记·线段树与树状数组

线段树与树状数组

线段树和树状数组,是两个十分相似的数据结构。他们能使对一个区间的数修改以及查询的速度提升许多。两个结构本质相同,各有优缺点。
线段树
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)。
比如讲一个有4个数的线段树,是长这个样子的:
在这里插入图片描述
一号节点,代表着区间1~4

二号节点,代表区间1~2

三号节点,代表区间3~4

以此类推。。。。。。

很容易发现,对于n号节点来说,n×2代表着它的区间的前半段,n×2+1代表着它的区间的后半段。

树状数组

树状数组是一个很奇特的树,它的节点会比线段树少一些,也能表示一个数组。
c数组就是树状数组,能看出来。
c1=a1;
c2=a1+a2;
c3=a3;
c4=a1+a2+a3+a4;
以此类推。。。。。。 很难说出他们的关系,但是如果把它们变为二进制:
c0001=a0001
c0010=a0001+a0010
c0011=a0011
c0100=a0001+a0010+a0011+a0100
你会发现,将每一个二进制,去掉所有高位1,只留下最低位的1,然后从那个数一直加到1。
总结

时间复杂度

虽然它们都是nlogn,但是,你会发现,在查询时,树状数组最坏情况是logn(比如8个数,然后查询8),但是线段树是所有情况都是nlogn,稍慢于树状数组。

空间复杂度

树状数组完胜于线段树,线段树要开2倍到4倍内存(推荐4倍),但是树状数组一倍就够了。

适用范围

线段树之所以存在的理由是因为它能适用于很多方面,不仅仅是区间、单点的查询修改,还有标记等等,可以用于模拟、DP等等,而且空间经过离散化以后也可以相对压缩,所以适用范围线段树更加广一些。
详细讲解

例题:P2068 统计和

原题地址
题目描述

给定一个长度为n(n<=100000),初始值都为0的序列,x(x<=10000)次的修改某些位置上的数字,每次加上一个数,然后提出y (y<=10000)个问题,求每段区间的和。时间限制1秒。

输入输出格式

输入格式:
第一行1个数,表示序列的长度n

第二行1个数,表示操作的次数w

后面依次是w行,分别表示加入和询问操作

其中,加入用x表示,询问用y表示

x的格式为"x a b" 表示在序列a的位置加上b

y的格式为"y a b" 表示询问a到b区间的加和

输出格式:
每行一个数,分别是每次询问的结果

输入输出样例

输入样例#1:
5
4
x 3 8
y 1 3
x 4 9
y 3 4
输出样例#1:
8
17
代码:
树状数组做法:

#include <iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
int tree[100010],n,m;
int lowbit(int x)
{
    return x&(-x);//能整出这个数的2的幂次的最大幂
}
void add(int p,int x)//建立线段树
{
    while(p<=n)
    {
        tree[p]+=x;
        p+=lowbit(p);
    }
}
int ask(int p)//查询
{
    int ans=0;
    while(p>0)
    {
        ans+=tree[p];
        p-=lowbit(p);
    }
    return ans;
}
int main()
{
    int i,a,b,q;
    char k;
    cin>>n>>m;
    memset(tree,0,sizeof(tree));
    for(i=1;i<=m;i++)
    {
        cin>>k>>a>>b;
        if(k=='x') add(a,b);
        if(k=='y')
        {
            q=ask(b)-ask(a-1);
            printf("%d\n",q);
        }
    }
    return 0;
}

树状数组解决区间最大值问题

例题:P1531 I Hate It

原题地址

题目背景

很多学校流行一种比较的习惯。老师们很喜欢询问,从某某到某某当中,分数最高的是多少。这让很多学生很反感。

题目描述

不管你喜不喜欢,现在需要你做的是,就是按照老师的要求,写一个程序,模拟老师的询问。当然,老师有时候需要更新某位同学的成绩

输入输出格式

输入格式:
第一行,有两个正整数 N 和 M ( 0<N<=200000,0<M<5000 ),分别代表学生的数目和操作的数目。学生ID编号分别从1编到N。第二行包含N个整数,代表这N个学生的初始成绩,其中第i个数代表ID为i的学生的成绩。接下来有M行。每一行有一个字符 C (只取’Q’或’U’) ,和两个正整数A,B。当C为’Q’的时候,表示这是一条询问操作,它询问ID从A到B(包括A,B)的学生当中,成绩最高的是多少。当C为’U’的时候,表示这是一条更新操作,如果当前A学生的成绩低于B,则把ID为A的学生的成绩更改为B,否则不改动。

输出格式:
对于每一次询问操作,在一行里面输出最高成绩

输入输出样例

输入样例#1:
5 6
1 2 3 4 5
Q 1 5
U 3 6
Q 3 4
Q 4 5
U 2 9
Q 1 5
输出样例#1:
5
6
5
9

代码:

#include<cstdio>
#include<cmath>
#include<iostream>
#include<cstring>
using namespace std;
int dd[200020],aa[200020];//dd[]存储树状数组,aa存储原数组
int n,m;
char c;
int lowbit(int x)
{
    return x&(-x);
}
int main()
{
    int i,j,a,b,ans;
    cin>>n>>m;
    for(i=1;i<=n;i++)
        cin>>aa[i];
    memset(dd,0,sizeof(dd));
    for(i=1;i<=n;i++)//建立树状数组
    {
        for(j=i;j<=n;j+=lowbit(j))
        {
            dd[j]=max(aa[i],dd[j]);
        }
    }
    for(i=1;i<=m;i++)
    {
        cin>>c>>a>>b;
        if(c=='U')
        {
            aa[a]=max(b,aa[a]);//不可少的一行
            for(j=a;j<=n;j=j+lowbit(j))
            {
                dd[j]=max(b,dd[j]);
            }
        }
        else
        {
            ans=0;
            while(a<=b)
            {
                while(b-lowbit(b)>=a)
                {
                    ans=max(ans,dd[b]);
                    b=b-lowbit(b);
                }
                ans=max(ans,aa[b]);//是aa,不是dd
                b--;
            }
            cout<<ans<<endl;
        }
    }
    return 0;
}

差分

然而,对于一些较为简单的题目,如果查询并不复杂,可以使用差分的方法。

例题:P2367 语文成绩

原题地址

题目描述

语文老师总是写错成绩,所以当她修改成绩的时候,总是累得不行。她总是要一遍遍地给某些同学增加分数,又要注意最低分是多少。你能帮帮她吗?

输入输出格式

输入格式:
第一行有两个整数n,p,代表学生数与增加分数的次数。

第二行有n个数,a1~an,代表各个学生的初始成绩。

接下来p行,每行有三个数,x,y,z,代表给第x个到第y个学生每人增加z分。

输出格式:
输出仅一行,代表更改分数后,全班的最低分。

输入输出样例

输入样例#1:
3 2
1 1 1
1 2 1
2 3 1
输出样例#1:
2
说明

对于40%的数据,有n<=1000

对于60%的数据,有n<=10000

对于80%的数据,有n<=100000

对于100%的数据,有n<=5000000,p<=n,学生初始成绩<=100,z<=100

代码:
区间修改?乍一看似乎要用线段树才能解决。不过读完题目,就会发现只需要在最后求出最小值,并不需要线段树这样的在线算法。

于是自然而然想到差分了,这样只需要O(n)的时间复杂度就可以解决这道题了。

AC代码:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
int qq[5000050],di[5000050];
int n,p;
using namespace std;
int main()
{
    int i,l,r,t,ans,k;
    scanf("%d%d",&n,&p);
    qq[0]=0;
    for(i=1;i<=n;i++)
    {
        scanf("%d",&qq[i]);
        di[i]=qq[i]-qq[i-1];
    }
    for(i=1;i<=p;i++)
    {
        scanf("%d%d%d",&l,&r,&t);
        di[l]+=t;
        di[r+1]-=t;
    }
    ans=k=di[1];
    for(i=2;i<=n;i++)
    {
        k+=di[i];
        if(k<ans) ans=k;
    }
    printf("%d\n",ans);
    return 0;
}

线段树模板

例题:P3372 【模板】线段树 1

原题地址

题目描述

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

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数据范围内)

代码:
lazy_tag的思想是,我们把一个区间拆成多个区间,每个区间打上一个标记,标记它的叶子节点要加上多少,它自己的值可以在O(1)的时间内算出,等到我们要使用它的儿子时,再把标记下压到它的儿子上 首先,我们需要一个更新自己的push_up()函数
然后,我们需要一个push_down()函数,用于下压标记。
由于博主太懒,详细讲解请转至详细讲解

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<string>
#include<map>
#include<vector>
using namespace std;
long long int t,n,qq[100010],sum[400010],tag[400010];
void push_up(long long int now)
{
	sum[now]=sum[now*2]+sum[now*2+1];
}
void build(long long int now,long long int l,long long int r)
{
	if(l==r) sum[now]=qq[l];
	else
	{
		long long int mid=(l+r)/2;
		build(now*2,l,mid);
		build(now*2+1,mid+1,r);
		push_up(now);
	}
}
void push_down(long long int now,long long int l,long long int r)
{
	if(tag[now])
	{
		long long int mid=(l+r)/2;
		sum[now*2]=sum[now*2]+(mid-l+1)*tag[now];
		sum[now*2+1]=sum[now*2+1]+(r-mid)*tag[now];
		tag[2*now]+=tag[now];
		tag[2*now+1]+=tag[now];
		tag[now]=0;
		push_up(now);
	}
}
void update(long long int now,long long int l,long long int r,long long int q_l,long long int q_r,long long int x)
{
	if(l>=q_l&&r<=q_r)
	{
		sum[now]=sum[now]+(r-l+1)*x;
		tag[now]=tag[now]+x;
	}
	else
	{
		push_down(now,l,r);
		long long int mid=(l+r)/2;
		if(mid>=q_l) update(2*now,l,mid,q_l,q_r,x);
		if(mid<q_r) update(2*now+1,mid+1,r,q_l,q_r,x);
		push_up(now);
	}
}
long long int get_sum(long long int now,long long int l,long long int r,long long int q_l,long long int q_r)
{
	long long int ans=0;
	if(q_l<=l&&q_r>=r) return sum[now];
	push_down(now,l,r);
	long long int mid=(l+r)/2;
	if(mid>=q_l) ans=ans+get_sum(now*2,l,mid,q_l,q_r);
	if(mid<q_r) ans=ans+get_sum(now*2+1,mid+1,r,q_l,q_r);
	push_up(now);
	return ans;
}
int main()
{
	long long int i,j,x,fl,a,b;
	cin>>n>>t;
	for(i=1;i<=n;i++) cin>>qq[i];
	build(1,1,n);
	while(t--)
	{
		cin>>fl;
		if(fl==1)
		{
			cin>>a>>b>>x;
			update(1,1,n,a,b,x);
		}
		else
		{
			cin>>a>>b;
			cout<<get_sum(1,1,n,a,b)<<endl;
		}
	}
    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值