形式化题目

给定长度为 nn 的数组 a1,…,ana_1,\dots,a_n。对每个窗口长度 L=1,2,…,nL=1,2,\dots,n,求

SL=∑i=1n−L+1(max⁡j=ii+L−1aj)(min⁡j=ii+L−1aj)S_L=\sum_{i=1}^{n-L+1}\Big(\max_{j=i}^{i+L-1}a_j\Big)\Big(\min_{j=i}^{i+L-1}a_j\Big)

并按 LL 从小到大的顺序输出 S1,S2,…,SnS_1,S_2,\dots,S_n。

样例 [1,5,2,4,3][1,5,2,4,3] 中,长度为 3 的窗口是 [1,5,2],[5,2,4],[2,4,3][1,5,2],[5,2,4],[2,4,3],所以第 3 窗口值 =5×1+5×2+4×2=23=5\times1+5\times2+4\times2=23;完整的答案是 55 35 23 15 5。

正解

思路

朴素做法撑不住。 最直接的写法是枚举每个子数组、各自求 max⁡\max 和 min⁡\min,共 Θ(n2)\Theta(n^2) 个子数组、每个还要扫一遍,Θ(n3)\Theta(n^3);退一步,对每个长度 LL 用双端队列做一次滑窗,每个 LL 是 O(n)O(n),总计仍是 O(n2)O(n^2)。n=105n=10^5 时 n2=1010n^2=10^{10},无论哪种都超时。真正的问题是:答案虽然只有 nn 个,但生成答案的窗口有 Θ(n2)\Theta(n^2) 个,不能逐个窗口访问。

换一个枚举顺序。 固定右端点 ii,让左端点 ll 从 ii 一路左移到 1,考察

fi(l)=max⁡(al..ai)×min⁡(al..ai).f_i(l)=\max(a_l..a_i)\times\min(a_l..a_i).

max⁡(al..ai)\max(a_l..a_i) 只有在 ala_l 比之前遇到的所有值都大时才会变大,这些位置恰好是「从 ii 往左看的记录值位置」——把它们从右往左收集起来,正是单调递减的 max 栈(弹出条件 top <= x,相等值只留更靠右那个,而相等不会改变 max⁡\max,所以不漏变化点);min⁡\min 对应单调递增的 min 栈。两个栈按下标从大到小归并,每次先走下标更大的变化点,就把 fi(l)f_i(l) 切成若干段,段内 (max⁡,min⁡)(\max,\min) 恒定,段恰好对应一段连续的左端点,也就对应一段连续的窗口长度。

数据范围 ai⩽100a_i\leqslant100 的真正用途在这里。 栈内值严格单调,且每个值都取自 {1,…,100}\{1,\dots,100\},所以每个栈至多 100 个元素;每个右端点至多 200200 段,全部右端点合计 O(100n)O(100n) 次走段。这一步把「按左端点逐个处理」换成了「按变化点跳跃处理」,而值域上限保证跳跃次数是常数级的。

段按长度差分入账。 一段左端点区间 (nxt,left](\text{nxt},\text{left}] 对应长度区间 [i−left+1, i−nxt][i-\text{left}+1,\ i-\text{nxt}],若逐长度赋值,段内左端点多时又是 O(n)O(n)。改成差分:d[lo] += prod、d[hi+1] -= prod,每段 O(1)O(1);全部右端点处理完后对 d[1..n] 做一次前缀和,得到 S1..SnS_1..S_n。落到代码里就是 d[i-left+1] += prod、d[i-nxt+1] -= prod;最后剩下的左端点区间是 [0,left][0,\text{left}],长度上界为 i+1i+1,所以收尾段的减项写在 d[i+2],差分数组开到 n+3 才够。

下面用样例 [1,5,2,4,3][1,5,2,4,3] 走一遍:每个右端点固定后,把归并出的段(本例中每段只含一个左端点)列出来,最后一列是该乘积计入哪个窗口值。

右端点 ii 左端点 ll 窗口长度 L=i−l+1L=i-l+1 max⁡\max min⁡\min max⁡×min⁡\max\times\min 计入
1 1 1 1 1 1 S1S_1
2 2 1 5 5 25 S1S_1
2 1 2 5 1 5 S2S_2
3 3 1 2 2 4 S1S_1
3 2 2 5 2 10 S2S_2
3 1 3 5 1 5 S3S_3
4 4 1 4 4 16 S1S_1
4 3 2 4 2 8 S2S_2
4 2 3 5 2 10 S3S_3
4 1 4 5 1 5 S4S_4
5 5 1 3 3 9 S1S_1
5 4 2 4 3 12 S2S_2
5 3 3 4 2 8 S3S_3
5 2 4 5 2 10 S4S_4
5 1 5 5 1 5 S5S_5

