数据结构之树状数组(绝对让你理解清楚)

 

 数据结构之树状数组


目录

1:引子~~~

2:树状数组的基本概念

3:树状数组的实现及基本操作

4:树状数组的应用

1.数星星

2.求逆序对

5:树状数组的拓展

<1> :区间修改,单点查询

<2>区间修改,区间查询

6:树状数组的例题

洛谷P1774 最接近神的人

洛谷P3608 [USACO17JAN]Balanced Photo G

洛谷P3368 【模板】树状数组 2

洛谷P2184 贪婪大陆


1:引子~~~

假如现在需要你维护一种数据结构,需要能支持区间求和和单点

更新,那么该怎么办?

方法一:暴力枚举

不用想,TLE妥妥的

方法二:线段树

额,本篇不予讨论

那该怎么办呢?

当当当当,树状数组闪亮登场。


2:树状数组的基本概念

你可能会好奇,树状数组到底是树还是数组?

树状数组,顾名思义是用数组模拟的树形结构,其本质是一个一维数组。

为什么不用树维护呢?

用不着嘛,你肯定不会用动规,二分,模拟退火写A+B吧(貌似还真有用树状数组写的)

咳咳,不多说了额

树状数组可以解决大部分基于区间上的更新以及求和问题

树状数组可以解决的问题都可以用线段树解决,这两者的区别在哪里呢?

树状数组区间修改和查询的复杂度都是O(logN),相比线段树系数要少很多,比传统数组要快,而且容易写。

但是遇到复杂的区间问题还是不能解决,功能还是有限。

发个图来更好的理解一下树状数组

如上图,其中A是普通数组,C是树状数组(此图只是方便理解,真正存的样子也是一条线)

这个图是什么意思呢?

我们先看编号是奇数的

很显然:

C[1]=A[1]

C[3]=A[3]

C[5]=A[5]

C[7]=A[7]

再看看编号是偶数的

C[2]=C[1]+A[2]=A[1]+A[2]

C[4]=C[2]+C[3]+A[4]=A[1]+A[2]+A[3]+A[4]

C[6]=C[5]+A[6]=A[5]+A[6]

C[8]=C[4]+C[6]+C[7]+A[8]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8]

你可能会好奇为什么是这样的关系,不着急,慢慢来。

其实,树状数组的核心也是二进制

不信?来,继续上图

细心的同学不难发现红色数字就是编号的数字对应的二进制数

那跟树状数组有什么关系?这个父子关系是怎么来的?

来,我们把每个数的二进制数加上他二进制表示法中最后一个1(例如1100要加上100,101要加上1,10要加上10)

1:1+1=10=2

2:10+10=100=4

3:11+1=100=4

4:100+100=1000=8

5:101+1=110=6

6:110+10=1000=8

7:111+1=1000=8

怎么样~~~是不是很神奇,这个关系就是这样推出来的


3:树状数组的实现及基本操作

这种取出最后一个1的操作叫做lowbit,lowbit(X)就是取出(十进制数)X的最后一个1;

那么问提又来了,这个操作怎么实现?

其实也很简单 

lowbit(X)=X&-X

为什么?

假设x的二进数是10001100

那么-x就是把x取反再加1=01110100

再相与=000001000=100

看100,就是10001100的最后一个1

代码实现:

int void(int x)
{
    return x&-x;
}

好了那么树状数组的基本操作也可以开始啦!

洛谷P3374 【模板】树状数组 1

题目描述

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

  • 将某一个数加上 x

  • 求出某区间每一个数的和

输入格式

第一行包含两个正整数 n,m分别表示该数列数字的个数和操作的总个数。

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

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

  • 1 x k 含义:将第 x个数加上k

  • 2 x y 含义:输出区间 [x,y]内每个数的和

输出格式

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

输入输出样例

输入 

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

输出 

14
16

对于 100% 的数据,1≤n,m≤5×105。

