树状数组学习

Posted sure05

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了树状数组学习相关的知识,希望对你有一定的参考价值。

输入一个长度为n的数组,有以下两种操作:
1.输入一个数m,输出数组中下标1~m的前缀和
2.对指定下标的数值进行修改
我们有两种思路,一种是for循环累加,另一种是利用前缀和数组。两种算法多 次操作时间复杂度在O(n^2),我们不妨来用树状数组进行操作。

树状数组介绍(BIT,Fenwick Tree)

定义:是一个查询和修改复杂度都为log(n)的数据结构。主要用于查询任意两位之间的所有元素之和(维护前缀和),但是每次只能修改一个元素的值。(逻辑结构上是一棵树,但实际上只是个数组。)
技术图片
技术图片
其实还可以发现:层数k的值 = 数组C下标的二进制末尾0的个数= 数组C下标的二进制最后一个1的位置技术图片技术图片
了解了以上这些,我们就来看看关于值的修改和求取前缀和。下面先放两张图。
技术图片
技术图片
可以看到,我们以前所学的操作是对整个数值进行操作,所以时间复杂度高达O(n^2),而现在是求取部分和,时间复杂度就降低到O(nlogn)
技术图片

树状数组的运用

技术图片

int lowbit(int x)  {
	return x&(-x); 	//二进制补码与运算,一假即假
} 

技术图片
这是生成树状数组的函数(也是改变数组中数值的函数)

int change(int k,int value)
{
	while(k<=n)
	{
		c[k]+=value;
		k+=lowbit(k);
	}
}

这是区间查询的函数

int sum(int i)
{
	int s=0;
	while(i>0)
	 {
		s+=c[i];
		i-=lowbit(i);
	 } 
	 return s;
}
int qujiansum(int a,int b)
{
	return sum(b)-sum(a-1);
}

在数值原有的基础上改变值(其实和change函数一样)

void add (int i, int val) {		//用add (i)表示A[i] + val
	while (i <= n)  {
		c[i] += val;
		i += lowbit(i);
 	}
}

关于这个函数:
add(i, -A[i]); //将A[i]的值删除
add(i, val); //修改为val
A[i] = val; //修改A[i]的值为val

下面给一个完整的求取前缀和代码

#include<bits/stdc++.h>
using namespace std;
const int MAXN=9999999;
int n,start,end,value;
int c[MAXN];
int lowbit(int x){
	return x&(-x);
}
int change(int k,int value)
{
	while(k<=n)
	{
		c[k]+=value;
		k+=lowbit(k);
	}
}
int sum(int i)
{
	int s=0;
	while(i>0)
	 {
		s+=c[i];
		i-=lowbit(i);
	 } 
	 return s;
}
int qujiansum(int a,int b)
{
	return sum(b)-sum(a-1);
}
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	    {
		 cin>>value;
		 change(i,value);
		}
	int q;
	cin>>q;
	for(int i=1;i<=q;i++){
	cin>>start>>end;
	cout<<qujiansum(start,end);	}
	return 0;
} 

运用树状数组求逆序数

逆序数的简介:对于n个不同的元素,先规定各元素之间有一个标准次序(例如n个 不同的自然数,可规定从小到大为标准次序),于是在这n个元素的任一排列中,当某两个元素的先后次序与标准次序不同时,就说有1个逆序。一个排列中所有逆序总数叫做这个排列的逆序数。

举个例子来尝试理解:技术图片
先用暴力来做一下:

int ans = 0;
for (int i = 1; i <= n; i ++) {
	for (int j = i + 1; j <= n; j ++) {
		if (A[i] < A[j]) {	//后面有多少个数比它小
			ans ++;
		}
	}
}

可以看到,用暴力来做的话时间复杂度为O((n^2)/2) ≈ O(n^2)

那我们不妨再用树状数组来尝试下:

for (int i = n; i >= 1; i --) {	//最后一个数后面没有数
	ans += sum(A[i]-1);	//A[i]-1排除相等的数
	add (A[i], 1);
}

时间复杂度大大减少,为O(n2logn) ≈ O(n*logn)

小结

在很多的情况下,线段树都可以用树状数组实现.凡是能用树状数组的一定能用线段树.当题目不满足减法原则的时候,就只能用线段树,不能用树状数组.例如数列操作如果让我们求出一段数字中最大或者最小的数字,就不能用树状数组了.

感谢明老师的教学

















以上是关于树状数组学习的主要内容,如果未能解决你的问题,请参考以下文章

(填坑)树状数组1

树状数组 浅显学习

树状数组学习

树状数组review学习

数据结构:树状数组 学习笔记

学习笔记1-回顾树状数组与莫队思路