树状数组

问题引入

树状数组是一种实现起来比较简单的高级数据结构。

我们知道,对于一个数组a[i],其前缀和s[i]表示a数组里面前i个元素之和,而求区间l到r的元素之和可以用s[r] - s[l-1]来求。

现在有个单点修改,区间查询的问题,也就是修改原始数组a中某个元素,然后查询某段区间内元素之和。

暴力做法修改a中某个元素的时间复杂度是O(1),查询区间和复杂度是O(n);如果将前缀和数组利用起来,那么查询区间和操作的复杂度固然可以降为O(1),但是单次修改操作后更新前缀和数组的复杂度却是O(n)的。

现在我们需要实现修改和查询操作复杂度均是O(logn)的一个算法。

lowbit运算

二进制位运算可以很简单的实现一些功能,比如x & (x - 1)就可以将x的二进制表示中最后一个1去掉,而lowbit运算则可以只保留一个整数的最后一个1,lowbit(x) = -x & x是怎么实现这个功能的呢?(如果不想了解其中原理,可以跳过后面三段,减轻思维负担。)

首先,二进制的编码方式分为原码、补码和反码,正数的原码与补码、反码相同,负数的反码等于原码除符号位外按位取反,负数的补码等于原码除符号位外按位取反后加上1,而计算机中存储的就是整数的补码形式。

比如x = 0,1010,开头的0表示符号位,-x的原码就是1,1010,反码是1,0101,补码就是1,0110,

那么x & -x = 0,0010,与x的0,1010相比只保留了最后一个1.更加清楚的解释就是开始x最后一个1后面跟着若干个0,比如说1000对x按位取反后最后一个1变成了0,后面的数都是1,也就是0111,再加上个1就会连续进位成为1000,和x最开始的样子保持一致,而x的最后一个1之前的元素都取反了与x相与自然都变成了0.这样就保留了x的最后一个1。

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

树状数组的定义

现在我们知道了lowbit(x)可以提取x的最后一个1,那么要如何在对数时间复杂度内实现单点修改和区间查询操作呢?我们先来分析下使用前缀和数组进行单点修改的复杂度为什么是O(n)的,s[i]存储着前i个元素的和,换句话说,s[i]管理着前i个元素,而a[i]被后面n - i个元素管理着。如果我们修改了a[i],那么从s[i]一直到s[n]这n - i + 1个元素的前缀和都改变了,归根结底就是因为s[n]存储着n个元素的和,一旦修改,牵一发而动全身。如果我们定义一个新的前缀和数组tr[n]来存储n前面logn个元素的和呢?那么相当于tr[i]只管理着logi个元素,只有这logi个元素其中的一个改变了,tr[i]才会改变,修改的复杂度就会大大降低了。

现在我们定义tr[n]为第n个元素及其之前的lowbit(n) - 1个元素之和,比如n = 6时,6 = (0110),lowbit(6) = (10) = 2,tr[6] = a[6] + a[5]。

如图所示,图中的C数组就是我们说的tr数组,我们将tr[i]管理的元素视为i的孩子节点,比如tr[6]是求第五个和第六个元素的和,那么tr[5]就是tr[6]的孩子节点。为什么tr[i]要管理着前面的lowbit(i)个元素呢?这是由二进制分组决定的,比如7 = 0111 = 0001 + 0010 + 0100 = 1 + 2 + 4,我们将前七个元素分为三组,第一组7,第二个6 5,第三组4 3 2 1,可以看出,每组的个数恰好是lowbit的值,lowbit(7) = 1,剩下6个元素,lowbit(6) = 2,剩下4个元素,lowbit(4) = 4。也就是:

tr[7] = a[7]
tr[6] = a[6] + a[5]
tr[4] = a[4] + a[3] + a[2] + a[1]

我们求前7个元素之和s7 = tr[7] + tr[6] + tr[4],求前x个元素之和时,x中有几个1,lowbit运算就会执行几次,前x个元素也就会分成多少组,而x中1的个数必然不超过x的字长,也就是区间求和操作最多对logn个元素进行求和,实现了求和的O(logn)的复杂度。

那么修改操作的复杂度呢?我们知道对于节点x而言,其前面的lowbit(x) - 1个元素都是x的孩子节点,它们的改变都会引起tr[x]的改变,所以我们修改了x,对于下标小于x的元素而言,是没有影响的,因为tr[i]只会管理前面的元素,修改x,首先改变的就是a[x],继而改变了tr[x],然后一路沿着x的父节点往上,影响着x的祖先节点的值,那么如何求x的父节点呢?也就是管理着x的节点。

比如tr[12],12 = 1100,lowbit(12) = 4,也就是12管理着12,11,10,9四个节点,对于其中的任一个孩子节点比如10 = 1010而言lowbit(10) = 2,10 + 2 = 12,10的父节点就是12,再比如9 = 1001,lowbit(9) = 1,9 + 1 = 10也就是说,9的父节点是10。lowbit(9) = 1,所以9只管理着自己,然后10管理着10,9,12管理着12,11,10,9,再之后就是16管理着前16个元素。我们得出的结论是,节点x的父节点是x + lowbit(x)。

树状数组的实现

如果不想理解细节,只考虑实现,只需要理解三点:

  • lowbit(x)表示取x的最后一个1
  • tr[x]管理着x前面的lowbit(x)个元素
  • x在树状数组中的父节点是x + lowbit(x)

知道了这三点,我们就可以实现树状数组的插入和查询操作了。

插入x,只会影响x和其祖先节点,相当于自下而上更新tr数组

void add(int x,int c){for(;x <= n;x += lowbit(x)) tr[x] += c;
}

查询前x个元素之和,相当于给x分组,第一组是x到x - lowbit(x) + 1,第二组是x - lowbit(x)到x - lowbit(x) - lowbit(x - lowbit(x)) + 1,…。第一组元素之和是tr[x],第二组元素之和是tr[x-lowbit(x)],…。将各组元素之和加起来就是我们要求的前x个元素之和了。

int query(int x){int res = 0;for(;x;x -= lowbit(x)) res += tr[x];return res;
}

至于区间和,求出前r个元素之和和前l - 1个元素之和,相减就得到了区间和。

树状数组的核心操作代码就只要寥寥几行,还是很简洁的。当然这只是树状数组在单点更新,区间求和上的应用,倘若要求区间最值,改变下树状数组的定义即可,可以定义为tr[i]为i及其左边的lowbit(i)个元素中的最值,同样是一个元素管理lowbit个元素,复杂度和区间求和一致。

树状数组应用之求逆序对的个数

比如有一个数组3 4 2 5 6 1,我们要求这个数组里有多少个逆序对,对于某个元素,可以遍历之后的元素,统计比该元素小的元素个数,就可以求出逆序对的个数了。我们也可以使用hash的方法来每个元素坐标有多少个比它大的元素,遍历到3,cnt[3] = 1,然后cnt[4] = 1,遍历到2时,如果数组内的元素都在一定的范围内,我们就可以遍历cnt数组中2之后的位置,统计下此时cnt[2]之后有多少个元素,然后发现有2个元素,因此2的坐标比它大的有两个元素,这种统计一个数左边有多少个比它大或者比他小的元素,需要对cnt数组求和,由于在遍历过程中我们需要不断的更新cnt数组,所以也属于单点更新,区间查询问题,可以使用树状数组来解决。

初始情况cnt[i] = 0,cnt数组是原始数组,tr[i]数组是cnt数组生成的的树状数组。原始数组cnt的值都是0,表示没有元素出现,遍历到3,树状数组3右边的元素之和还是0,所以3左边没有比3大的元素,将3插入到树状数组中,原始数组cnt[3] = 1;继续遍历4,cnt数组里4右边的元素和还是0,将4加入树状数组,cnt[4] = 1;接着遍历2,此时树状数组里面比2大的有两个,就求出了2左边比它大的元素个数。

下面一道题就是树状数组在求逆序对数目上的经典应用。

题目描述

在完成了分配任务之后,西部 314 来到了楼兰古城的西部。

相传很久以前这片土地上(比楼兰古城还早)生活着两个部落,一个部落崇拜尖刀(V),一个部落崇拜铁锹(∧),他们分别用 V 和 ∧ 的形状来代表各自部落的图腾。

