单调栈
单调栈是一种内部元素具有单调性的栈,可以解决与“以某个值为最值的最大区间”等问题。
例题:P2866 [USACO06NOV] Bad Hair Day S
有 \(N \ (1 \le N \le 80000)\) 头奶牛,第 \(i\) 头牛的身高为 \(h_i \ (1 \le h_i \le 10^9)\)。每只奶牛往右边看,可以看到严格小于它身高的牛的头顶,直到看到了身高不小于它的牛(看不到这头牛的头顶),或者右边没有其他奶牛为止。第 \(i\) 头牛可以看到的头顶数量为 \(C_i\),求 \(\sum_{i=1}^n C_i\)。假设有 \(6\) 头牛,高度分别为 \(10,3,7,4,12,2\),那么它们分别可以看到 \(3,0,1,0,1,0\) 头牛的头发,其总和为 \(5\)。
分析:如果使用暴力枚举求解,对于每一头牛都往右边枚举身高小于它的牛,那么时间复杂度是 \(O(n^2)\) 的,无法通过本题。
可以转变一下思路,求每头牛能看见几头牛,等价于计算每头牛能被多少其他牛看见。一头牛能被哪些牛看见?左边比它高的牛,再左边比它高的牛,……。可以维护一个序列,满足这个序列里存的都是对于第 \(i\) 头牛来说,其左边的身高比它高的牛的身高(这会是一个单调递减的序列),可以使用栈来进行维护。
最开始栈是空的。身高为 \(10\) 的牛进栈,原来栈里没有牛,那么答案增加 \(0\);身高为 \(3\) 的牛来了,则需要从栈顶开始,把所有小于等于 \(3\) 的牛都出栈,此时左边剩 \(1\) 头牛,答案增加 \(1\),然后再将其入栈;身高为 \(7\) 的牛来了,从栈顶开始,把所有小于等于 \(7\) 的牛都出栈(比如刚才入栈的 \(3\) 就要出栈),此时左边有 \(1\) 头牛,答案增加 \(1\),然后再将其入栈,……,以此类推。
像这样维护一个值单调递减(或者递增)的数据结构,称为单调栈。
参考代码
#include <cstdio>
#include <stack>
using std::stack;
using ll = long long;
const int N = 80005;
int h[N];
int main()
{
int n; scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &h[i]);
stack<int> s;
ll ans = 0; // 极端情况下,答案是n的平方级别,超过了int范围,需要long long类型
for (int i = 1; i <= n; i++) {
while (!s.empty() && s.top() <= h[i]) s.pop();
ans += s.size();
s.push(h[i]);
}
printf("%lld\n", ans);
return 0;
}
在本题中,单调栈能以 \(O(n)\) 的时间复杂度找到每一个元素右边第一个比它大的数字(就是把这个数字挤出栈的数字),如果设 \(r_i\) 表示 \(h_i\) 右侧第一个大于等于 \(h_i\) 的位置,实际上本题就是在求 \(\sum_{i=1}^n (r_i - i - 1)\)。
参考代码
#include <cstdio>
#include <stack>
using std::stack;
using ll = long long;
const int N = 80005;
int h[N], r[N];
int main()
{
int n; scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &h[i]);
stack<int> s; // 栈中存储的是元素的下标
for (int i = 1; i <= n; i++) {
while (!s.empty() && h[i] >= h[s.top()]) {
r[s.top()] = i; s.pop();
}
s.push(i);
}
while (!s.empty()) { // 注意栈中剩余的元素代表其右侧没有大于等于它的元素
r[s.top()] = n + 1; s.pop();
}
ll ans = 0; // 极端情况下,答案是n的平方级别,超过了int范围,需要long long类型
for (int i = 1; i <= n; i++) ans += r[i] - i - 1;
printf("%lld\n", ans);
return 0;
}
习题:P5788 【模板】单调栈
参考代码
#include <cstdio>
#include <stack>
using std::stack;
const int N = 3000005;
int a[N], f[N];
int main()
{
int n; scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
stack<int> s;
for (int i = 1; i <= n; i++) {
while (!s.empty() && a[i] > a[s.top()]) {
f[s.top()] = i; s.pop();
}
s.push(i);
}
for (int i = 1; i <= n; i++) printf("%d ", f[i]);
return 0;
}
习题:P2947 [USACO09MAR] Look Up S
参考代码
#include <cstdio>
#include <stack>
using std::stack;
const int N = 100005;
int h[N], ans[N];
int main()
{
int n; scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &h[i]);
stack<int> s;
for (int i = 1; i <= n; i++) {
while (!s.empty() && h[i] > h[s.top()]) {
ans[s.top()] = i; s.pop();
}
s.push(i);
}
for (int i = 1; i <= n; i++) printf("%d\n", ans[i]);
return 0;
}
习题:P1106 删数问题
解题思路
依次考虑每一次删除。假如 175438
删一个数,结果必然是一个五位数,当删除某一个数位时相当于找了一个更小的数来做高位,所以应该删除那些下一位数值比当前位小的位,而删除 7
比删除 5
要更好,因为 7
是更高的数位。接下来的每一次删除都是同样的方式。
因此需要保留的数位实际上应该是单调不降的,这可以用单调栈来维护。
用单调栈维护单调不降的数位,当栈顶大于当前处理的数位时相当于就是要删除栈顶对应的数位,输入的 \(k\) 限制了出栈次数。
如果最后出栈次数没有用完,则删除最后几位直到用完所有出栈次数。
注意输出时去除前导 \(0\) 以及特别注意答案就是 \(0\) 的情况。
参考代码
#include <cstdio>
#include <cstring>
#include <stack>
using std::stack;
const int LEN = 255;
char num[LEN], ans[LEN];
int main()
{
int k;
scanf("%s%d", num + 1, &k);
int len = strlen(num + 1);
stack<char> s;
for (int i = 1; i <= len; i++) {
while (!s.empty() && k > 0 && num[i] < s.top()) {
k--; s.pop();
}
s.push(num[i]);
}
while (!s.empty() && k > 0) {
k--; s.pop();
}
// 注意本题要求输出的结果中去掉前导0
int idx = 0;
while (!s.empty()) {
ans[++idx] = s.top(); s.pop();
}
while (idx > 1 && ans[idx] == '0') idx--;
for (int i = idx; i >= 1; i--) printf("%c", ans[i]);
return 0;
}
例题:SP1805 HISTOGRA - Largest Rectangle in a Histogram
如果矩形的高度从左到右递增,那么答案是多少?显而易见,可以尝试以每个矩形的高度作为最终矩形的高度,并把宽度延伸到右边界,得到一个矩形,在所有这样的矩形面积中取最大值就是答案。
受这种思路启发,如果能够知道以每一个高度的矩形作为最终矩形的高度向左、向右最多能延伸到哪,则这一段之间的就是最终矩形的宽度,在所有这样的矩形面积中取最大的。而向左、向右最多能延伸到哪实际上就是找每个高度的左边、右边第一个高度低于自己的位置。这一点可以通过单调栈实现。
参考代码
#include <cstdio>
#include <stack>
#include <algorithm>
using std::max;
using std::stack;
using ll = long long;
const int N = 100005;
int h[N], l[N], r[N];
int main()
{
while (true) {
int n; scanf("%d", &n);
if (n == 0) break;
for (int i = 1; i <= n; i++) scanf("%d", &h[i]);
stack<int> s;
for (int i = 1; i <= n; i++) {
while (!s.empty() && h[s.top()] > h[i]) {
r[s.top()] = i; s.pop();
}
s.push(i);
}
while (!s.empty()) {
r[s.top()] = n + 1; s.pop();
}
for (int i = n; i >= 1; i--) {
while (!s.empty() && h[s.top()] > h[i]) {
l[s.top()] = i; s.pop();
}
s.push(i);
}
while (!s.empty()) {
l[s.top()] = 0; s.pop();
}
ll ans = 0;
for (int i = 1; i <= n; i++) {
ans = max(ans, 1ll * h[i] * (r[i] - l[i] - 1));
}
printf("%lld\n", ans);
}
return 0;
}
例题:P4147 玉蟾宫
有一个 \(N \times M \ (1 \le N,M \le 1000)\) 的矩阵,每个格子里写着
R
或者F
。找出其中的一个子矩阵,其元素均为F
并且面积最大。输出它的面积乘以 \(3\)。
分析:预处理出每一个格子所处位置向上最多连续的 F
格子的高度,则相当于沿着每一行计算上一题的“最大矩形面积”问题。
如何预处理?设 \(h_{i,j}\) 表示第 \(i\) 行第 \(j\) 列的格子向上所能延伸的连续 F
格子高度,则当该格子是 R
时,\(h_{i,j} = 0\),当该格子是土地时,\(h_{i,j} = h_{i-1,j} + 1\)。
参考代码
#include <cstdio>
#include <stack>
#include <algorithm>
using std::stack;
using std::max;
const int N = 1005;
char f[5];
int a[N][N], l[N], r[N];
int main()
{
int n, m; scanf("%d%d", &n, &m);
int ans = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
scanf("%s", f);
a[i][j] = f[0] == 'R' ? 0 : a[i - 1][j] + 1;
}
stack<int> s;
for (int j = 1; j <= m; j++) {
while (!s.empty() && a[i][j] < a[i][s.top()]) {
r[s.top()] = j; s.pop();
}
s.push(j);
}
while (!s.empty()) {
r[s.top()] = m + 1; s.pop();
}
for (int j = m; j >= 1; j--) {
while (!s.empty() && a[i][j] < a[i][s.top()]) {
l[s.top()] = j; s.pop();
}
s.push(j);
}
while (!s.empty()) {
l[s.top()] = 0; s.pop();
}
for (int j = 1; j <= m; j++) {
ans = max(ans, a[i][j] * (r[j] - l[j] - 1));
}
}
printf("%d\n", ans * 3);
return 0;
}
例题:P1950 长方形
小明今天突发奇想,想从一张用过的纸中剪出一个长方形。
为了简化问题,小明做出如下规定:
(1)这张纸的长宽分别为 \(n,m\)。小明将这张纸看成是由\(n \times m\)个格子组成,在剪的时候,只能沿着格子的边缘剪。
(2)这张纸有些地方小明以前在上面画过,剪出来的长方形不能含有以前画过的地方。
(3)剪出来的长方形的大小没有限制。
小明看着这张纸,想了好多种剪的方法,可是到底有几种呢?小明数不过来,你能帮帮他吗?输入格式
第一行两个正整数 \(n,m\),表示这张纸的长度和宽度。
接下来有 \(n\) 行,每行 \(m\) 个字符,每个字符为*
或者.
。
字符*
表示以前在这个格子上画过,字符.
表示以前在这个格子上没画过。输出格式
仅一个整数,表示方案数。
样例输入
6 4 .... .*** .*.. .*** ...* .***
样例输出
38
数据规模
对 \(10\%\) 的数据,满足 \(1\leq n\leq 10,1\leq m\leq 10\)
对 \(30\%\) 的数据,满足 \(1\leq n\leq 50,1\leq m\leq 50\)
对 \(100\%\) 的数据,满足 \(1\leq n\leq 1000,1\leq m\leq 1000\)
分析:本题可以通过枚举矩形的四条边,然后判断里面是否全是没有画过的部分,但这样时间复杂度很高,所以需要优化效率。
以行为单位处理,统计以每一行为底边的句型数量。举个例子,假设目前在统计第 \(4\) 行。
令 \(h_i\) 表示第 \(i\) 列从底往上延伸空格子的数量,\(l_i\) 表示这一列左边第一个满足 \(h\) 值小于等于 \(h_i\) 的列号(如果没有的话就是 \(0\)),\(r_i\) 表示这一列右边第一个满足 \(h\) 值小于 \(h_i\) 的列号(如果没有的话就是 \(m+1\))。
根据乘法原理,包括这一列最底下的小格子,同时又被这一列的高度限制的长方形的数量是 \((i - l_i) \times (r_i - i) \times h_i\)。为什么 \(l\) 和 \(r\) 的计算一个带等号另一个不带等号,因为这样计数才不会重复不会遗漏(思考为什么?)。每一行都用这种方式处理,然后将每一行的结果汇总就得到了整个问题的结果。
\(h_i\) 如何计算?如果这个格子是画过的格子,则 \(h_i = 0\),否则 \(h_i\) 的值就是同一列上一行的 \(h_i\) 的值再加 \(1\)。
如何求得 \(l_i\) 和 \(r_i\) 呢?使用单调栈。求 \(r_i\) 需要找到严格小于的,那么判断是否将栈顶元素挤出时,如果待处理的元素等于栈顶元素时不必出栈,只有遇到更小的才会被挤出来,满足栈是单调不减的;而求 \(l_i\) 需要找到小于等于的,则从后往前计算,判断是否将栈顶元素挤出时,如果这个元素小于等于栈顶元素,那么栈顶元素就会被挤出,满足栈是单调递增的。
参考代码
#include <cstdio>
#include <stack>
using std::stack;
using ll = long long;
const int N = 1005;
char ch[N][N];
int h[N][N], l[N], r[N];
int main()
{
int n, m; scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%s", ch[i] + 1);
for (int j = 1; j <= m; j++) {
h[i][j] = ch[i][j] == '*' ? 0 : h[i - 1][j] + 1;
}
}
ll ans = 0; // 需要考虑极端情况:n=m=1000,而且全是没画过的格子,长方形的数量是n的4次方数量级
for (int i = 1; i <= n; i++) {
// 为了方便在出栈时求出每个元素左边和右边的符合要求的位置
// 栈里实际存储的是下标而不是元素本身
stack<int> s;
// 顺着求右边第一个小于这个数的位置
for (int j = 1; j <= m; j++) {
while (!s.empty() && h[i][s.top()] > h[i][j]) {
r[s.top()] = j; s.pop();
}
s.push(j);
}
while (!s.empty()) {
r[s.top()] = m + 1; s.pop();
}
// 倒着求左边第一个小于等于这个数的位置
for (int j = m; j >= 1; j--) {
while (!s.empty() && h[i][s.top()] >= h[i][j]) {
l[s.top()] = j; s.pop();
}
s.push(j);
}
while (!s.empty()) {
l[s.top()] = 0; s.pop();
}
for (int j = 1; j <= m; j++) {
ans += 1ll * h[i][j] * (j - l[j]) * (r[j] - j);
}
}
printf("%lld\n", ans);
return 0;
}
习题:P1901 发射站
解题思路
根据题意,“一个发射站发出的能量被两边最近的且比它高的发射站接收”,所以就是要求出每一个发射站左边/右边第一个更高的发射站的位置,进而将发射站的能量累加上去,最后找出接收能量最多的发射站。
参考代码
#include <cstdio>
#include <stack>
#include <algorithm>
using std::stack;
using std::max;
const int N = 1000005;
int h[N], v[N], l[N], r[N], power[N];
int main()
{
int n; scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d%d", &h[i], &v[i]);
stack<int> s;
// 求出每个发射站右边第一个更高的发射站位置
for (int i = 1; i <= n; i++) {
while (!s.empty() && h[i] > h[s.top()]) {
r[s.top()] = i; s.pop();
}
s.push(i);
}
while (!s.empty()) s.pop();
// 求出每个发射站左边第一个更高的发射站位置
for (int i = n; i >= 1; i--) {
while (!s.empty() && h[i] > h[s.top()]) {
l[s.top()] = i; s.pop();
}
s.push(i);
}
// 更新每个发射站接收到的能量
for (int i = 1; i <= n; i++) {
if (l[i] > 0) power[l[i]] += v[i];
if (r[i] > 0) power[r[i]] += v[i];
}
int ans = 0;
// 找出接收最多能量的发射站接收到的能量值
for (int i = 1; i <= n; i++) ans = max(ans, power[i]);
printf("%d\n", ans);
return 0;
}
习题:CF1313C2 Skyscrapers (hard version)
解题思路
枚举哪一栋作为最高的摩天大楼,假设位置 \(i\) 的楼是最高楼,则其建设高度为其限高 \(m_i\),那么其左边的楼建设高度都要小于等于 \(m_i\),考虑 \(i-1\) 这个位置,如果 \(m_{i-1} \ge m_i\),则 \(i-1\) 这个位置建设高度也是 \(m_i\),接下来的位置同理……
但如果 \(m_{i-1} < m_i\),则 \(i-1\) 位置的建楼高度就是 \(m_{i-1}\),从这里我们可以得到一个结论:如果 \(j\) 是 \(i\) 左边第一个使得 \(m_j < m_i\) 的位置,则当位置 \(i\) 作为所有楼中的最高楼时,从 \(j+1\) 到 \(i\) 这一段的建楼高度都是 \(m_i\)。
仿照前缀和的思想,我们可以定义 \(lsum_i\) 表示当位置 \(i\) 作为最高楼时,从 \(1 \sim i\) 的建楼总高度,则有 \(lsum_i = lsum_j + (i - j) \times m_i\),其中 \(j\) 表示 \(m_i\) 左边第一个有 \(m_j < m_i\) 的位置,而每一个 \(i\) 对应的这个 \(j\) 可以通过单调栈在 \(O(n)\) 的时间复杂度下全部求出。同理,还可以定义 \(rsum_i\) 表示当位置 \(i\) 作为最高楼时,从 \(i \sim n\) 的建楼总高度,则有 \(rsum_i = rsum_j + (j - i) \times m_i\),\(j\) 表示 \(m_i\) 右边第一个有 \(m_j < m_i\) 的位置。
求出所有的 \(lsum_i\) 和 \(rsum_i\) 之后,如果位置 \(i\) 作为最高楼的位置,那么总的建楼高度为 \(lsum_i + rsum_i - m_i\),因而只需扫描一遍找到最优的位置即可。
参考代码
#include <cstdio>
#include <stack>
#include <algorithm>
using std::stack;
using std::min;
using ll = long long;
const int N = 500005;
int m[N], ans[N];
int l[N], r[N]; // l[i]/r[i]表示左边/右边第一个小于m[i]的位置
ll lsum[N], rsum[N];
int main()
{
int n; scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &m[i]);
stack<int> s;
for (int i = 1; i <= n; i++) {
while (!s.empty() && m[i] < m[s.top()]) {
r[s.top()] = i; s.pop();
}
s.push(i);
}
while (!s.empty()) {
r[s.top()] = n + 1; s.pop();
}
for (int i = n; i >= 1; i--) {
while (!s.empty() && m[i] < m[s.top()]) {
l[s.top()] = i; s.pop();
}
s.push(i);
}
while (!s.empty()) {
l[s.top()] = 0; s.pop();
}
for (int i = 1; i <= n; i++) {
lsum[i] = lsum[l[i]] + 1ll * (i - l[i]) * m[i];
}
for (int i = n; i >= 1; i--) {
rsum[i] = rsum[r[i]] + 1ll * (r[i] - i) * m[i];
}
int mid = 0; ll maxsum = 0;
for (int i = 1; i <= n; i++) {
ll sum = lsum[i] + rsum[i] - m[i];
if (sum > maxsum) {
maxsum = sum; mid = i;
}
}
ans[mid] = m[mid];
for (int i = mid - 1; i >= 1; i--) {
ans[i] = min(ans[i + 1], m[i]);
}
for (int i = mid + 1; i <= n; i++) {
ans[i] = min(ans[i - 1], m[i]);
}
for (int i = 1; i <= n; i++) printf("%d ", ans[i]);
return 0;
}
习题:P2422 良好的感觉
解题思路
由于所有的元素都是正的,因此对于一个元素 \(a_i\) 来说,如果它作为某个区间中最不舒服的那一天,也就是该区间中最小的元素,这个区间越长越好(因为区间总和不可能因为区间变长而减小)。因此如果对于每一个 \(a_i\) 求出它左边、右边第一个小于 \(a_i\) 的位置 \(l_i, r_i\),则以 \(a_i\) 作为区间最小值的最长区间就是 \([l_i + 1, r_i - 1]\),那么对应的舒适程度为 \((r_i - l_i - 1) \times (a_{l_i+1} + \cdots + a_{r_i - 1})\),找出最大的结果即可。
\(l_i, r_i\) 可以利用单调栈预处理求出,区间和的计算可以预处理前缀和来快速求得,时间复杂度 \(O(n)\)。
参考代码
#include <cstdio>
#include <algorithm>
#include <stack>
using std::max;
using std::stack;
using ll = long long;
const int N = 100005;
int a[N], l[N], r[N];
ll sum[N];
int main()
{
int n; scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]); sum[i] = sum[i - 1] + a[i];
}
stack<int> s;
for (int i = 1; i <= n; i++) {
while (!s.empty() && a[i] < a[s.top()]) {
r[s.top()] = i; s.pop();
}
s.push(i);
}
while (!s.empty()) {
r[s.top()] = n + 1; s.pop();
}
for (int i = n; i >= 1; i--) {
while (!s.empty() && a[i] < a[s.top()]) {
l[s.top()] = i; s.pop();
}
s.push(i);
}
while (!s.empty()) {
l[s.top()] = 0; s.pop();
}
ll ans = 0;
for (int i = 1; i <= n; i++) {
ans = max(ans, (sum[r[i] - 1] - sum[l[i]]) * a[i]);
}
printf("%lld\n", ans);
return 0;
}
习题:P6503 [COCI2010-2011#3] DIFERENCIJA
解题思路
考虑将 \(\sum \limits_{i=1}^{n} \sum \limits_{j=i}^{n} (\max \limits_{i\le k\le j} a_k-\min \limits_{i\le k\le j} a_k)\) 拆成 \((\sum \limits_{i=1}^{n} \sum \limits_{j=i}^{n} \max \limits_{i\le k\le j} a_k) - (\sum \limits_{i=1}^{n} \sum \limits_{j=i}^{n} \min \limits_{i\le k\le j} a_k)\)。
这里最大值那一项和最小值那一项可以分离开计算,并且两者计算方式类似,不妨先分析最大值那一项,最小值那一项同理。
考虑对每个 \(a_i\) 计算它对整个式子的贡献,\(a_i\) 要有贡献需要它作为一个区间的最大值,因此如果设 \(l_i\) 代表 \(a_i\) 左边第一个大于 \(a_i\) 的位置,\(r_i\) 代表 \(a_i\) 右边第一个大于 \(a_i\) 的位置,则此时有 \((i - l_i) \times (r_i - i)\) 个区间会以 \(a_i\) 作为区间最大值,因此这就是 \(a_i\) 对式子的贡献。
但是,如果一个区间内的最大值有好几个数相等,那么这种计算方式会重复计算。如何去重?
调整 \(l_i, r_i\) 的含义,让 \(l_i\) 和 \(r_i\) 中的其中一个改为左边/右边第一个大于等于 \(a_i\) 的位置,另一个不带等号,这样就不会导致重复计数。这样的 \(l_i\) 和 \(r_i\) 用单调栈预处理。
参考代码
#include <cstdio>
#include <stack>
using std::stack;
using ll = long long;
const int N = 300005;
int a[N], lmax[N], rmax[N], lmin[N], rmin[N];
int main()
{
int n; scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
stack<int> smax, smin;
for (int i = 1; i <= n; i++) {
while (!smax.empty() && a[i] > a[smax.top()]) {
rmax[smax.top()] = i; smax.pop();
}
smax.push(i);
while (!smin.empty() && a[i] < a[smin.top()]) {
rmin[smin.top()] = i; smin.pop();
}
smin.push(i);
}
while (!smax.empty()) {
rmax[smax.top()] = n + 1; smax.pop();
}
while (!smin.empty()) {
rmin[smin.top()] = n + 1; smin.pop();
}
for (int i = n; i >= 1; i--) {
while (!smax.empty() && a[i] >= a[smax.top()]) {
lmax[smax.top()] = i; smax.pop();
}
smax.push(i);
while (!smin.empty() && a[i] <= a[smin.top()]) {
lmin[smin.top()] = i; smin.pop();
}
smin.push(i);
}
while (!smax.empty()) {
lmax[smax.top()] = 0; smax.pop();
}
while (!smin.empty()) {
lmin[smin.top()] = 0; smin.pop();
}
ll sum_max = 0, sum_min = 0;
for (int i = 1; i <= n; i++) {
sum_max += 1ll * (i - lmax[i]) * (rmax[i] - i) * a[i];
sum_min += 1ll * (i - lmin[i]) * (rmin[i] - i) * a[i];
}
printf("%lld\n", sum_max - sum_min);
return 0;
}
习题:P1823 [COI2007] Patrik 音乐会的等待
解题思路
由于两个人可以互相看到,为了避免重复计算,下面只考虑每个人的单个方向。
先假设每个人身高不一样,考虑一个人的右边,最远的互相看到的人就是右边第一个高于他的人,再右边的人就看不到了,因此可以维护一个单调递减的栈。当栈顶矮于当前正在处理的人时,进行出栈,并且出栈的人和当前这个人就是一对互相看到的人。注意如果出栈之后栈中仍有元素,则这也是一对互相看到的人,相当于此时的栈顶和当前这个人可以互相看到(相当于栈顶是当前这个人左边第一个比他高的人,两个人可以互相看到,但是栈里其他的人就不可能和当前的人互相看到了)。
参考代码
#include <cstdio>
#include <stack>
using std::stack;
using ll = long long;
const int N = 500005;
int h[N];
int main()
{
int n; scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &h[i]);
stack<int> s;
ll ans = 0;
for (int i = 1; i <= n; i++) {
while (!s.empty() && h[i] > s.top()) { // 这个地方不管是>还是>=都有正确性问题
ans++; s.pop();
}
if (!s.empty()) ans++;
s.push(h[i]);
}
printf("%lld\n", ans);
return 0;
}
这个程序不能通过样例,但却能获得 \(20\) 分,原因是当每个人的身高不会重复时程序是完全正确的。
而无法通过样例的原因是因为样例中存在重复身高的人,当单调栈的栈顶等于当前处理的人的身高时,不管此时出栈还是不出栈,都可能对后续的计算造成影响。例如,身高依次是 \([2,1,1,2]\),实际上中间这两个身高为 \(1\) 的人和第一个人都可以形成互相看见的对,和最后一个人也都可以形成互相看见的对。而单调栈在相等时如果选择出栈,则第二个人与第四个人形成的互相看见的对没被统计到;如果选择不出栈,则第三个人和第一个人形成的互相看见的对没被统计到。
所以怎么样才能不遗漏计数呢?实际上栈中如果要维护连续的身高相等的人应该将他们视作一个整体,在栈中同时维护人的身高和人数,当要入栈的人身高和栈顶的人身高相等时,将人数打包。
参考代码
#include <cstdio>
#include <stack>
#include <utility>
using std::stack;
using std::pair;
using ll = long long;
using pii = pair<int, int>;
int main()
{
int n; scanf("%d", &n);
stack<pii> s; // 身高,人数
ll ans = 0;
for (int i = 1; i <= n; i++) {
int x; scanf("%d", &x);
int cnt = 1;
while (!s.empty() && x >= s.top().first) {
ans += s.top().second;
if (s.top().first == x) cnt += s.top().second;
s.pop();
}
if (!s.empty()) ans++;
s.push({x, cnt});
}
printf("%lld\n", ans);
return 0;
}