树状数组

作者: 剪刀刀 | 来源:发表于2017-12-10 21:47 被阅读0次

    问题

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

    • 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)

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

    相关文章

      网友评论

        本文标题:树状数组

        本文链接:https://www.haomeiwen.com/subject/bunvixtx.html