单调栈
单调栈是一种内部元素具有单调性的栈,可以解决与“以某个值为最值的最大区间”等问题。
对于一个数组
考虑单调队列维护区间最大值的过程:当
while (!s.empty() && a[i] > a[s.top()]) { r[s.top()] = i; s.pop(); } s.push(i);
例如
例题:P2866 [USACO06NOV] Bad Hair Day S
有
头奶牛,第 头牛的身高为 。每只奶牛往右边看,可以看到严格小于它身高的牛的头顶,直到看到了身高不小于它的牛(看不到这头牛的头顶),或者右边没有其他奶牛为止。第 头牛可以看到的头顶数量为 ,求 。假设有 头牛,高度分别为 ,那么它们分别可以看到 头牛的头发,其总和为 。
分析:如果使用暴力枚举求解,对于每一头牛都往右边枚举身高小于它的牛,那么时间复杂度是
可以转变一下思路,求每头牛能看见几头牛,等价于计算每头牛能被多少其他牛看见。一头牛能被哪些牛看见?左边比它高的牛,再左边比它高的牛,……。可以维护一个序列,满足这个序列里存的都是对于第
最开始栈是空的。身高为
像这样维护一个值单调递减(或者递增)的数据结构,称为单调栈。
参考代码
#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; }
在本题中,单调栈能以
参考代码
#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
是更高的数位。接下来的每一次删除都是同样的方式。
因此需要保留的数位实际上应该是单调不降的,这可以用单调栈来维护。
用单调栈维护单调不降的数位,当栈顶大于当前处理的数位时相当于就是要删除栈顶对应的数位,输入的
如果最后出栈次数没有用完,则删除最后几位直到用完所有出栈次数。
注意输出时去除前导
参考代码
#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; }
例题:P2422 良好的感觉
找到一个区间使得 区间和 乘 区间最小值 最大,
解题思路
枚举每一位
参考代码
#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; }
例题: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 玉蟾宫
有一个
的矩阵,每个格子里写着 R
或者F
。找出其中的一个子矩阵,其元素均为F
并且面积最大。输出它的面积乘以。
解题思路
预处理出每一个格子所处位置向上最多连续的 F
格子的高度,则相当于沿着每一行计算上一题的“最大矩形面积”问题。
如何预处理?设 F
格子高度,则当该格子是 R
时,
对每一行做一遍单调栈,对这一行的每个格子,求出对应的左右边界。扫一遍单调栈以后,最后还在栈里的,就是没有能淘汰它的。针对这种情况,可以设成边界,从右往左的时候可以认为被下标为
每一行计算的时间复杂度为
参考代码
#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)这张纸的长宽分别为。小明将这张纸看成是由 个格子组成,在剪的时候,只能沿着格子的边缘剪。
(2)这张纸有些地方小明以前在上面画过,剪出来的长方形不能含有以前画过的地方。
(3)剪出来的长方形的大小没有限制。
小明看着这张纸,想了好多种剪的方法,可是到底有几种呢?小明数不过来,你能帮帮他吗?输入格式
第一行两个正整数
,表示这张纸的长度和宽度。
接下来有行,每行 个字符,每个字符为 *
或者.
。
字符*
表示以前在这个格子上画过,字符.
表示以前在这个格子上没画过。输出格式
仅一个整数,表示方案数。
样例输入
6 4 .... .*** .*.. .*** ...* .*** 样例输出
38 数据规模
对
的数据,满足
对的数据,满足
对的数据,满足
分析:本题可以通过枚举矩形的四条边,然后判断里面是否全是没有画过的部分,但这样时间复杂度很高,所以需要优化效率。
以行为单位处理,统计以每一行为底边的句型数量。举个例子,假设目前在统计第
令
根据乘法原理,包括这一列最底下的小格子,同时又被这一列的高度限制的长方形的数量是
如何求得
参考代码
#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)
解题思路
枚举哪一栋作为最高的摩天大楼,假设位置
但如果
仿照前缀和的思想,我们可以定义
求出所有的
参考代码
#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 良好的感觉
解题思路
由于所有的元素都是正的,因此对于一个元素
参考代码
#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
解题思路
考虑将
这里最大值那一项和最小值那一项可以分离开计算,并且两者计算方式类似,不妨先分析最大值那一项,最小值那一项同理。
考虑对每个
但是,如果一个区间内的最大值有好几个数相等,那么这种计算方式会重复计算。如何去重?
调整
参考代码
#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; }
这个程序不能通过样例,但却能获得
而无法通过样例的原因是因为样例中存在重复身高的人,当单调栈的栈顶等于当前处理的人的身高时,不管此时出栈还是不出栈,都可能对后续的计算造成影响。例如,身高依次是
所以怎么样才能不遗漏计数呢?实际上栈中如果要维护连续的身高相等的人应该将他们视作一个整体,在栈中同时维护人的身高和人数,当要入栈的人身高和栈顶的人身高相等时,将人数打包。
参考代码
#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; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?