我们用树状数组维护区间和,单点修改很简单,但是注意不能只修改一个点,要把他的父节点也修改

代码如下

void update(int x,int z)
{
    for(int i=x;i<=n;i+=lowbit(i))
        c[i]+=z;
}

区间求和其实也很简单利用前缀和思想,我们必须求前Y项和和前X-1项的和

例如求前六项和:

其实就等于C[6]+C[4]

这两个数有什么关系?(往二进制想)

其实就是减掉末尾的1,和更新正好相反

每次都减掉末尾的1,直到都减没了(也就是等于0),同时把数组的值加上去

看一下代码:

int query(int x)
{
    int ans=0;
    for(int i=x;i;i-=lowbit(i))
        int+=c[i];
    return int;
}

完整代码如下:

#include<bits/stdc++.h>
using namespace std;
long long n,q,c[1000500],a,w,s;
int lowbit(int x)
{
	return x & -x;
}
void update(int x,int z)
{
	for(int i=x;i<=n;i+=lowbit(i))
		c[i]+=1ll*z;
}
long long query(int x)
{
	long long ans=0;
	for(int i=x;i;i-=lowbit(i))
		ans+=c[i];
	return ans;
}
int main()
{
	cin>>n>>q;
	for(int i=1;i<=n;i++)
	{
		scanf("%lld",&a);
		update(i,a);
	}
	while(q--)
	{
		scanf("%lld%lld%lld",&a,&w,&s);
		if(a==1)
		{
			update(w,s);
		}
		else
		printf("%lld\n",query(s)-query(w-1));
	}
	return 0;
}

4:树状数组的应用

1.数星星

题目描述

天文学家经常要检查星星的地图,每个星星用平面上的一个点来表示,每个星星都有坐标。我们定义一个星星的“级别”为给定的星星中不高于它并且不在它右边的星星的数目。天文学家想知道每个星星的“级别”。

                            5
                          *
                        4
                      *
                    1       2   3
                  *       *   *

例如上图,5号星的“级别”是3(1,2,4这三个星星),2号星和4号星的“级别”为1。

给你一个地图,你的任务是算出每个星星的“级别”。

输入格式

输入的第一行是星星的数目N(1<=N<=60000),接下来的N行描述星星的坐标(每一行是用一个空格隔开的两个整数X,Y,0<=X,Y<=32000)。

星星的位置互不相同。星星的描述按照Y值递增的顺序列出,Y值相同的星星按照X值递增的顺序列出。

输出格式

输出包含N行,一行一个数。第i行是第i个星星的“级别”

input

5
1 1
5 1
7 1
3 3
5 5

output

0
1
2
1
3

           由于题目对输入x,y的限制,所以我们不需要管用,直接用树状数组维护比x小的个数就好了,但要注意x可能=0,出现卡死的情况,所以维护树状数组的时候需要加1.

话不多说,上代码;

#include<bits/stdc++.h>
using namespace std;
int n,x,y,c[35000];
int lowbit(int x)
{
	return x & -x;
}
void update(int x)
{
	for(int i=x;i<=32000;i+=lowbit(i))
		c[i]++;
}
int query(int x)
{
	int ans=0;
	for(int i=x;i;i-=lowbit(i))
	ans+=c[i];
	return ans;
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d%d",&x,&y);
		printf("%d\n",query(x+1));
		update(x+1);
	}
	return 0;
}

2.求逆序对

洛谷P1908 逆序对

题目描述

猫猫 TOM 和小老鼠 JERRY 最近又较量上了,但是毕竟都是成年人,他们已经不喜欢再玩那种你追我赶的游戏,现在他们喜欢玩统计。

最近,TOM 老猫查阅到一个人类称之为“逆序对”的东西,这东西是这样定义的:对于给定的一段正整数序列,逆序对就是序列中 a_i>a_j​ 且 i<j的有序对。知道这概念后,他们就比赛谁先算出给定的一段正整数序列中逆序对的数目。注意序列中可能有重复数字。

