1. 从“不会起名”的标题到真正要做的题
先交代一下背景。IXOI R1 这场我赛后才补,群里转得最多的一道题就是 P15444《出题人完全不会给题目起名字》。看到这个题名,有经验的老选手多少会心一笑——这说明出题人自己也知道,这题不靠标题提示方向。麻烦也在这:你扫完题面,依然没有任何线索能告诉你该往数据结构、数学还是 DP 上想。
我在本地把这题重新“翻译”成正常模型之后发现,本质其实非常干净。去掉自嘲的外壳,它要你算的东西是这样的:
给定一个长度为 n 的数组 a,对于所有非空连续子区间 [l, r],求每个子区间的最大值和最小值之差的总和,结果对某个大质数取模。
用式子写就是:
Σ(max(a[l..r]) − min(a[l..r]))
这类题有个特点,你要不去想“取模范围是什么、n 给到多少”,否则很容易被表面吓住。按复刻题面常见的数据范围,n 一般给到 2×10^5 左右甚至更大,a[i] 可以到 10^9 量级,所以期望做法必须是 O(n log n) 以内。
看到“最大值”“最小值”“所有子区间”“总和”同时出现,基本可以锁定一道套路题。第一步先做拆解:
Σ(max − min) = Σ(max) − Σ(min)
这个式子意味着,我不需要真的去枚举每个子区间,只要求出两个东西:
- 所有子区间最大值之和;
- 所有子区间最小值之和。
然后拿前者减后者就是答案。到这里,问题已经从一个“所有区间统计”变成了“每个元素作为最大值的贡献之和”和“每个元素作为最小值的贡献之和”。
这类贡献法在算法题里出现频率极高,几乎每个想冲 OneInDark 之类集训队的选手都该刻在脑子里。接下来的关键就变成了:怎么快速知道一个元素会在多少个区间里当最大值。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 左右边界用一开一闭:单调栈数贡献的原理
2.1 最大值贡献的完整逻辑
假设我盯着 a[i],想知道它作为最大值的区间有多少个。
一个区间如果想让 a[i] 当最大值,条件很直接:区间里不能有任何元素大于 a[i]。更准确地讲,区间左端点不能越过 a[i] 左侧最近的那个“比 a[i] 大的元素”,右端点也不能越过右侧最近的那个“比 a[i] 大的元素”。
设左侧最近的比 a[i] 大的位置为 L[i],右侧最近的“比 a[i] 大或相等”的位置为 R[i]。
为什么右侧不是严格比它大,而是“大或相等”?
因为数组里很可能有相等元素。如果左右两侧都取严格大于,那么当最大值有多个相同值时,同一段区间会被多个位置重复贡献,或者漏掉一部分。比如数组 [5, 5],如果两侧全部用严格大于来限定,a[1] 认为右侧没有严格比 5 大的元素,于是把 [1,1]、[1,2]、[2,1]? 你写代码时怎么划都容易出问题。
正确做法是人为规定一个“归属权”:假设一个区间的最大值有多个相同取值时,只把它算给“最右边”的那个最大值元素。
这样左侧要找“严格大于 a[i]”的位置,右侧则找“大于等于 a[i]”的位置。因为如果右边出现了相等元素,这个区间就不再归当前元素管。反过来你也可以定义成左边带等、右边严格大,本质一样,关键是别两边都严格或两边都带等。
当边界 L[i] 和 R[i] 都确定了,以 i 作为区间最右最大值的区间数量就是:
(i − L[i]) × (R[i] − i)
左端点的可选范围是 L[i]+1 到 i,一共 (i − L[i]) 个;右端点的可选范围是 i 到 R[i]−1,一共 (R[i] − i) 个。两者组合起来,恰好覆盖所有以 i 为“代表最大值”的区间。
2.2 用单调栈找 L[i] 和 R[i]
找“每个位置左边第一个比我大/大的位置”,这是单调栈最经典的场景。
先看最大值情况