西部 314 在楼兰古城的下面发现了一幅巨大的壁画,壁画上被标记出了 n 个点,经测量发现这 n 个点的水平位置和竖直位置是两两不同的。

西部 314 认为这幅壁画所包含的信息与这 n 个点的相对位置有关,因此不妨设坐标分别为 (1,y1),(2,y2),…,(n,yn),其中 y1∼yn 是 1 到 n 的一个排列。

西部 314 打算研究这幅壁画中包含着多少个图腾。

如果三个点 (i,yi),(j,yj),(k,yk) 满足 1≤i<j<k≤n 且 yi>yj,yj<yk,则称这三个点构成 V 图腾;

如果三个点 (i,yi),(j,yj),(k,yk) 满足 1≤i<j<k≤n 且 yi<yj,yj>yk,则称这三个点构成 ∧ 图腾;

西部 314 想知道,这 n 个点中两个部落图腾的数目。

因此,你需要编写一个程序来求出 V 的个数和 ∧ 的个数。

输入格式
第一行一个数 n。

第二行是 n 个数,分别代表 y1,y2,…,yn。

输出格式
两个数,中间用空格隔开,依次为 V 的个数和 ∧ 的个数。

数据范围
对于所有数据,n≤200000,且输出答案不会超过 int64。
y1∼yn 是 1 到 n 的一个排列。

输入样例:
5
1 5 3 2 4
输出样例:
3 4

分析

统计V的数目与统计逆序对的方法如出一辙,我们可以枚举中间元素,一共有n种情况,比如中间元素是x,在数组里x的左边有a个比x大的元素,a的右边有b个比x大的元素,那么我们可以在a的左边比它大的元素中任选一个,再在x的右边比它大的元素中任选一个,就可以构成V结构了,根据乘法原理一共有ab中组合方案。还需要求 ∧的数目,因此我们需要统计的是每个元素左边、右边比它大和比它小的元素的数目。

本题有个比较好的条件“y1∼yn 是 1 到 n 的一个排列”,这就保证了y的取值各不相同且有规律可循,我们用数组数组查询出了第i元素左边比它小的元素个数t1,而i的左边一共有i - 1个元素,其中t1个比它小,则剩下的i - 1 - t1个元素就都比它大了。y数组里一共n个元素,范围都不超过n,则比yi小的元素一共yi - 1个,既然第i个元素左边有t1个比它小的元素,那么右边比它小的元素个数就是t2 = yi - 1 - t1,同理可求出右边比它大的元素是n - i - t2个。这样一来一次查询操作就求出了第i个元素左右两边比它大和比它小的元素个数了。

代码

#include <cstdio>
using namespace std;
const int N = 200005;
typedef long long ll;
int a[N],tr[N];
int n;
int lowbit(int x){return -x & x;
}
void add(int x,int c){for(;x <= n;x += lowbit(x)) tr[x] += c;
}
int query(int x){int res = 0;for(;x;x -= lowbit(x)) res += tr[x];return res;
}
int main(){scanf("%d",&n);for(int i = 1;i <= n;i++)   scanf("%d",&a[i]);ll res1 = 0,res2 = 0;for(int i = 1;i <= n;i++){ll t1 = query(a[i]-1),t2 = i - 1 - t1;res1 += t2 * (n - a[i] - t2);res2 += t1 * (a[i] - 1 - t1);add(a[i],1);}printf("%lld %lld\n",res1,res2);return 0;
}

