如何实现前缀加1操作下带权累积平方和的O(logn)单次查询解法?
嘿,这个问题其实是区间更新+动态查询总和的经典场景,核心是先把问题转化成数学表达式,再找合适的数据结构来高效维护状态。我来一步步给你拆解解决方案:
问题转化与数学分析
首先你得注意到:数组A里的每个元素A[i],本质上就是有多少次操作的k是大于等于i的——毕竟每次操作k都会给0到k的元素加1,所以A[i]就是被这类操作覆盖的次数。我们把这个次数记为cnt[i],那题目要求返回的结果就可以写成:
total = Σ (cnt[i]² * W[i]) ,其中i从0到n-1
现在每次执行操作k,就是给cnt[0]到cnt[k]的每个值都加1。我们需要快速算出操作后total的新值。先推导一下这次操作会让total增加多少:
对于每个i ≤ k,(cnt[i]+1)²*W[i] - cnt[i]²*W[i] = (2*cnt[i] + 1)*W[i]。把这些增量加起来,总变化量就是:
Δtotal = Σ (2*cnt[i] + 1)*W[i] (i从0到k) = 2*Σ(cnt[i]*W[i]) + ΣW[i] (i从0到k)
这就意味着,只要能快速拿到两个值:
preW[k]:W[0]到W[k]的总和(这是固定值,提前预处理好就行)preCW[k]:cnt[0]*W[0]到cnt[k]*W[k]的总和(这个是动态变化的,得高效维护)
就能算出每次操作的增量,进而更新total。但问题是,操作后所有包含0..k的前缀和都会增加preW[k],如果直接维护前缀和数组,每次操作都是O(n)复杂度,显然不符合要求。这时候线段树就是最优的选择了。
线段树解决方案
我们可以用线段树来维护每个区间的三个核心状态,同时用懒标记来处理区间更新的延迟传递,保证每次操作都是O(logn)复杂度。
线段树节点要存啥?
每个节点代表一个区间[l, r],需要维护:
sumW:这个区间里所有W[i]的总和(固定值,建树时就确定了)sumCW:这个区间里cnt[i]*W[i]的总和(动态变化)sumC2W:这个区间里cnt[i]²*W[i]的总和(就是我们最终要的总和的一部分)lazy:懒标记,记录这个区间里所有cnt[i]需要加的次数(还没传递给子节点)
区间更新怎么处理?
当我们给一个区间的cnt[i]加1时,三个动态值的更新规则很清晰:
sumC2W= sumC2W + 2*sumCW + sumW
(因为每个cnt[i]变成cnt[i]+1,平方项展开后,每个元素的增量是2*cnt[i]+1,乘以W[i]求和就是2*sumCW + sumW)sumCW= sumCW + sumW
(每个cnt[i]*W[i]都增加了W[i],所以总和增加整个区间的W之和)- 如果当前节点不是叶子节点,就把懒标记加1,留到需要访问子节点时再传递。
懒标记怎么下推?
当我们需要访问当前节点的子节点时,如果懒标记不为0,就得把这个标记传递下去:
- 分别给左、右子节点执行上述的区间加1操作(用当前的懒标记值,不是只加1)
- 然后把当前节点的懒标记重置为0
查询结果
我们要的total就是线段树根节点的sumC2W,每次操作后直接读这个值就行,O(1)搞定。
复杂度分析
- 建树时间:O(n)
- 单次操作(区间更新+读取结果):O(logn),完全满足题目要求的复杂度。
伪代码实现参考
class SegmentTreeNode: def __init__(self, l, r): self.l = l self.r = r self.left = None self.right = None self.sumW = 0 self.sumCW = 0 self.sumC2W = 0 self.lazy = 0 def build_segment_tree(l, r, W): node = SegmentTreeNode(l, r) if l == r: node.sumW = W[l] return node mid = (l + r) // 2 node.left = build_segment_tree(l, mid, W) node.right = build_segment_tree(mid+1, r, W) node.sumW = node.left.sumW + node.right.sumW return node def push_lazy(node): if node.lazy == 0 or not node.left: return # 更新左子节点 left = node.left left.sumC2W += 2 * left.sumCW + left.sumW * node.lazy left.sumCW += left.sumW * node.lazy left.lazy += node.lazy # 更新右子节点 right = node.right right.sumC2W += 2 * right.sumCW + right.sumW * node.lazy right.sumCW += right.sumW * node.lazy right.lazy += node.lazy # 重置当前节点的懒标记 node.lazy = 0 def update_range(node, target_l, target_r): if node.r < target_l or node.l > target_r: return if target_l <= node.l and node.r <= target_r: node.sumC2W += 2 * node.sumCW + node.sumW node.sumCW += node.sumW node.lazy += 1 return push_lazy(node) update_range(node.left, target_l, target_r) update_range(node.right, target_l, target_r) # 合并子节点的状态 node.sumCW = node.left.sumCW + node.right.sumCW node.sumC2W = node.left.sumC2W + node.right.sumC2W def get_total_value(node): return node.sumC2W
使用示例
- 用你的
W数组构建线段树:root = build_segment_tree(0, n-1, W) - 每次操作
k:先调用update_range(root, 0, k),然后调用get_total_value(root)就能得到结果。
进阶替代方案:前缀和+差分优化
如果你不想用线段树,也可以通过数学变形,用前缀和和差分数组来维护状态,但这个方法的推导和维护逻辑相对复杂,不如线段树直观。简单说就是:
- 用差分数组
diff维护cnt的变化:每次操作k,diff[0] +=1,如果k+1 <n就diff[k+1] -=1 - 预处理
W的后缀和suffixW[j] = Σ_{i=j}^{n-1} W[i] - 维护两个变量来计算最终的
total,具体推导这里就不展开了,感兴趣可以自己试试。
内容的提问来源于stack exchange,提问作者piedpiper

