[洛谷]P3368 【模板】树状数组 2 (#树状数组+差分)

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

题目描述

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

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

2.求出某一个数的值

输入格式

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

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

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

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

操作2: 格式:2 x 含义:输出第x个数的值

输出格式

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

输入输出样例

输入 #1复制

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

输出 #1复制

6
10

说明/提示

时空限制:1000ms,128M

数据规模:

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

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

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

样例说明:

故输出结果为6、10


思路

准备在树状数组模版2详细讲解树状数组。

目录

1.1 概述/前言

1.2 实现树状数组

1.3 树状数组模板1

2.1 差分

2.2 树状数组模版2

3 总结

1.1 概述/前言

树状数组是个好东西,树状数组很完美地将树形结构和二进制联系在一起。树状数组的复杂度都为O(logn),且常数比线段树小,一般为1/2。

刚学树状数组的同学都会认为线段树比树状数组简单。我承认是,我自己也这样觉得,因为你想想,从十进制的角度去看二进制,肯定很别脑。

树状数组用来干嘛呢?树状数组是用数据压缩的思想由二进制实现的数据结构。有单点修改+区间查询或区间修改+单点查询的作用。(可以扩展功能,但是这里就不提及)

树状数组和线段树有什么关系呢?就是弟弟和哥哥的关系。线段树可以实现的功能和扩展功能比树状数组多。本模版还可以使用zkw线段树这个高级的东西,这里也不提及。

好了闲话扯完,进入正题。

1.2 实现树状数组

咱先不管树状数组这种数据结构到底的结构什么,先来了解下lowbit这个函数

电脑中有一种叫做补码的操作(由于电脑是二进制,它们存的相反数是它的取反+1),一个数与它的相反数做与操作时会返回二进制下最右边的1的位置。lowbit这个函数的功能就是求某一个数的二进制表示中最低的一位1。听起来有点抽象,举个例子。

例如有一个数x=6,它的二进制就为110。那么lowbit(x)返回的是6的二进制下,也就是110中10的值:2。因为110最后一个1表示2。

本蒟蒻智商有限,只会套模版:

inline int lowbit(int x)
{
    return x&-x;
}

然后,为了简化区间修改的效率,我们需要建立这样的一个数组bit,换句话说是数据结构:数组bit中第k位的值为原数组中的一段区间和,这个区间的长度是lowbit(k),终点是k。即用循环枚举出与它相关的位置,都增加(减少)即可。

我们就把这种数组bit称为树状数组

 代码

inline void update(int x,int k)
{
	while(x<=n)//修改区间[x,n]的值 
	{
		bit[x]+=k;//第k位数组加上k 
		x+=lowbit(x);
	}
}

听起来有点雾?我们来模拟一下样例。

输入:

5
1 5 4 2 3

输入第1个数a[1]时,x=1,k=1;加上的树状数组数组位数分别为第1位,第2位,第4位。树状数组为:1 1 0 1 0。

输入第2个数a[2]时,x=2,k=5;加上的树状数组数组位数分别为第2位,第4位。树状数组为:1 6 0 6 0。

输入第3个数a[3]时,x=3,k=4;加上的树状数组数组位数分别为第3位,第4位。树状数组为:1 6 4 10 0。

输入第4个数a[4]时,x=4,k=2;加上的树状数组数组位数是第4位。树状数组为:1 6 4 12 0。

输入第5个数a[5]时,x=5,k=3;加上的树状数组数组位数是第5位。树状数组为:1 6 4 12 3。完成。

可以发现的是:

树状数组bit[1]=a[1];

树状数组bit[2]=a[1]+a[2];

树状数组bit[3]=a[3];

树状数组bit[4]=a[1]+a[2]+a[3]+a[4];

树状数组bit[5]=a[5]。

怎么样?是不是对lowbit这个函数影响加深了呢?其实就是上面那张图。a[x]+k同时也会使bit[x]+k,bit[x]+k时bit[x+x的二进制表示中最低的一位1所在位置]同时也会+k。

我们可以发现这个操作的本质就是修改树状数组的值,所以它是单点修改的函数

接下来是查询。注意我们不能直接查询某个数的前缀和,而是要减去区间前缀和差。举个例子。

一个数组={1,5,4,2,3},求区间[3,5]的和。

那我们就要求区间[1,5]与区间[1,3-1]的差。sum([1,5])-sum([1,3-1])=sum([3,5])。

模拟一下。

先求5的前缀和。所求的就是第5(101)项+第4(100)项就是3+12=15。

再求2的前缀和。所求的就是第2(10)项就是6。

最后作差就是15-6=9。

用a数组检验一下,发现a[3...5]=4+2+3=9,是对的。所以求区间[x,y]的前缀和,与求区间[1,x-1]和区间[1,y]的前缀和是一样的。

如果理解了update那么sum也不难理解。

inline ll sum(ll k)
{
	ll i(0);
	while(k>0)
	{
		i+=bit[k];
		k-=lowbit(k);
	}
	return i;
}

1.3 树状数组模版1

#include <stdio.h>
#include <iostream>
#define ll long long int
using namespace std;
ll n,m,s,a[500001],bit[500001];
inline ll lowbit(ll x)
{
	return x&-x;
}
inline void update(ll x,ll k)
{
	while(x<=n)//修改区间[x,n]的值 
	{
		bit[x]+=k;//第k位数组加上k 
		x+=lowbit(x);
	}
}
inline ll sum(ll k)
{
	ll i(0);
	while(k>0)
	{
		i+=bit[k];
		k-=lowbit(k);
	}
	return i;
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	register ll i,j;
	cin>>n>>m;
	for(i=1;i<=n;i++)
	{
		cin>>a[i];
		update(i,a[i]);//在区间[i,n]上加上a[i]的值 
	}
	for(i=1;i<=m;i++)
	{
		int x,y,z;
		cin>>x>>y>>z;
		if(x==1) update(y,z);//虽然只是单点修改,但是树状数组是前缀和思想,所以[y,n]都有影响,全部加上z的值。 
		if(x==2) cout<<sum(z)-sum(y-1)<<endl; 
	}
	return 0;
}

其中update的效率为O(NlogN),单点修改效率为O(logN),查询的效率为O(logN)。本模版程序总效率为O(MlogN)。 

 至此,树状数组模版1已经讲完。

2.1 差分

我们可以看到树状数组模版1的本质是单点修改&区间查询,而树状数组模版2中是区间修改&单点查询,该怎么实现呢?

看起来与之前模版1差不多。但是你自己想一下就会发现模版1直接套模版2会TLE。因为复杂度达到了O(NM)。

突破口:差分。

我其实之前也不会实现差分和它的原理,今天才学的。

下面的文字转载于一位洛谷大佬。

来介绍一下差分

设数组a[]={1,6,8,5,10},那么差分数组b[]={1,5,2,-3,5}

也就是说b[i]=a[i]-a[i-1];(a[0]=0;),那么a[i]=b[1]+....+b[i];(这个很好证的)。

假如区间[2,4]都加上2的话

a数组变为a[]={1,8,10,7,10},b数组变为b={1,7,2,-3,3};

发现了没有,b数组只有b[2]和b[5]变了,因为区间[2,4]是同时加上2的,所以在区间内b[i]-b[i-1]是不变的.

所以对区间[x,y]进行修改,只用修改b[x]与b[y+1]:

b[x]=b[x]+k;b[y+1]=b[y+1]-k;

 2.2 树状数组模版2

#include <stdio.h>
#include <iostream>
#define ll long long int
using namespace std;
ll n,m,s,a[500001],bit[500001];
inline ll lowbit(ll x)
{
	return x&-x;
}
inline void update(ll x,ll k)
{
	while(x<=n)//修改区间[x,n]的值 
	{
		bit[x]+=k;//第k位数组加上k 
		x+=lowbit(x);
	}
}
inline ll sum(ll k)
{
	ll i(0);
	while(k>0)
	{
		i+=bit[k];
		k-=lowbit(k);
	}
	return i;
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	register ll i,j;
	cin>>n>>m;
	for(i=1;i<=n;i++)
	{
		cin>>a[i];
		update(i,a[i]-a[i-1]);//a[i]-a[i-1]=b[i]
		//现在加入树状数组的都是差分数组了 
	}
	for(i=1;i<=m;i++)
	{
		int x,y,z,Case;
		cin>>Case;
		if(Case==1)
		{
			cin>>x>>y>>z;
			update(x,z);//在b数组区间[x,y]进行修改,本质上只要修改b[x]和b[y+1] 
			update(y+1,-z);
			//b[x]=b[x]+z
			//b[y+1]=b[y+1]-z
		}
		if(Case==2)
		{
			cin>>x;
			cout<<sum(x)<<endl;
		}
	}
	return 0;
}

3 总结

可以看出,树状数组的码量比线段树少,且执行效率比线段树略快,主要原因是树状数组利用到了二进制思想和数组树化,线段树的常数主要是由于递归带来的开销导致的。有一种数据结构叫做zkw线段树,专门把递归版线段树改成了非递归的,很牛逼。当然,树状数组的功能是有限的,并没有线段树多。还有一个数据结构叫做“超级树状数组”,扩展了树状数组的功能,我们以后具体研究。

最后,一定要记住:如果看不懂也不要紧,会用就可以。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值