树状数组的应用

126 阅读5分钟

写在前面

本文旨在介绍树状数组的应用,关于其原理与复杂度证明的博客不在少数,想要深入了解的读者自行搜索,这里不再赘述。

基本介绍

树状数组通常用于维护区间,如区间查询和单点修改、单点查询和区间修改,查询和修改的时间复杂度均为(logn)。
废话不多说,下面用一道经典的例题来介绍用法

求逆序对

题目描述: pair.png

数据范围:序列长度<=500000 序列中的每个数不超过1000_000_000
从本题要求和数据范围来看,需要先将序列中的数字离散化,离散化后每个位置的数相对大小不变
下面给出我认为比较无脑的离散化方法:

(C++)
a[n]//输入序列
map<int,int>cnt;
for(int i=0;i<n;i++)
{
    cnt[a[i]]=1;
}
int cur=1;
for(auto &[_,num])num=cur++;
for(int i=0;i<n;i++)a[i]=cnt[a[i]];

离散化本质是在不改变相对大小的前提下将序列中的数映射到1-n范围(n为序列中不同数的个数)
做完离散化之后,我们就可以用树状数组中下标为i的元素存储大小为i的数字个数,这样我们每次用logn的代价求出小于i的数字个数,同理修改也是logn,然后顺序遍历序列,求出小于当前数的数字个数,用已遍历的个数即可得到大于当前数的数字个数,再将当前数加入树状数组
时间复杂度:O(nlogm) n为序列大小 m为离散化后序列中的最大数即序列中不同数的个数
题目链接洛谷->逆序对

下面的题目才是正文,让我理解了树状数组的局限性和如何通过转化条件来消除局限性
题目描述:
sum.png 这是某编程网站的一道题,由于设定原因和页面布局,我在做的时候看到了题目提示用树状数组和前缀和(把数据范围和样例与提示挨在一起,想看不到都难),但还是没做出来,下面说一下我看了题解后梳理的思路
大概思路:题目要求任意区间中出现次数为偶数的数的异或和,看到区间和容易想到前缀和,利用前缀和可以快速求出区间的异或和,题目限定出现次数为偶数的数的异或和,进一步想,单纯利用前缀和求出的区间异或和等价于出现次数为奇数的异或和(相同的数异或偶数次结果为0,0不改变异或结果),正好题目的所求相反。
再进一步想,如果能求出区间中不同的数的异或和,再与由前缀和求出的异或和做一次异或,这样出现次数为奇数的数变为偶次,出现次数为偶数的数变为奇次即得到题目所求。那么现在的问题就是如何在能接受的时间复杂度内求出区间的不同的数的异或和。

如何用树状数组求区间中不同的数的异或和

我在做的时候也差不多卡在这一步上,不知道如何用树状数组求任意区间的特定异或和。如果是在线查询的话,应该是做不到的,要转换成离线查询。
将题目中的m组查询按照右端点从小到大排序,用树状数组维护前缀异或和,查询的方式不变,但是修改的时候要变化,额外用一个map维护每个数的上一次出现的下标,在将某个数加入树状数组时,如果这个数不是第一次出现,在当前下标异或一次的基础上,额外将这个数上一次出现下标异或一次,并记录这个数最后一次出现的下标。\

下面来讨论一下为什么这样做以及这样做为什么对

首先,如果不将右端点排序,应该是无从下手的(至少在使用树状数组的条件下应该是这样)。按右端点递增之后,只需要考虑左端点变化情况下求区间不同的数的异或和,就是你将i位置数的影响加入树状数组后,你后续求区间异或和的区间(a,b)都满足b>=i
其次,解析上述方法为什么能维护区间不同数的异或和,遍历到i位置的数num的时候,如果num之前没有出现过,只在树状数组当前下标i加入num的影响,这样求得区间不同数的异或和都包含num。再看如果num之前出现过且出现位置为j,如果同没有出现的情况一样操作,那么(j+1,i)区间异或一次num,但是(1,j)这部分之前已经异或了num,这样获取区间异或和时得到的是异或了偶数次num的结果,所以将(1,j)异或一次num,得到任意区间(k,j)都异或了一次num (1<=k<=j)
最后,上述方法得到区间异或和只能在右端点递增的情况下保证正确性。
由于本人代码不易读懂,故不给出题解代码,只给出题目链接。 码蹄集->偶数个数的异或和

相似的题:
码蹄集->配对最小数
码蹄集->区间数据处理

免责声明:以上是我自己的理解,必然存在许多错误和逻辑漏洞,写这篇博客只是为了记录一下。

写在最后

秋招的失利让我心灰意冷,只有在写算法题的时候才能得到内心的平静。
唉。