Day 152-Q3 — 部分配列の最小値の総和(モノトニックスタック寄与法)

2026-09-13 赤色 Master / Phase 8+ ★★★★★★★★★ 左右非対称境界(厳密/等号込み)で重複値の二重カウントを避ける寄与法

問題

長さ $N$ の整数配列 $A=(A_1,\dots,A_N)$ が与えられる。$1\le l\le r\le N$ を満たすすべての部分配列 $(A_l,\dots,A_r)$ について、その最小値 $\min(A_l,\dots,A_r)$ を計算し、その総和を $10^9+7$ で割った余りを求めよ。

入力形式

N
A_1 A_2 ... A_N

制約

$1 \le N \le 200000$
$1 \le A_i \le 10^9$

入出力例

入力例1

4
3 1 2 4

出力例1

17

入力例2

3
2 2 2

出力例2

12

概念図:重複値[2,2,2]での境界と寄与

A=[2,2,2](0-indexed)非対称境界の寄与 i=0 (2) i=1 (2) i=2 (2) PLE=-1 (左に2未満なし) NLE=n=3 (右に2以下なし・i=2のみ) i=0: left=0-(-1)=1, right=NLE[0]-0=1-0=1 → 寄与 2×1×1=2 i=1: left=1-(-1)=2, right=NLE[1]-1=2-1=1 → 寄与 2×2×1=4 i=2: left=2-(-1)=3, right=NLE[2]-2=3-2=1 → 寄与 2×3×1=6 合計 = 2+4+6 = 12(左右対称にすると誤って20になる)

ヒント(段階的開示)

ヒント1(方向性)

$O(N^2)$通りの部分配列を全部数え上げるのは$N\le200000$では間に合わない。「和 = 各要素 × その要素が最小値になる部分配列の個数」という寄与の形に変形できないか考える。

ヒント2(アプローチ)

要素$A_i$が最小値になるのは「$A_i$より真に小さい要素が区間に含まれない」とき。左右それぞれ「$A_i$より小さい要素が現れるまでの距離」をモノトニックスタックで$O(N)$求め、掛け合わせる。同じ値が複数あるとき二重カウントしないよう、左右で不等号の向き(厳密/等号込み)を変える必要がある。

ヒント3(誘導)
# PLE[i]: i より左で最初に A[j] < A[i] となる j(無ければ -1)
# NLE[i]: i より右で最初に A[k] <= A[i] となる k(無ければ N)
PLE = [-1] * n
stack = []
for i in range(n):
    while stack and A[stack[-1]] >= A[i]:
        stack.pop()
    PLE[i] = stack[-1] if stack else -1
    stack.append(i)

NLE = [n] * n
stack = []
for i in range(n - 1, -1, -1):
    while stack and A[stack[-1]] > A[i]:
        stack.pop()
    NLE[i] = stack[-1] if stack else n
    stack.append(i)

# 各 i の寄与 = A[i] * (i - PLE[i]) * (NLE[i] - i)

模範解答 (Python)

import sys


def main():
    MOD = 10**9 + 7
    data = sys.stdin.read().split()
    n = int(data[0])
    a = list(map(int, data[1:1 + n]))

    ple = [-1] * n
    stack = []
    for i in range(n):
        while stack and a[stack[-1]] >= a[i]:
            stack.pop()
        ple[i] = stack[-1] if stack else -1
        stack.append(i)

    nle = [n] * n
    stack = []
    for i in range(n - 1, -1, -1):
        while stack and a[stack[-1]] > a[i]:
            stack.pop()
        nle[i] = stack[-1] if stack else n
        stack.append(i)

    ans = 0
    for i in range(n):
        left = i - ple[i]
        right = nle[i] - i
        ans = (ans + a[i] * left % MOD * right) % MOD

    print(ans)


main()

Step-by-Step 解説

1重複なしの例で手計算
A=[3,1,2,4]の全部分配列の最小値の和は$3+1+1+1+1+1+1+2+2+4=17$。寄与法でも値1の寄与$1\times2\times3=6$を含め検算一致。
2重複がある例で非対称境界の必要性を実測
A=[2,2,2]で左右とも厳密未満を境界にすると合計20(正解12を超える二重カウント)。本問の非対称境界(左は厳密未満のみ境界、右は同値も境界)を使うと寄与は$2,4,6$の合計12で一致。
3なぜ非対称にすると二重カウントが消えるか
同じ値が複数箇所にあるとき「その値を代表する添字」を最も左側の出現位置に一意に決めたい。右側だけ「同値も含めて止める」ことで、同値の最小値が複数含まれる部分配列でも最も左の添字だけが「自分が最小値」と主張できるようにする。

よくあるミス

ミス原因正しい書き方
左右とも同じ不等号で境界を決めてしまう重複要素での二重カウントに気づかない片方は>=で押し出し、もう片方は>で押し出すと非対称にする
PLE/NLEの初期値を$0$や$n-1$にしてしまう「見つからない場合」の番兵を誤る見つからない場合はPLE=-1、NLE=nとする
掛け算の途中でmodを取るタイミングを誤る言語による整数表現の違いを意識していないPythonでは問題にならないが、C++等移植時は都度% MODを挟む
スタックに値そのものを積んでしまう実装の煩雑化スタックにはインデックスを積み、値は都度A[]で引く
$N=1$等コーナーケースでスタックが空の場合の処理漏れ境界条件の考慮漏れstackが空ならPLE=-1、NLE=nとする分岐を必ず入れる

次のステップ

  • 発展: 「最大値の総和」に変えた場合、境界判定の不等号の向きがどう変わるか導出してみる。
  • 次回予告: 永続AVL木(Persistent AVL Tree)

自己評価

自分の回答

気づき・メモ