按列求和正好是 55 35 23 15 5。注意 i=5i=5 时归并顺序是「先 max 栈的 l=4l=4,再 min 栈的 l=3l=3,再 max 栈的 l=2l=2,再 min 栈的 l=1l=1」——两栈按下标大小交错推进,指针各走一次,总共 ∣max栈∣+∣min栈∣|max栈|+|min栈| 步。

同一下标同时刷新最大值和最小值

当某个 ala_l 同时是新的最大值和新的最小值时,两个栈都会在这个下标停一次:第一次产生非空区间正常入账,第二次产生的是空区间(d[lo] += prod 与 d[hi+1] -= prod 落在同一格),自动抵消,随后两处极值都已更新。所以代码不需要对这种情况写特判,但必须保留这次「空入账」,否则会重复计一段。

一般情况下一段会覆盖多个长度。例如数组 [1,3,3,2][1,3,3,2] 固定右端点 i=4i=4(值 2)时,max 栈是 (3,3),(2,4)(3,3),(2,4),min 栈是 (1,1),(2,4)(1,1),(2,4),归并出三段:

段(左端点) 窗口长度区间 [i−left+1, i−nxt][i-\text{left}+1,\ i-\text{nxt}] max⁡\max min⁡\min 乘积 差分操作
l=4l=4 [1,1][1,1] 2 2 4 d[1] += 4、d[2] -= 4
l∈(1,3]l\in(1,3],即 l=2,3l=2,3 [2,3][2,3] 3 2 6 d[2] += 6、d[4] -= 6
l=1l=1(收尾段) [4,4][4,4] 3 1 3 d[4] += 3、d[5] -= 3

第二段说明了差分的价值:窗口 [a3..a4]=[3,2][a_3..a_4]=[3,2] 与 [a2..a4]=[3,3,2][a_2..a_4]=[3,3,2] 的极值相同(max⁡=3,min⁡=2\max=3,\min=2),乘积 6 一次入账就同时覆盖长度 2 和 3,不需要逐长度写两遍。

代码

Python 版:

python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-02 17:25
# update_at: 2026-10-02 17:25

import sys


def solve() -> None:
    data = iter(map(int, sys.stdin.buffer.read().split()))
    n = next(data)
    values = [next(data) for _ in range(n)]

    # 按"窗口长度"下标做差分:d 是差分数组,前缀和后 d[L] 即第 L 窗口值。
    # a_i <= 100,答案上界 1e5 * 100^2 = 1e9,直接输出,无需取模。
    d = [0] * (n + 3)

    # 两个单调栈存 (值, 下标),栈内下标从栈顶往栈底递减:
    # max 栈的值严格递减、min 栈的值严格递增,元素即"左端点左移时极值发生变化的位置"。
    max_stack: list[tuple[int, int]] = []
    min_stack: list[tuple[int, int]] = []

    for i, x in enumerate(values):
        while max_stack and max_stack[-1][0] <= x:
            max_stack.pop()
        while min_stack and min_stack[-1][0] >= x:
            min_stack.pop()
        max_stack.append((x, i))
        min_stack.append((x, i))

        cur_max = cur_min = x      # 只含 a[i] 这一个左端点时的极值
        left = i                   # 当前已登记的最左左端点
        p_max = len(max_stack) - 2  # 指针停在栈顶的下一层:下一个极值变化点
        p_min = len(min_stack) - 2

        # 左端点从 i 往 0 走,逐个吞并两栈的变化点:
        # 每一步 (cur_max, cur_min) 在左端点 (nxt, left] 上不变,把这段按长度差分入账。
        while p_max >= 0 or p_min >= 0:
            # 两栈都还有变化点时,先走下标更大的那个:更靠近 i 的变化先发生;
            # 同一下标同时是 max/min 变化点时会退化出一段空区间,加减抵消,不影响答案。
            take_max = p_max >= 0 and (p_min < 0 or max_stack[p_max][1] >= min_stack[p_min][1])
            if take_max:
                value, nxt = max_stack[p_max]
                p_max -= 1
            else:
                value, nxt = min_stack[p_min]
                p_min -= 1

            prod = cur_max * cur_min
            d[i - left + 1] += prod  # 左端点 left 对应的窗口长度 = i-left+1
            d[i - nxt + 1] -= prod   # 左端点越过 nxt 后这对极值失效
            left = nxt
            if take_max:
                cur_max = value
            else:
                cur_min = value

        # 剩下左端点 [0, left] 全部落在同一对极值上,长度区间到 i+1 为止。
        prod = cur_max * cur_min
        d[i - left + 1] += prod
        d[i + 2] -= prod

    out: list[str] = []
    total = 0
    for length in range(1, n + 1):
        total += d[length]
        out.append(str(total))
    sys.stdout.write(' '.join(out))


if __name__ == "__main__":
    solve()

