树状数组

问题

年终,呵呵保险公司的市场部突然变得非常清闲。为了避免被老板批评,市场部决定自己给自己找事情做。至于具体要做什么事,经过一周的争论也没有得出一个让大家都满意的结果。正在一筹莫展时,老板突然给市场部划出一笔不小的年终活动经费。索性,市场部经理一拍脑袋说,不如给客户们送点福利,既给自己找到了事情做,又会增加客户的好感度,一举两得。迫于经理的权力,市场部的大小员工,只能拍手说好。最后,市场部决定要为客户发放一波礼品,以来提高公司的口碑和声望,并让客户切身的感受到呵呵公司的温暖。后,经过多次调研和讨论,最终礼品确定为以下几种:

  • iPhone X (64GB)深空灰 ¥8388
  • NVIDIA GTX 1060(6GB)¥2199
  • Kingston DDR4 2133 8GB ¥759
  • 苏泊尔不粘锅烹饪炒菜锅 ¥159
  • 3M扫把思高易扫净扫把套装 ¥56
  • 得力DELI 170mm办公剪刀 ¥3.5

不过,由于技术部一直特别繁忙,当市场部请求客户信息数据时,技术人员根本懒得理他们。经过市场部的多次软磨硬泡,技术人员迫于无奈,最终给出形如下表的客户信息,表中包含客户的姓名、地址、电话和注册时间:

name addrees phone register_time
Alice Shanghai 13387253309 2012-01-03
Bob Beijing 13549662818 2015-09-16
Tom Guangzhou 15691891573 2016-03-24

现在,市场部的具体需求是,在不超出活动预算的情况下,为客户发放上述几种礼品。市场部可能会向同一个客户发放多个礼品,但不一定每个客户都能分配到礼品。同时,市场部希望能够随时查看当前分配计划下,所花费的总预算是多少,以及某个注册时间区间内,所有用户分配到的礼品预算和是多少。

比如,对于上表用户的一个分配计划如下:

name addrees phone register_time gift
Alice Shanghai 13387253309 2012-01-03 -
Bob Beijing 13549662818 2015-09-16 Kingston DDR4 2133 8GB ¥759
Tom Guangzhou 15691891573 2016-03-24 3M扫把思高易扫净扫把套装 ¥56

那么总预算为¥759 + ¥56 = ¥815,而 [2012-01-03, 2015-09-16] 时间段的用户预算为¥759。

由于分配计划会一直发生变动,所以上述的统计均需满足实时性。

另外,市场部在讨论活动计划时浪费了太多的时间,所以他们希望预算统计的算法要尽量快。

解决方案

观察上表,在不考虑礼品分配策略及发放的情况下,我们可以忽略name、address和phone三列,与需求相关的仅仅是register_time和gift,前者影响统计条件,后者影响预算结果。而对于这两列,我们可以使用离散映射的方式,对其做简单处理,以便计算。转化后,信息表如下:

register_time gift
1 0
2 759
3 56

根据呵呵公司市场部的需求,总预算依旧是将所有礼品的金额加和,而时间区间内的预算统计则是区间礼品金额加和。因此,我们可以简化需求模型:

  • 实时计算gift总额
  • 实时计算某个连续区间内gift总额

显然,第二个需求完全包含了第一个需求,因为第一个需求可以看做在总区间内求gift总额。为此,该问题转化为一个求区间和的模型。但与求区间和问题不同的是,单条记录的gift金额会发生不断的变化。如果仅仅维护前缀和,那么每次修改的代价都将是O(N),总时间复杂度将是O(NM),不能够使人满意。

而后,我们决定采用树状数组来解决这个问题。考虑由8个客户信息组成的表,构建树状数组结构如下图:

image.png

其中A数组代表了按照register_time排序后,每个客户的礼物金额。C数组的计算方式如下:

  • C[1] = A[1];
  • C[2] = A[1] + A[2];
  • C[3] = A[3];
  • C[4] = A[1] + A[2] + A[3] + A[4];
  • C[5] = A[5];
  • C[6] = A[5] + A[6];
  • C[7] = A[7];
  • C[8] = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] + A[7] + A[8];
  • C[i] = A[i - 2k + 1] + A[i - 2k + 2] + … + A[i]; (其中k为i从低位到高位连续0的个数,比如i = (8)10 = (1000)2 时,k = 3)

用C语言实现2k的计算,即lowbit

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

对于第一个需求,即查询连续区间内的数值和,比如求A[3 ~ 7],那么A[3 ~ 7] = SUM[7] - SUM[2]

  • SUM[7] = C[7] + C[6] + C[4]
  • SUM[2] = C[2]

可以证明,SUM[i]的计算复杂度为O(log2N)

求和的C语言实现如下,即sum

int sum(int i)
{
    int result = 0;
    for (;i > 0;i -= lowbit(i))
    {
        result += C[i];
    }
    return result;
}

至此,我们完成了求区间和这一需求。另一个问题是,如何在更新时减小维护的时间复杂度。假设需要对第i条记录增加一个金额为j的礼品,那么C语言的实现如下,即add

int add(int i, int j)
{
    for (;i < N;i += lowbit(i))
    {
        C[i] += j;
    }
}

同样,维护的时间复杂度为O(log2N),总的时间复杂度为O(Mlog2N)

至此,呵呵保险公司的所有需求被解决。

你可能感兴趣的:(树状数组)