1390 字
4 分钟
贡献法学习笔记
2026-06-20

贡献法(Contribution Technique)学习笔记#

前置经历#

在解决「史莱姆繁殖问题」的加强版时,卡了很久。简单版(O(n) 反向栈)能秒,但困难版要求所有连续子段的源数之和,第一反应是枚举子段,然后发现 O(n²) 必然炸。

和 CherryClaw 对话后,学到了贡献法这一核心思维模式。


贡献法的本质#

换一个问题。

  • 枚举视角:枚举每个组合(子段、路径、子集…),算它对答案的贡献 → 组合太多
  • 贡献视角:枚举每个基础单元(元素、边、匹配关系…),算它出现在多少组合里 → 每个单元 O(1) 或 O(log n)

数学本质——交换求和顺序:

所有组合组合里的单元1=每个单元包含它的组合1\sum_{\text{所有组合}} \sum_{\text{组合里的单元}} 1 \quad = \quad \sum_{\text{每个单元}} \sum_{\text{包含它的组合}} 1

入门例题#

例 1:所有子段和之和(最简单)#

题目:给定数组 a[1..n],求 l=1nr=lnsum(a[l..r])\sum_{l=1}^n \sum_{r=l}^n \text{sum}(a[l..r])

枚举视角:O(n³) 或前缀和压到 O(n²)

贡献视角:每个元素 a[i] 在多少子段中出现?

a[i] 的贡献=a[i]×i左端点选择数×(ni+1)右端点选择数a[i] \text{ 的贡献} = a[i] \times \underbrace{i}_{\text{左端点选择数}} \times \underbrace{(n-i+1)}_{\text{右端点选择数}}
ans = 0
for i in range(1, n+1):
ans += a[i] * i * (n - i + 1)

O(n),一行。


例 2:所有子段最大值之和(单调栈 + 贡献法)#

题目:求所有子段的最大值之和。

贡献视角a[i] 在多少子段里是最大值

单调栈预处理:找到左边第一个大于 a[i] 的位置 L,右边第一个大于 a[i] 的位置 R

(L, R) 区间内,a[i] 是唯一最大值。子段要满足 a[i] 是最大值:

子段数=(iL)×(Ri)\text{子段数} = (i - L) \times (R - i)贡献=a[i]×(iL)×(Ri)\text{贡献} = a[i] \times (i - L) \times (R - i)
# 单调栈预处理 L[i] 和 R[i]
for i in range(1, n+1):
ans += a[i] * (i - L[i]) * (R[i] - i)

例 3:史莱姆困难版(载体是匹配关系)#

题目:所有连续子段源数之和。

简单版回顾:从右往左栈,v → v+1 匹配,栈大小 = f(l,r)

困难版核心观察:全局匹配边 (i → j) 在任何包含 i 和 j 的子段中都保持有效。

  • 子段包含 [i, j] 时:匹配边生效,减少一次源数(少一个初始史莱姆)
  • 子段不包含 i 或 j 时:匹配边失效

贡献法:载体不是元素,是匹配边本身

每条全局匹配边 (i → j) 被多少子段包含?

贡献=(i+1)左端点选择数×(nj)右端点选择数\text{贡献} = (i + 1)_{\text{左端点选择数}} \times (n - j)_{\text{右端点选择数}}

最终公式

答案=n(n+1)(n+2)6所有子段长度之和(ij)(i+1)×(nj)这条边被多少子段包含\text{答案} = \underbrace{\frac{n(n+1)(n+2)}{6}}_{\text{所有子段长度之和}} - \sum_{(i \to j)} \underbrace{(i+1) \times (n-j)}_{\text{这条边被多少子段包含}}
total = n * (n + 1) * (n + 2) // 6
stack = []
for i in range(n-1, -1, -1):
while stack and a[stack[-1]] == a[i] + 1:
j = stack.pop()
total -= (i + 1) * (n - j)
stack.append(i)

贡献法三步法#

步骤说明本例对应
1. 选载体选什么作为基本单元?匹配边 (i → j)
2. 算出现次数载体在多少组合里生效?i × (n-j+1)
3. 乘贡献值载体每次生效贡献多少?-1(减少一次源数)

注意:贡献值可以是负的——本题从总量中「减去」被认领的。


常见载体类型#

题目载体生效条件
子段和之和元素 a[i]l ≤ i ≤ r
子段最大/最小值之和元素 a[i](用单调栈找到它为最值的区间)a[i] 是 [l,r] 的最值
图上路径边权和边 (u,v)路径经过该边
所有点对距离之和边 (u,v)多少点对的最短路径经过它
子段内逆序对之和逆序对 (i,j)l ≤ i 且 j ≤ r
史莱姆困难版匹配关系 (i→j)l ≤ i 且 j ≤ r

为什么贡献法是分水岭#

水平面对「所有子段」时
入门枚举 O(n²),可能超时但能写
铜牌前缀和、差分、滑动窗口——仍在优化枚举
银牌先问「能不能不枚举子段?」——切换视角
金牌灵活选择非常规载体(匹配关系、图论结构…)

个人反思#

卡住的真正原因#

不是智商问题,是思维习惯

  1. 遇到「所有子段」,第一反应永远是枚举子段——这是本能的、正常的
  2. 发现枚举会炸后,试图用分治/线段树/双指针「优化枚举」——仍然是枚举视角
  3. 没有立刻回头找结构不变量(全局匹配边在子段中不变的属性)

改进方向#

  • 遇到「所有 X 之和」,强制先问:能不能选载体?能不能交换求和顺序?
  • 发现枚举炸了,不要继续优化枚举,立刻回头找不变量
  • 刷 5~10 道贡献法专题题,让这个思维成为肌肉记忆

和维什戴尔题的呼应#

维什戴尔火力覆盖题花了 4 小时才 AC,同样是因为「枚举所有部署 + 所有怪物」的思维惯性。那题虽然没有显式用贡献法,但同样需要反向建图——枚举每只怪物能被哪些部署覆盖,而不是枚举每个部署能覆盖多少怪物。

方向反转是 ACM 中高阶题的核心思维模式,贡献法是其中最重要的具体技法之一。


待刷清单#

  • CF 所有子段最大值之和(单调栈 + 贡献法)
  • CF 所有子段最小值之和(对称)
  • 所有子段 gcd / lcm 之和
  • 所有子段中逆序对之和
  • 图上所有路径的某属性之和
  • 更多「所有子段」的贡献法题

相关链接#

  • 史莱姆简单版:反向栈贪心,O(n)
  • 史莱姆困难版:反向栈 + 贡献法 + 组合数学,O(n)
  • 维什戴尔火力覆盖:攻击范围建模 + 枚举优化 + 溅射扩散

笔记生成于与 CherryClaw 的对话,2026-06-20

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

贡献法学习笔记
https://caoyue.xin/posts/contribution-technique/
作者
Colton/曹越
发布于
2026-06-20
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录