最近在论文"Private and Continual Release of Statistics"中了解到了一个这样的structure:
Definition 3.2 (p-sum). A p-sum is a partial sum of consecutive items. Let 1 ≤ i ≤j. We use the notation ∑[i,j]:=∑jk=iσ(k) to denote a partial sum involving items i through j.
其实关于这个结构到底是干嘛的我还不是很理解(文章讲了有关它的许多内容,但是我只看得懂最后两段描述)。文中是利用这个结构为一个二进制机制服务,后来有文章似乎用它来添加呈对数增长的噪音。它能为计算连续序列的部分和省下计算内存(但是单单从计算和的这部分,难道是累加的意思吗?如果单纯是累加,那似乎这个机制又没什么作用),对于一个长度为T的序列,只需要int(logT)+1的长度即可计算它们的区间累积和。
记录本博客一是觉得这个结构有点意思,二是太久没写代码,而且也一直打算写的另一篇博客也好几个月了都没动工…所以先来适应一下。本博客主要是按照自己对文中描述的理解来实现这个结构。
p-sum理解
p-sum结构用图表示是一颗二叉区间数,叶子结点代表每个参加计算的item,它们都有自己的值,内部结点代表区间和,即他下面的叶子结点之和。从1开始,每到达一个item(叶节点),到达的时间count+=1,这个count也可以理解为到达的次序,根据它的值来进行结点的更新。 树里的每一个结点都可以称作是一个p-sum。 如下图,count=1,2,3…8。
对于每一个count,将它转化为二进制之后,如果二进制中第j位为1,那么就意味着它包含一个2j个item的p-sum 。以T=7为例,二进制为111,即在j=0,1,2 的位上值都为1,那么它包含20=1,21=2,22=4个item,对应三个p-sum,即下图中的7,[5,6],[1,4]。
因此,根据这颗二叉区间数可以看出,当新到达结点可以参与合并的时候,结点就会不断向上合并直到无法合并。以下图为例,同样颜色的结点可以合并,不同层的结点之间无法合并。
例如:当第一个(count=1)到达时,没有能够合并的点,因此只存下结点1的value。第二个结点到达时,可以进行合并,于是1和2结点值合并成[1,2](相加),一旦结点参与合并,那么它就不再有作用,因此可以释放储存它的空间,即释放储存结点1和2的空间,将[1,2]值覆盖到原本存结点1的位置。这就达到了节约内存的目的。
此时内存中列表只存下了一个结点[1,2]值,当count=3的结点到达时,无法合并,存下它的值。当count=4的结点到达时,可以进行合并,3和4合并成[3,4],同时[3,4]和[1,2]也可以进行合并,合并成[1,4]。此时又只需要存一个结点的值即可,将原来存[1,2],3,4结点空间释放,将[1,4]存入原来储存[1,2]的位置即可。
在这个过程中,我们可以发现一些规律:对于第count时刻达到的item,最多只需要log(count)取整+1个单位空间即可完成上述操作。
比如当count=1时候,需要1个单位空间存下;
当count=2时,需要存下1和2,然后合并成[1,2],在这个过程中,最多只需要2个单位空间;
…;
当count=7的时候,空间中只存有[1,4],[5,6],7,需要三个单位;
当count=8时,多增加储存结点8的空间,共4个单位,然后进行合并,最终只需要消耗一个单位。
换句话说,当一个结点达到时,对节点进行储存或更新时所需要的最大内存量就是它能参与构成二叉区间数的层高。
上面为了方便理解,举了具体例子,接下来介绍它的一般更新方法。
定义当前储存在列表中的结点值为αj,αj代表它是2j个item的和。对于count时刻到达的item,首先将count二进制化,找到这个二进制表示最低位非0的位数j。count由count-1进位得来,对于count-1的二进制表示,它的i<j位都是1,因此才能在进位后导致第j位=1,后面都是0。因此当进位之后,就产生了一个2j个item之和的p-sum——αj,其中αj=∑(i<j)αi+valcount。即由原来的所有αi(i<j)+当前item值合并成一个新的结点αj。 合并之后αi(i<j)失去作用,可以被覆盖,将αj存入原来储存最大i的αi位置即可,此时αj作为列表中最后一位有效位(优化的做法是“失去作用”的αi不用被真的清除,只需要用一个指针记录最后一位有效位即可)。
p-sum列表中αj中j是递减的,在实际的代码中,当找到当前count的最低非0下标j之后,只要往p-sum列表最后一位有效位开始累加j项即可。
代码
import random
from cmath import log
#测试数据
nums=[random.randint(1,500) for i in range(100)]
#psum结点值记录
psum_lst=[]
#psum_lst中最后一个有效结点的指针
p_last=-1
#转二进制,也可以直接使用python自带的bin()函数
def to_bin(num):
bin_Str=[]
while(num!=0):
bin_Str.append(str(num%2))
num//=2
return ''.join(reversed(bin_Str))
#更新p_sum结构
def p_sum(bin_str,val):
global p_last,psum_lst
lens=len(bin_str)
idx=0
now_sum=val
#计算当前count(二进制格式)最低位有效‘1’之后的位数个数
for i in range(lens-1,-1,-1):
if(bin_str[i]=='1'):
idx=lens-1-i
break
#如果这个idx>0说明发生了进位,需要更新节点
if(idx>0):
for i in range(p_last,p_last-idx,-1):
now_sum+=psum_lst[i] #将低位结点都累加,形成新的结点值
p_last=p_last-idx+1 #更新p_last,p_last之后的结点无意义,可以被后面的结点更新
psum_lst[p_last]=now_sum #将新节点填入列表中
#总函数
def cal_psum():
global p_last,psum_lst
#初始化psum_lst长度
total_cnt=len(nums)
psum_lst=[0 for i in range(int(log(total_cnt,2).real)+1)]
for i in range(total_cnt):
cnt_bin=to_bin(i+1)
p_sum(cnt_bin,nums[i])
#测试函数
def test():
print('realsum: ', sum(nums)) #真实的总和
cal_psum()
# 计算有效结点的总和
cal_sum=0
for i in range(p_last+1):
cal_sum+=psum_lst[i]
print('cal_sum: ',cal_sum)
test()
多次测试结果都正确: