2026-01-26 18:05:00
发布于:广东
🔍 第一步:深入题目解析 —— 把“音乐”翻译成“算法语言”
我们先不急着想怎么做,而是精准建模:
有 n 个数 A₁, A₂, ..., Aₙ
一个「超级和弦」= 一个连续子数组,其长度 len 满足:L ≤ len ≤ R
它的美妙度 = 这个子数组的区间和
所有满足长度约束的子数组,构成一个候选集合 S(注意:不同位置即使和相同,也是不同和弦)
从 S 中选出 k 个互不相同的子数组(即 k 个不同的区间),使得它们的区间和之和最大
→ 等价于:在所有合法区间中,选出前 k 大的区间和,求和
✅ 所以核心子问题是:
如何高效地、按降序顺序,逐个取出所有满足长度 ∈ [L,R] 的连续子数组的前 k 大和?
⚠️ 注意陷阱:
不能把所有 O(n(R−L+1)) ≈ O(n²) 个区间全生成(n=5×10⁵ 时,最坏超 2.5×10¹¹ 个,内存/时间双爆炸)
不能只对每个起点暴力扫长度(仍是 O(n(R−L)),不可接受)
也不能用普通堆暴力枚举——因为“第 k 大”需要可控的扩展方式,而非盲目搜索
🧩 第二步:关键洞察 —— 固定左端点,右端点可选范围是连续区间!
设子数组为 [i, j],则长度为 j−i+1 ∈ [L,R] ⇒
对固定左端点 i,j 的取值范围是:
[
j \in [,i+L-1,; \min(i+R-1,, n),]
]
记该区间为 [lᵢ, rᵢ]。
而 [i, j] 的和 = prefix[j] − prefix[i−1],其中 prefix 是前缀和数组(prefix[0]=0, prefix[i]=A₁+⋯+Aᵢ)
→ 对固定 i,[i,j] 的和 = (prefix[j]) − Cᵢ(Cᵢ = prefix[i−1] 是常数)
→ 所以:在 j ∈ [lᵢ, rᵢ] 上,使和最大的 j ⇔ 使 prefix[j] 最大的 j
💡 这就引出了一个极其重要的观察:
对每个左端点 i,它能贡献的最大可能和弦值,就是
max{ prefix[j] | j ∈ [lᵢ, rᵢ] } − prefix[i−1]
而这个最大值,可以通过区间最值查询(RMQ) 快速得到!
但注意:我们不是只要最大值,而是要前 k 大的所有值——包括每个 i 对应的第2大、第3大……直到取满 k 个。
这就启发我们使用一种经典策略:
✅ 「堆 + 可持久化/延迟扩展」思想:每次弹出当前全局最大值,再将该来源的「次优候选」推入堆中
类似「合并 k 个有序链表找前 k 大」、「多路归并」、「滑动窗口最大值的拓展」——这是信奥中反复出现的分治式贪心 + 堆驱动扩展范式。
🛠️ 第三步:系统解题框架 —— 分四步走(请你在纸上同步画图/列步骤)
✅ 步骤 1:预处理前缀和 + 构建 RMQ 结构
计算 prefix[0..n](O(n))
为 prefix[0..n] 构建 ST 表(Sparse Table) 或 线段树,支持 O(1) 或 O(log n) 查询任意区间 [l, r] 的最大值及其位置(⚠️ 位置很重要!后面要分裂区间)
→ 推荐 ST 表(静态,O(n log n) 预处理,O(1) 查询),适合本题(无修改)
✅ 步骤 2:初始化大根堆(优先队列)
堆中每个元素是一个元组:(value, i, l, r, pos)
value:当前区间 [l,r] 内 prefix[j] 的最大值减去 prefix[i−1](即该 i 对应的最优和弦值)
i:左端点
l, r:当前 j 的可选范围(初始为 [i+L−1, min(i+R−1,n)])
pos:取得最大 prefix[j] 的下标 j₀(由 RMQ 返回)
对每个合法左端点 i(从 1 到 n−L+1),计算其对应初始最优值,入堆
→ 共最多 O(n) 个初始元素
✅ 步骤 3:执行 k 轮「取最大 + 分裂扩展」
弹出堆顶 → 得到当前最大和弦值,累加进答案
设弹出元素对应 (val, i, l, r, j₀)
此时,为了后续能拿到该 i 的第二优选择,我们需要把 j 的候选区间 排除掉 j₀,拆成两段(如果存在):
左半段:[l, j₀−1](若 l ≤ j₀−1)
右半段:[j₀+1, r](若 j₀+1 ≤ r)
对每一段,用 RMQ 查询其内部 prefix[j] 的最大值及位置,构造新元组入堆
🔍 为什么这样能保证不重不漏、且按序产出?
→ 因为每个 (i, j) 对应唯一和弦;我们对每个 i 的所有 j 按 prefix[j] 降序“组织”成一棵隐式的最大堆二叉树(根是最大,左右子树分别是左右子区间的最大);整个过程等价于遍历 k 个最大节点 —— 这正是「堆 + 区间分裂」的经典正确性保障(类似「第 k 小的数在矩阵中」的解法)
✅ 步骤 4:输出累加和
📚 第四步:信奥知识锚点 —— 这些你必须真正掌握!
知识点 为什么关键? 你是否熟练?(自测)
前缀和 + 区间和转化 把「连续子数组和」转为「两个前缀和之差」,是处理子数组问题的第一步直觉 □ 是 □ 否
RMQ(ST 表) 本题需频繁查区间最大值及其位置;ST 表 O(1) 查询 + 支持返回下标(实现时注意存 max_val 和 max_idx 两个数组) □ 是 □ 否
优先队列(heapq)自定义比较 Python 的 heapq 默认小根堆,需存负值或用 (-val, ...);元组比较按字典序,注意字段顺序设计 □ 是 □ 否
「分裂区间」的贪心正确性 这是本题灵魂!类比:为何不用 set 去重?为何不暴力枚举?理解「每个区间被至多访问一次」的时间复杂度证明 □ 是 □ 否
复杂度分析能力 总时间 = O(n log n)(ST 表) + O(n log n)(建堆) + O(k log(n + k))(k 次 pop/push,每次 push 最多 2 个,堆大小 ≤ n + 2k)→ 符合 n,k ≤ 5×10⁵ □ 是 □ 否
🧪 第五步:用样例亲手推演(强烈建议你暂停阅读,自己动手!)
输入:n=4, k=3, L=2, R=3,A = [3,2,-6,8]
求 prefix = [0,3,5,-1,7]
合法左端点 i:1,2,3(因为 i+L−1 ≤ n ⇒ i ≤ n−L+1 = 3)
i=1 ⇒ j ∈ [2,3] → query prefix[2..3] = max{5,−1}=5 at j=2 → sum = 5 − prefix[0] = 5
i=2 ⇒ j ∈ [3,4] → max{−1,7}=7 at j=4 → sum = 7 − prefix[1] = 7−3 = 4
i=3 ⇒ j ∈ [4,4] → max{7}=7 at j=4 → sum = 7 − prefix[2] = 7−5 = 2
→ 初始堆:[(5,1,2,3,2), (4,2,3,4,4), (2,3,4,4,4)]
取最大:5 → 对应 [1,2](音符1+2 = 3+2=5)
→ 分裂 j∈[2,3] 排除 j=2 ⇒ 剩 [3,3]
→ query prefix[3]=−1 → new val = −1−0 = −1 → push (−1,1,3,3,3)
取次大:4 → 对应 [2,4](2+(−6)+8=4)
→ j∈[3,4] 排除 j=4 ⇒ 剩 [3,3]
→ prefix[3]=−1 → val = −1−3 = −4 → push (−4,2,3,3,3)
取第三大:2 → 对应 [3,4](−6+8=2)
→ 累加:5+4+2 = 11 ✅
你发现了吗?这三个和弦恰好是:[1,2], [2,4], [3,4] —— 都长度∈[2,3],互不相同,和最大。
💡 最后送你一句信奥心法:
当问题要求“前 k 大”且候选集过大时,永远先问自己:
① 能否将候选集按某种规则分组(如按左端点)?
② 每组内部能否快速获取极值?
③ 极值被取走后,剩余部分能否被高效继承/分裂?
—— 这三个问题的答案,往往就指向了正解的骨架。
你现在可以思考:
如果 R−L 很小(比如 ≤10),有没有更简单的做法?(提示:对每个 i 枚举 j ∈ [i+L−1, i+R−1],用堆维护 top-k,O(n(R−L) log k))
如果 k=1,本题退化为什么?还能用 RMQ 吗?(当然可以,但更简单:枚举所有合法区间 O(n(R−L)))
Python 实现时,heapq 里存元组要注意什么?如何确保最大值先出?(想想负号 or key 参数不可
这里空空如也















有帮助,赞一个