AcWing 241 楼兰图腾(树状数组详解)相关推荐

  1. 【数据结构】树状数组详解(Leetcode.315)

    前言 最近做题时遇到一个关于树状数组的题力扣https://leetcode-cn.com/problems/count-of-smaller-numbers-after-self/但是CSDN上仅有 ...

  2. 楼兰图腾(树状数组)

    题目描述: 思路: 题目字很多,但是题意并不难懂,其实我们就是要找当前数前面和后面分别有多少数大于或小于当前数字,然后再将找到的两个数量相乘就是当前数字能组成的图腾数量. 例如: 1 2 3 1 2 ...

  3. acwing-241. 楼兰图腾-树状数组板子题+开脑洞

    原题链接 题意:给你一组数,让你找出所有aiak的个数与ai>aj<ak的个数. 思路: 看到这个题瞬间想到了三层的暴力(暴力破万物),但是n是200000,跑三层服务器会跑吐的,必然超时 ...

  4. szu 寒训第二天 树状数组 二维树状数组详解,以及树状数组扩展应用【求逆序对,以及动态第k小数】

    树状数组(Binary Index Tree) 树状数组可以解决可以转化为前缀和问题的问题 这是一类用以解决动态前缀和的问题 (有点像线段树简版) 1.对于 a1 + a2 + a3 + - + an ...

  5. 树状数组详解(附图解,模板及经典例题分析)

    导言 深藏于算法与数据结构中的思想非常的美妙,尤其是当我们一个一个攻克其中的难点,体会其中蕴含的"哲理"时, A 题的自信力也会有所增加,心情也会格外的舒爽.最近重新接触了树状数组 ...

  6. 树状数组详解(超详细)(完整代码在四 五最后)

    一,树状数组的优点 前缀和的思想,可以通过O(n)的预处理,使得多次查询区间值都是o(1),但只能解决不修改,多次查询的问题. 差分思想,能通过差分数组,将区间修改变成O(1)的,最后通过一次O(n) ...

  7. AcWing 241 楼兰图腾

    241. 楼兰图腾 在完成了分配任务之后,西部 314来到了楼兰古城的西部. 相传很久以前这片土地上(比楼兰古城还早)生活着两个部落,一个部落崇拜尖刀(V),一个部落崇拜铁锹(∧),他们分别用 V 和 ...

  8. 0x42.数据结构进阶 - 树状数组

    目录 一.树状数组与逆序对 A.luogu P1908 逆序对(模板题) B.AcWing 241. 楼兰图腾 树状数组的拓展应用 1.区间加,求单点值 A.AcWing 242. 一个简单的整数问题 ...

  9. AcWing 蓝桥杯AB组辅导课 05、树状数组与线段树

    文章目录 前言 一.树状数组 1.1.树状数组知识点 1.2.树状数组代码模板 模板题:AcWing 1264. 动态求连续区间和 例题 例题1.AcWing 1265. 数星星[中等,信息学奥赛一本 ...

最新文章

  1. VS2012/13本地发布网站详细步骤(可带数据库)
  2. 基于ssh的ktv预定管理系统
  3. 虚拟电脑键盘app_说到弹吉他,这几个APP你一定用得上
  4. maven指定项目的构建、打包和tomcat插件的pom.xml配置
  5. Objecttive-C 创建多线程
  6. shell [] [[]]的区别(转)
  7. 大数据阶段划分及案例单词统计
  8. 调试WebApi的一些方法
  9. python的复数实部和虚部都是整数_Python(一)
  10. oracle取字段第三位字符,oracle截取字符串(截取某个字符前面的字符串)
  11. 路由器工作模式Classless与Classful实验分析
  12. cxgrid的FINDPANEL编程
  13. 超简单实现的C语言关机恶搞小程序
  14. 数学建模学习笔记(三十一)模糊评价法
  15. chrome安装crx文件
  16. 二维数组的初始化(二维数组的赋值)
  17. Sql 学习查询多种条件(记录自己常用一些方法,本人学习用)
  18. opencv学习笔记三十六:AKAZE特征点检测与匹配
  19. 【Docker】docker安装elasticsearch集群,Kibana安装以及开启认证
  20. 为什么maven没有3.7的版本

热门文章

  1. H3C交换机作为DHCP服务器,由从IP分配地址。
  2. ipconfig命令
  3. 福建农大计算机学院院长,福建农林大学计算机与信息学院张金山副书记一行到访国科科技...
  4. 南京邮电大学电工电子(数电)实验报告——动态显示电路 存储器的应用
  5. 100句十分精辟的人生格言
  6. ssd固态硬盘的优缺点
  7. 如何拥有(建)一个自己的网站-服务器建站
  8. linux echo 怎么输出换行符到文件?(echo -e 用于打印带转义字符的输出)
  9. 2021年,SEO新生态,如何做好搜索优化?
  10. JavaScript与Java的姐妹情缘