Update:数据已加强。

输入格式

第一行,一个数 n,表示序列中有 n个数。

第二行 n 个数,表示给定的序列。序列中每个数字不超过 10^9。

输出格式

输出序列中逆序对的数目。

输入输出样例

输入 #1复制

6
5 4 2 6 3 1

输出 #1复制

11

说明/提示

对于 25% 的数据,n≤2500

对于 50% 的数据,n≤4×104。

对于所有数据,n≤5×105

请使用较快的输入输出


首先暴力不说了

第一种正解是归并排序,但在此我们不做讨论

第二种方法是用树状数组维护在1~i-1中小于等于a[i]的个数

再用总个数(i-1)减去这个这个个数,再加起来,就是逆序对数量

注意,数字的大小是1e9,数组开不了这么大,需要离散化

代码如下

#include<bits/stdc++.h>
using namespace std;
const int maxn=5e5+10;
int n,a[maxn],p[maxn];//p是离散化数组
int c[maxn];
long long ans=0;
bool cmp(int x,int y)
{
  return a[x]<a[y];
}
int lowbit(int x)
{
  return x&-x;
}
void update(int x,int v)
{
  for(long long i=x;i<=n;i+=lowbit(i))
		c[i]+=v;
}
long long query(long long x)
{
	long long ans=0;
	for(long long i=x;i;i-=lowbit(i))
	ans+=c[i];
	return ans;
}
int main()
{ 
  scanf("%d",&n);
  for(int i=1; i<=n; i++) 
  {
     scanf("%d",&a[i]);
     p[i]=i;
  }
  stable_sort(p+1,p+n+1,cmp);//离散化处理
  for(int i=1; i<=n; i++) 
  a[p[i]]=i;
  for(int i=1; i<=n; i++)
  {
    	ans=ans+i-1-query(a[i]);
		update(a[i],1);
  }
  printf("%lld\n",ans);
  return 0;
}

5:树状数组的拓展

<1> :区间修改,单点查询

我们都知道差分是D[i]=a[i]-a[i-1]

然后修改只需要让D[L]+X,D[R]-X(为什么?我不想解释)

查询只要求个前缀和就行了

那么这种思想也可以在树状数组应用

我们用树状数组维护差分数组

代码如下

#include<bits/stdc++.h>
using namespace std;
int n,m,a[50005],c[50005]; //对应原数组和树状数组 
int lowbit(int x)
{
    return x&-x;
}
void updata(int k,int x) 	//区间修改 
{   
    for(int i=k;i<=n;i+=lowbit(i))
    c[i]+=x;
} 
int query(int k)    //单点求和 
{    
    int ans = 0;
	for(int i=k;i;i-=lowbit(i))
    ans+=c[i];
    return ans;
}
int main()
{
	 int f,x,y,k;
     cin>>n>>m;    
	 for(int i=1;i<=n;i++)
	 {
        cin>>a[i];
        updata(i,a[i]-a[i-1]);  //构造树状数组 
	 }
	 for(int i=1;i<=m;i++)
	 {
	 	cin>>f;
	 	if(f==1)//区间修改 
	 	{
	 		cin>>x>>y>>k;
	 		updata(x,k); //类似于差分的修改 
			updata(y+1,-k);
	 	}
	 	else
	 	{
	 		cin>>k;
	 		cout<<query(k)<<endl; 
	 	}
	 }
	return 0;
}

<2>区间修改,区间查询

和上面类似,这里不说了,可以自己尝试写写

实在不会可以去搜一搜

6:树状数组的例题

洛谷P1774 最接近神的人

洛谷P3608 [USACO17JAN]Balanced Photo G

洛谷P3368 【模板】树状数组 2

洛谷P2184 贪婪大陆

好了,你听懂了吗?

猜你喜欢

转载自blog.csdn.net/jwg2732/article/details/107876297