美团2面技术之被问二维树状数组和前缀和

Posted Java架构没有996

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了美团2面技术之被问二维树状数组和前缀和相关的知识,希望对你有一定的参考价值。

前些日子在做一个 “时间线上的区间统计” 的需求时,突然对树状数组有了新的认识。

来自实际工作中的需求

每一天有一个目标值 value ,给定日期 t1 和 t2,求 t1 到 t2 的 value 总和
这种需求在数据统计的场景上很常见,例如,统计最近 30 天的访问量。如果指标数量比较少,时间区间比较固定,我们可以通过预先计算把结果缓存起来达到快速查询的目的。如果时间区间不定,我们就需要通过遍历求和来解决问题。当访问量很大,这种遍历的做法就不可取了。

静态前缀和

这种问题大家在 leetcode 应该做过很多次了,假如不需要任何的修改,我们可以使用前缀和来解决区间查询的问题,当然前提是运算函数必须满足结合律(比如算平均数,就不满足结合律)。于是区间求值的问题就变成了:

\\sum_{i=m}^{n}v_{i}=\\sum_{i=0}^{n}v_{i}-\\sum_{i=0}^{m-1}v_{i}∑ i=m n ​ v i ​ =∑ i=0 n ​ v i ​ −∑ i=0 m−1 ​ v i ​

只需要预处理一遍,每一次区间查询都只需要查询两个点的值即可,复杂度极低

动态前缀和

当我们需要更新某一个点时,整个前缀和都得更新一次,(除非是在最后面插入一个点),这时候更新复杂度是 o(n)o(n)

阶段前缀和

我们还是回归到有意义的下标——时间。注意到,我们的前缀和是可以按阶段来的。我们定义每一天的前缀和,等于从当月 1 号到这一天的前缀和。而当月最后一天的前缀和,则等于当年 1 月 1 号到这一天的前缀和。而当年最后一天的前缀和,则等于从起始年到这一天的前缀和。整个前缀和就分成了 3 个阶段。

这时候,从起始时间到当前时间的前缀和,等于从起始时间到当前年的前缀和,加上当年 1 月 1 号到这一天的前缀和,再加上当月 1 号到这一天的前缀和。

我们查询区间不再是 2 个点,而是 6 个点。

更新操作,只需要更新年累计值,当年月累计值,当月日累计值即可,更新操作的数量大大降低。

阶段数量为固定数的前缀和

上面的例子用了有意义的时间下标来描述优化更新的办法。通常我们在做题的时候,下标是没有意义的,这时候我们就需要定义一个规则。

设置连续 k 个下标为一个子前缀和,那么我们就得到 n/kn/k 个子前缀和,他们的和形成一个 n/kn/k 的数组,这个数组更新又可以按照这个规则继续分阶段,于是我们又得到 n/k^2n/k
2
个子前缀和,以此类推。每个子前缀和的最后一个节点的值是这个他所在的最大子前缀和的和。

举个例子:

当 k = 3 时,数组 [1,2,3,4,5,6,7,8,9] 的前缀和为:


1
1+2               =3
1+2+3             =6
4
4+5               =9
4+5+6             =15
7
7+8               =15
1+2+3+4+5+6+7+8+9 =45
10//加入Java开发交流君样:756584822一起吹水聊天

读取:最多读取log_{k}{n}log k

n 个值就可以得到任意一个前缀和
更新:每一轮需要更新log_{k}{n}log k

n 个前缀和,每个前缀和最多需要更新 (k-1)(k−1) 个值,复杂度是 o(log_{k}{n} * (k - 1))o(log k ​ n∗(k−1))

树状数组
特别的,当 k=2k=2 时,就是我们所说的树状数组

假设读取和更新都是均衡随机的,读取和更新的复杂度总和是o(log_{k}{n}+log_{k}{n} * (k - 1))o(log k ​ n+log k ​ n∗(k−1))

当 k=2k=2 时,这个式子计算值最低

延伸

事实上应用的时候,很多时候是不可能绝对平衡的。

假设更新次数远小于读取次数,那么我们可以适当提高 k 值,以达到加速读取前缀和的目的。特别的,当 k=nk=n 时,每一轮更新将达到 o(n)o(n) 的更新。

假设读取次数远小于更新次数,那么我们可以适当降低 k 值,已达到加速更新前缀和的目的。特别的,当 k=1k=1 时,就相当于保存整个原始数组,每次读取需要在线重新统计前缀和。

树状数组和线段树

大家在总结比较树状数组和线段树的时候,通常会提起:树状数组是点更新,区间查询;线段树是区间更新,区间查询。

这两者其实有本质的区别:

树状数组由于空间仍然为 o(n)o(n),读取和更新次数的协调,并没有扩充空间换取更多应用空间。本质上还是前缀和

线段树则是将区间的个数扩充,每个点都被 o(log{n})o(logn) 个区间包含,有了这个优化,线段树就可以进行任意线段的操作了。

然而线段树结构太过松散,区间从大到小的缓存机制也意味着需要不断的读取和查询的交叉执行,不利于持久化,在工业界其实是很少见到的,除非整个树加载到内存操作,又有线程安全问题。

而树状数组这种结构和操作,根据下标就能确定需要读取和更新哪些分段前缀和,一次批量读取更新就可以解决读取更新的问题。对 io 敏感型的应用会有很好的场景。

最新2021整理收集的一些高频面试题(都整理成文档),有很多干货,包含mysql,netty,spring,线程,spring cloud、jvm、源码、算法等详细讲解,也有详细的学习规划图,面试题整理等,需要获取这些内容的朋友请加Q君样:756584822


以上是关于美团2面技术之被问二维树状数组和前缀和的主要内容,如果未能解决你的问题,请参考以下文章

$[SHOI2007]$ 园丁的烦恼 二维数点/树状数组

树状数组 / 二维树状数组

POJ 1195 Mobile phones(二维树状数组)

二维树状数组基本操作

单点修改区间查询(树状数组)

POJ-2029 Get Many Persimmon Trees---二维树状数组+枚举