C++ 版(同一算法):

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-04 21:50
 * update_at: 2026-10-04 21:50
 */
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100005;

typedef long long ll;

ll n;
int a[MAXN]; // 输入数组,a_i <= 100,用 int 控制内存
ll diff[MAXN + 3]; // diff 按窗口长度做差分,前缀和后 diff[L] 就是第 L 窗口值

// 单调栈元素:value 是极值,index 是它出现的下标。
struct StackNode {
    ll value;
    ll index;
};

StackNode max_stack[MAXN]; // 从右往左看的 max 记录值,值严格递减
StackNode min_stack[MAXN]; // 从右往左看的 min 记录值,值严格递增

void read_input() {
    cin >> n;
    for (ll i = 1; i <= n; i++) {
        cin >> a[i];
    }
}

// 固定右端点 i,让左端点从 i 往 1 左移,两栈按下标从大到小归并出极值不变的段,
// 每段对应一段连续的窗口长度,用差分 O(1) 入账。
void solve() {
    ll max_top = 0; // max_stack 中元素个数,栈顶下标是 max_top
    ll min_top = 0;

    for (ll i = 1; i <= n; i++) {
        ll x = a[i];

        while (max_top > 0 && max_stack[max_top].value <= x) {
            max_top--;
        }
        while (min_top > 0 && min_stack[min_top].value >= x) {
            min_top--;
        }
        max_top++;
        max_stack[max_top].value = x;
        max_stack[max_top].index = i;
        min_top++;
        min_stack[min_top].value = x;
        min_stack[min_top].index = i;

        ll cur_max = x; // 只含左端点 i 时的极值
        ll cur_min = x;
        ll left = i; // 当前已入账的最左左端点
        ll p_max = max_top - 1; // 栈顶的下一层即下一个极值变化点
        ll p_min = min_top - 1;

        while (p_max >= 1 || p_min >= 1) {
            // 两栈都有变化点时先走下标更大的那个,因为更靠近 i 的变化先发生。
            // 同一下标同时是 max/min 变化点时会退化成空区间,加减抵消,无需特判。
            bool take_max = (p_max >= 1) && (p_min < 1 || max_stack[p_max].index >= min_stack[p_min].index);
            ll value;
            ll nxt;
            if (take_max) {
                value = max_stack[p_max].value;
                nxt = max_stack[p_max].index;
                p_max--;
            } else {
                value = min_stack[p_min].value;
                nxt = min_stack[p_min].index;
                p_min--;
            }

            ll prod = cur_max * cur_min;
            diff[i - left + 1] += prod; // 左端点 left 对应窗口长度 i-left+1
            diff[i - nxt + 1] -= prod;  // 左端点越过 nxt 后这对极值失效
            left = nxt;
            if (take_max) {
                cur_max = value;
            } else {
                cur_min = value;
            }
        }

        // 剩下的左端点 [1, left] 极值都不再变化,长度上界是 i。
        ll prod = cur_max * cur_min;
        diff[i - left + 1] += prod;
        diff[i + 1] -= prod;
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    read_input();
    solve();

    ll total = 0;
    for (ll length = 1; length <= n; length++) {
        total += diff[length];
        if (length > 1) {
            cout << ' ';
        }
        cout << total;
    }
    cout << '\n';

    return 0;
}

复杂度

  • 时间:建栈每个元素进栈、弹栈各至多一次,O(n)O(n);每个右端点的走段数 ⩽∣max栈∣+∣min栈∣⩽200\leqslant |max栈|+|min栈| \leqslant 200,合计 O(n⋅A)O(n\cdot A),其中 A=min⁡(n,值域)=100A=\min(n,\text{值域})=100,即约 10710^7 量级。真实数据栈更短(实测最大组 n=98356n=98356 用时 0.13s)。
  • 空间:差分数组 O(n)O(n)、输入输出 O(n)O(n),两个单调栈各至多 100 个元素,峰值约 32MB,低于 128MB 限制。
  • 答案上界 n⋅(max⁡ai)2=109<231n\cdot(\max a_i)^2=10^9<2^{31},题面未要求取模,直接输出。

总结

  • 朴素做法逐窗口求 max⁡×min⁡\max\times\min 是 O(n2)O(n^2) 起步,超时的根因是窗口总数为 Θ(n2)\Theta(n^2)。
  • 换成「固定右端点、左端点左移」后,极值只在单调栈记录的变化点处改变,按变化点归并即可成段处理。
  • ai⩽100a_i\leqslant100 让两个栈(即每右端点的段数)至多各 100,整体 O(100n)O(100n),这是本题数据范围的隐藏用法。
  • 每段对应连续的窗口长度,用差分数组 O(1)O(1) 入账,末尾一次前缀和还原全部 nn 个答案;同下标双变化靠空区间自然抵消,无需特判。