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