数据结构:单调栈 / 题解:luogu P6503 [COCI 2010/2011 #3] DIFERENCIJA

题目链接:https://www.luogu.com.cn/problem/P6503

前置知识:单调栈

据说本题可以 dp,但是我不会。


将括号拆开,得到 \(\sum_{i=1}^{n}\sum_{j=i}^{n}\max_{i\leq k\leq j}a_k-\sum_{i=1}^{n}\sum_{j=i}^{n}\min_{i\leq k\leq j}a_k\)。前后可以单独处理,原理是类似的。

直接处理每个区间并不好处理,考虑一个 \(a_i\) 为答案做的贡献,即 \(a_i\) 是区间 \([L,R]\) 的最大值(最小值同理,这里以最大值为例,下同)的区间个数乘以 \(a_i\)。容易想到,\(a_i\) 是 \([L,R]\) 的最大值,当且仅当 \(L\leq i\leq R\) 且 \(\forall L\leq j<i,i<j\leq R,a_j<a_i\)。设 \(l_i\) 表示最小的 \(j\) 使得 \(a_{j,j+1,\dots,i-1}\leq a_i\),\(r_i\) 表示最大的 \(j\) 使得 \(a_{i+1,i+2,\dots,j}<a_i\)。[1]那么满足条件的区间左端点 \(L\) 可以取 \([l_i,i]\),右端点 \(R\) 可以取 \([i,r_i]\),根据乘法原理,可知 \(a_i\) 对答案的贡献为 \((i-l_i+1)(r_i-i+1)\times a_i\)。

现在问题转化为对于每个 \(i\),求 \(l_i,r_i\)。

我们可以用单调栈来解决这个问题,这里简单讲一下单调栈。单调栈本质是一个栈,栈中的元素始终具有单调性。举例:这里有一个(严格)单调递增栈,目前栈中的元素是 \(\{1,4,5,7\}\),假设我们要插入一个元素 \(3\),为了维护栈的单调性,我们要不断的弹出栈顶的元素来维护平衡。具体步骤:1. 不断弹出栈顶的元素直到栈顶元素小于新增的元素或栈为空;2. 插入新增元素。 例如上述例子,当前栈顶元素为 \(7\),\(7>3\),弹出该元素,此时栈中剩余 \(\{1,4,5\}\);\(5>3,4>3\),弹出 \(4,5\),此时栈中剩余 \(\{1\}\),\(1<3\),于是加入元素 \(3\)。

std::stack<int> st;//单调栈

int x; std::cin >> x;//新增元素
while(!st.empty() && st.top() >= x) st.pop();
st.push(x);
//注:实际问题中一般在数组上维护单调栈,所以一般存储下标

维护一个严格单调递减栈,每次弹栈后的栈顶元素就是 \(i\) 左侧第一个大于 \(a_i\) 的元素的下标[2],即 \(l_i-1\),\(r_i\) 同理反向维护,于是这题就做完了。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 3e5+5;
int n, ans, a[N], maxl[N], minl[N], maxr[N], minr[N];
stack<int> st_max, st_min;
signed main(){
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    cin >> n;
    for(int i = 1; i <= n; i++) cin >> a[i];
    for(int i = 1; i <= n; i++){
        while(!st_max.empty() && a[st_max.top()] <= a[i]) st_max.pop();
        maxl[i] = st_max.empty() ? 1 : st_max.top() + 1;
        st_max.push(i);
        while(!st_min.empty() && a[st_min.top()] >= a[i]) st_min.pop();
        minl[i] = st_min.empty() ? 1 : st_min.top() + 1;
        st_min.push(i);
    }
    while(st_max.size()) st_max.pop();
    while(st_min.size()) st_min.pop();
    for(int i = n; i >= 1; i--){
        while(!st_max.empty() && a[st_max.top()] < a[i]) st_max.pop();
        maxr[i] = st_max.empty() ? n : st_max.top() - 1;
        st_max.push(i);
        while(!st_min.empty() && a[st_min.top()] > a[i]) st_min.pop();
        minr[i] = st_min.empty() ? n : st_min.top() - 1;
        st_min.push(i);
    }
    for(int i = 1; i <= n; i++){
        ans += a[i] * (i - maxl[i] + 1) * (maxr[i] - i + 1);
        ans -= a[i] * (i - minl[i] + 1) * (minr[i] - i + 1);
    }
    cout << ans << '\n';
    return 0;
}

单调栈每个元素最多进出栈各一次,时间复杂度 \(O(n)\)。

单调栈的应用非常广泛,可以求前后缀最值,解决区间最值,优化 dp 等,下面是一些练习题:

对于区间问题,当正向思考很难得出答案时,不妨换一个角度求解,求每一个位置对答案的贡献,问题自然会迎刃而解。


  1. 如果你注意细节,你可能会问:为什么左边是严格,而右边是不严格的。如果两边都严格/不严格不可以吗?当然不可以。假设有一个序列 \([1,2,5,2]\)。严格情况下,位置 \(2\) 的最小值右边界只能到位置 \(3\),而位置 \(4\) 的最小值左边界只能到 \(3\),那么最小值 \(2\) 在区间 \([2,4]\) 的贡献就会被遗漏。不严格同理会被重复计算。 ↩︎

  2. 简单证明:假设栈顶元素(下标)是 \(j\),那么 \(a_j>a_i\),假设存在 \(k\)(\(j<k<i\))使得 \(a_k>a_i\),那么 \(k\) 不可能是本次弹栈被弹掉的元素,那么说明它在 \(i\) 之前被弹掉了,所以它一定是被一个小于 \(a_i\) 的元素弹掉了,即 \(a_k<a_i\),矛盾!故 \(k\) 不存在。 ↩︎

posted @ 2026-08-22 16:47  Antony_pico  阅读(5)  评论(0)    收藏  举报