文章目录
单调栈
题目来源为学校开的训练题:
单调栈其实写法特别简单,难的是它的思想:
维护一个具备单调性的栈,目的是及时排除对后续答案没有贡献的部分
A - Bad H
题目链接:POJ-3250
题目大意:
有N头牛,每头牛高度为 H i H_i Hi,一头牛可以看到排在它后面的身高小于它的牛的发型,以一头牛看到另一头牛的发型为事件,问该事件总共发生的次数。
以样例为例:
6
10
3
7
4
12
2
第一个6表示有6头牛
①第一头牛高度为10,可以看到第二、三、四头牛的发型(注意不能看到第五头牛的发型,因为第五头牛比它高,且第五头牛之后的牛即使比第一头牛矮也没法再看到了)
②第三头牛可以看到第四头牛的发型
③第五头牛可以看到第六头牛的发型
故答案为5
分析:
朴素做法是枚举,统计每头牛之后有几头牛比它矮(直到遇到比该牛高的牛),复杂度 O ( N 2 ) O(N^2) O(N2)
优化:
可以发现对于一个单调递减序列,如10、7、4、2、1, 答案显而易见为4+3+2+1,而如果用朴素做法需要进行大量没必要的枚举。因此我们可以利用单调性来做这道题
对于非单调的序列,如10、7、4、8、2、1
一个一个数的进行考虑:
①一开始只有10,答案为0
②加入7之后,形成了长度为2的单调递减序列,答案为1
③加入4之后,形成了长度为3的单调递减序列,答案+2变为3
④加入8之后,因为8比4和7大,所以答案只在原基础上加1(10能看到8),变为4
⑤加入2之后,7和4因为比8小所以肯定看不到2,我们不需要再考虑它们。10和8都比2大,所以能看到2,答案加2,变为6。其实可以将现在的序列看作10、8、2,因为对于之后再加进来的数,7和4不会再对答案产生任何贡献。
⑥同理,加入1之后,10、8、2都能看到1,答案加3,最终答案为9。
最终答案其实是2+1+3+2+1,即两个序列10、7、4和10、8、2、1的答案之和
现在来考虑当一个数加入之后,对答案的贡献。以上面的例子为例,加入8之后对答案的贡献只有1,因为前面只有10比它大,加入2之后,对答案的贡献为2,因为前面有10和8。加入1之后对答案的贡献为3,因为10、8、2都比它大,不妨想象一下,如果这时候再加入7,对答案的贡献为多少?
没错就是2,因为10、8都可以看到这个7,所以贡献为2。
我们还可以发现,其实当加入8之后,8前面的7和4就再也没有用了,因为它们不会再对答案产生任何贡献
那么这道题的做法就出来了,可以维护一个单调递减的栈。每加入一个数,这个数之前的比这个数小的数都可以直接删掉,因为不会再对答案产生贡献,而加入这个数产生的贡献也就是当前栈中数的个数,详情可见代码
代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <cstdlib>
#include <string>
#include <stack>
using namespace std;
stack <long long >st;
int main()
{
int N;
scanf("%d",&N);
long long ans=0;
for(int i=1;i<=N;i++){
long long temp;
scanf("%lld",&temp);
while(!st.empty()&&st.top()<=temp)
st.pop();
ans+=st.size();
st.push(temp);
}
printf("%lld\n",ans);
return 0;
}
B - Feel Good
题目链接:POJ-2796
题目大意:
给定一串数,从中挑选连续的k个数,这k个数的“价值”定义为这k个数中的最小值乘上这k个数的和。求这串数最大的价值为多少
分析:
一看到就感觉和求直方图最大矩形那题很像(下面的C题),朴素思路是枚举每个数作为最小数时,向左向右延伸看能延伸多少。
优化:
维护一个递增的单调栈,当遇到小于栈顶的数X时,将栈中所有大于X的数出栈。对于X来说,X前出栈的所有元素都能作为“以X为最小数时选出的k个数”的一部分,这个信息对X和后面的数仍是有贡献的,其它部分就没用了因此出栈。
很抽象,可结合C题理解,我们只保存对于后面的数仍有贡献的那部分。
坑点:给出的数有可能为0,因此ans的初始值应该设为-1。
具体见代码:
代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <cstdlib>
#include <string>
#include <stack>
using namespace std;
typedef struct{
long long low;//最小值
long long sum;//和
long long left;//左端
long long id;//序号(该数是第几个数)
}node;
stack<node>st;
int main()
{
int N;
scanf("%d",&N);
long long ans=-1;
long long l,r;
for(int i=1;i<=N;i++){
node temp;
scanf("%lld",&temp.low);
temp.sum=temp.low;
temp.left=i;
temp.id=i;
if(st.empty()||temp.low>=st.top().low)
st.push(temp);
else{
long long cnt=0;
while(!st.empty()&&temp.low<st.top().low){
temp.left=st.top().left;
cnt+=st.top().sum;
if(cnt*st.top().low>ans){
ans=cnt*st.top().low;
l=st.top().left;
r=i-1;
}
st.pop();
}
temp.sum+=cnt;
st.push(temp);
}
}
long long t=st.top().id;
long long cnt=0;
while(!st.empty()){
cnt+=st.top().sum;
if(cnt*st.top().low>ans){
ans=cnt*st.top().low;
l=st.top().left;
r=t;
}
st.pop();
}
printf("%lld\n%lld %lld\n",ans,l,r);
return 0;
}
C - Largest Rectangle in a Histogram
题目链接:POJ-2559
题目大意:
给定一个直方图,求最大矩形面积。
分析:
枚举所有可能的矩形面积,找出其中的最大值。朴素做法是对于每个矩形的高度,向左向右延伸,即得到了该高度下最大的矩形面积。
优化:
一个矩形一个矩形地考虑
所有矩形初始宽度为1
①加入第一个矩形,则答案记为第一个矩形面积
②加入第二个矩形,答案可能为第二个矩形的面积,也可能为第一个矩形的面积乘以2
③加入第三个矩形,答案可能为第三个矩形面积,也可能为第二个矩形面积乘2,也可能为第一个矩形面积乘以3
可以发现,在高度单调递增的直方图中,每个矩形对于前面所有矩形的贡献是令它们的宽度加1
④加入第四个矩形后,情况略微复杂,对于第一个矩形,因为它的高度小于第四个矩形,所以它的宽度还是可以加1,第二、三个矩形则不行,它们的宽度没变。对于第四个矩形,它的宽度可以为4。
也就是一个矩形对于它后面的矩形还是有贡献的,但是可以发现它对后面的矩形和下面这个被涂黑的矩形是一样的,上面的部分不会再产生贡献。
那么再加入第五个矩形之后,发现比第四个矩形高度小,宽度可以直接加3。
利用单调栈,维护一个高度递增的栈。在发现高度小于栈顶的矩形后,不断弹出比它高的矩形,但是要注意这些被弹出的矩形其实还会对后面的矩形产生贡献,但是我们可以把它转化为一个大矩形。
也就是把它们的贡献记录在当前要加入的矩形上
复杂度大致为 O ( N ) O(N) O(N)
代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <cstdlib>
#include <string>
#include <stack>
using namespace std;
typedef struct{
long long hight;
long long width;
}rectangle;
stack<rectangle>st;
int main()
{
int N;
while(~scanf("%d",&N)&&N){
long long ans=0;
for(int i=1;i<=N;i++){
rectangle temp;
scanf("%lld",&temp.hight);
temp.width=1;
if(st.empty()||temp.hight>=st.top().hight)
st.push(temp);
else{
long long cnt=0;
while(!st.empty()&&temp.hight<st.top().hight){
cnt+=st.top().width;
ans=max(ans,cnt*st.top().hight);
st.pop();
}
temp.width+=cnt;
st.push(temp);
}
}
long long cnt=0;
while(!st.empty()){
cnt+=st.top().width;
ans=max(ans,cnt*st.top().hight);
st.pop();
}
printf("%lld\n",ans);
}
return 0;
}
D-Garbage collecting station
题目链接:HDU-2559
题目大意:
这题网上找不到题解令我有些懵逼,但是没想到今天给它整出来了,也算满足了我的ak强迫症
题意:有n个垃圾站点,有一辆卡车按照一定的顺序经过这n个垃圾站点,最后把垃圾运到终点站。设运送垃圾的费用为垃圾的质量×运送的距离。题目给出每个垃圾站点的垃圾质量,及其到下一个站点的距离(最后一个垃圾站点的下一站即为终点站)
现在可以令其中两个站点变为储存垃圾的站。假设令第i个站点和第j个站点成为储存垃圾的站,则第1号站点到第i号站点的垃圾运送到i号站即可;第i+1号站点到第j号站点的垃圾运送到j号站即可;第j+1号及之后的站点还是运送到终点站
求新建两个站点后运送垃圾的花费变为了多少?
分析:
先构建模型:
每个站点建立一个矩形,矩形的高为该站点到终点站的距离,矩形的宽为该站点的垃圾的重量。
如上图建立了一个六个站点的直方图
可以明显地看出,该直方图矩形的高度必然是递减的。因为越往后的站点到终点站肯定越近。
构建这个模型的意义在于:
这个红色的矩形代表选择2号站点作为垃圾储存站收到的“效益”,效益即运送垃圾减少的花费。(这个红色矩形的高度为2号站到终点站的距离,宽度为1号站和2号站的垃圾质量之和)
可以发现选择越靠后的站点,虽然垃圾质量增加了,但是和终点站的距离也减小了,因此并不是选择越往后的站点收益越大
这个绿色的矩形则代表了选择5号站点获得的收益。可以发现它和选择2号站点所得到的矩形有重叠的部分,而这两个矩形的面积并(即重叠部分只计算一次)就是选择两个站点所得到的收益。
那么这道题也就转换为了在一个高度递减的直方图中求最大的两个矩形的面积并。
构建完模型之后就是解决了
思路:
因为长和宽一增一减,没有规律。所以我们只能枚举所有可能的矩形来寻找答案。但是 N 2 N^2 N2的复杂度显然还是会有超时的风险,所以尽可能地排除对答案没有贡献的矩形,以减少无用的枚举。
一个矩形一个矩形地考虑,因为只有1或2个矩形时,答案就是矩形的面积之和,所以从加入第3个矩形开始考虑:
加入第三个矩形之后,矩形的选择就多了两种方案:
第一种:
第二种:
可以发现这两种方案底下蓝色的矩形是公共部分,也就是第三个矩形的高度乘上前三个矩形的宽度之和。
同理,加入第四个矩形后会增加三种方案,并且第四个矩形的高乘上前四个矩形的宽度之和是公共部分。
即加入第N个矩形,会增加N-1种方案。而这N-1种方案,并不全都有意义,其中存在着一定的”单调性“
假设上述第二种方案比第一种方案的面积大
在加入第四个矩形后会形成三种新的方案,其中两种:
第一种:
第二种:
这两种方案其实就是加入第三个矩形时的两种方案的延伸。为了区分,分别称其为第
3
1
3_1
31、
3
2
3_2
32和第
4
1
4_1
41、
4
2
4_2
42种方案。
因为之前已经假设第 3 2 3_2 32比第 3 1 3_1 31种方案的矩形面积大,则我们必然可以得到第 4 2 4_2 42比第 4 1 4_1 41种方案面积要大。因为它们增加的面积都是一块矩形,且高度一样,而前者的宽度必然比后者大,则前者增加的面积必然也比后者大!而前者的面积本来就比后者大了,增加的面积又大,则结果也必然更大!
因此我们只需要维护一个单调递减队列就行,维护的内容为:假设加入第i个矩形,那么维护i-1个小矩形,这i-1个中第x个小矩形的面积为直方图中第x个矩形的宽乘上(第x个矩形的高度-加入的第i个矩形的高度)。之所以维护单调递减的队列即,因为后面的小矩形宽度更大,所以有可能超过前面。每加入一个矩形,需要对这个单调队列中所有小矩形进行更新,所以用数组来模拟队列较为方便。
用数组模拟,一个指针p来维护队列的前端即可。
代码:
#include <cstdio>
#include <iostream>
#include <queue>
#include <cstring>
using namespace std;
long long area[10005];
long long width[10005];
long long hight[10005];
long long sum_h[10005];
long long sum_w[10005];
int main()
{
int T;
scanf("%d",&T);
while(T--){
memset(area,0,sizeof(area));
int N;
scanf("%d",&N);
long long sum_area=0;
for(int i=1;i<=N;i++){
scanf("%lld %lld",&width[i],&hight[i]);
sum_w[i]=sum_w[i-1]+width[i];
}
sum_h[N+1]=0;
for(int i=N;i>=1;i--){
sum_h[i]=sum_h[i+1]+hight[i];
sum_area+=(sum_h[i]*width[i]);
}
long long ans=0;
int p=1;
for(int i=1;i<=N;i++){
long long mx=0,pos=p;
for(int j=p;j<i;j++){
area[j]+=sum_w[j]*hight[i-1];
if(area[j]>mx){
mx=area[j];
pos=j;
}
}
p=pos;
ans=max(ans,sum_h[i]*sum_w[i]+mx);
}
printf("%lld\n",sum_area-ans);
}
return 0;
}
E - Largest Submatrix of All 1’s
题目链接:POJ-3494
题目大意:
给定一个m*n的01矩阵,求面积最大的全1矩阵(即元素全为1的矩阵),这里的面积指元素个数
分析:
枚举行,以每一行为底,则将题目转化为了求直方图的最大矩形(C题)。
不知道为啥stack<node> st;
这句话写在for循环里会tle,阿巴阿巴
代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <cstdlib>
#include <string>
#include <stack>
using namespace std;
typedef struct{
int h,w;
}node;
int hight[2005];
stack<node> st;
int main()
{
int m,n;
while(~scanf("%d%d",&m,&n)){
memset(hight,0,sizeof(hight));
int ans=0;
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
int temp;
scanf("%d",&temp);
if(temp) hight[j]++;
else hight[j]=0;
}
for(int j=1;j<=n;j++){
node temp;
temp.h=hight[j];
temp.w=1;
if(st.empty()||hight[j]>=st.top().h)
st.push(temp);
else{
int cnt=0;
while(!st.empty()&&temp.h<st.top().h){
cnt+=st.top().w;
ans=max(ans,cnt*st.top().h);
st.pop();
}
temp.w+=cnt;
st.push(temp);
}
}
int cnt=0;
while(!st.empty()){
cnt+=st.top().w;
ans=max(ans,cnt*st.top().h);
st.pop();
}
}
printf("%d\n",ans);
}
return 0;
}
F - Terrible Sets
题目链接:POJ-2082
题目大意:
这题能写成这么复杂也是够了
给定一些底在x轴上,紧挨着且不会重叠的矩形,题目给出这些矩形的高和宽,求它们能形成的最大的矩形的面积。
分析:
和直方图中求最大矩形几乎一模一样,唯一的区别就是给定的矩形宽不相同。
代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <cstdlib>
#include <string>
#include <stack>
using namespace std;
typedef struct{
int w,h;
}rectangle;
stack<rectangle> st;
int main()
{
int N;
while(~scanf("%d",&N)&&N!=-1){
int ans=0;
for(int i=0;i<N;i++){
rectangle temp;
scanf("%d%d",&temp.w,&temp.h);
if(st.empty()||temp.h>=st.top().h){
st.push(temp);
continue;
}
int cnt=0;
while(!st.empty()&&temp.h<st.top().h){
cnt+=st.top().w;
ans=max(ans,st.top().h*cnt);
st.pop();
}
temp.w+=cnt;
st.push(temp);
}
int cnt=0;
while(!st.empty()){
cnt+=st.top().w;
ans=max(ans,st.top().h*cnt);
st.pop();
}
printf("%d\n",ans);
}
return 0;
}
G - 正方形
题目链接:SCU-2801
题目大意:
在一个N*N的平面中,给出M个障碍物的坐标,要求找到一个最大的内部不含障碍物的正方形
分析:
经典的dp题,但是用单调栈也是可以解决的。和E题几乎差不多,按行枚举,只不过现在要找的是最大正方形而不是最大矩形。
最大正方形的边长就是最大矩形的宽,所以一模一样的做法,只是更新答案的写法不一样而已
其实还是dp更快,因为dp就是 O ( N ) O(N) O(N)的复杂度,单调栈则是接近 O ( N ) O(N) O(N)的复杂度,略慢一点
dp代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <cstdlib>
#include <string>
#include <stack>
using namespace std;
int a[1005][1005];
int dp[1005][1005];
void init(){
for(int i=1;i<=1000;i++)
for(int j=1;j<=1000;j++)
a[i][j]=1;
memset(dp,0,sizeof(dp));
}
int main()
{
int n,m;
while(~scanf("%d%d",&n,&m)){
init();
int ans=0;
for(int i=0;i<m;i++){
int x,y;
scanf("%d%d",&x,&y);
a[x][y]=0;
}
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++){
a[i][j]?dp[i][j]=min(dp[i-1][j-1],min(dp[i-1][j],dp[i][j-1]))+1:dp[i][j]=0;
ans=max(ans,dp[i][j]);
}
printf("%d\n",ans);
}
return 0;
}
单调栈代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <cstdlib>
#include <string>
#include <stack>
using namespace std;
typedef struct{
int h,w;
}rectangle;
stack<rectangle>st;
int a[1005][1005];
void init(){
for(int i=1;i<=1000;i++)
for(int j=1;j<=1000;j++)
a[i][j]=1;
}
int main()
{
int n,m;
while(~scanf("%d%d",&n,&m)){
init();
int ans=0;
for(int i=0;i<m;i++){
int x,y;
scanf("%d%d",&x,&y);
a[x][y]=0;
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++)
if(a[i][j]) a[i][j]+=a[i-1][j];
for(int j=1;j<=n;j++){
rectangle temp;
temp.h=a[i][j];
temp.w=1;
if(st.empty()||temp.h>=st.top().h)
st.push(temp);
else{
int cnt=0;
while(!st.empty()&&temp.h<st.top().h){
cnt+=st.top().w;
ans=max(ans,min(st.top().h,cnt));
st.pop();
}
temp.w+=cnt;
st.push(temp);
}
}
int cnt=0;
while(!st.empty()){
cnt+=st.top().w;
ans=max(ans,min(st.top().h,cnt));
st.pop();
}
}
printf("%d\n",ans);
}
return 0;
}
H - Maximum Submatrix II
题目链接:SCU-3329
分析:
和E题一模一样,把0和1换一下而已
代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <cstdlib>
#include <string>
#include <stack>
using namespace std;
typedef struct{
int h,w;
}node;
stack<node>st;
int hight[1005];
int main()
{
int t;
scanf("%d",&t);
while(t--){
memset(hight,0,sizeof(hight));
int m,n;
int ans=0;
scanf("%d%d",&m,&n);
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
int temp;
scanf("%d",&temp);
if(!temp) hight[j]++;
else hight[j]=0;
}
for(int j=1;j<=n;j++){
node temp;
temp.h=hight[j];
temp.w=1;
if(st.empty()||hight[j]>=st.top().h)
st.push(temp);
else{
int cnt=0;
while(!st.empty()&&hight[j]<st.top().h){
cnt+=st.top().w;
ans=max(ans,cnt*st.top().h);
st.pop();
}
temp.w+=cnt;
st.push(temp);
}
}
int cnt=0;
while(!st.empty()){
cnt+=st.top().w;
ans=max(ans,cnt*st.top().h);
st.pop();
}
}
printf("%d\n",ans);
}
return 0;
}
单调队列
单调队列和单调栈运用的是相同的思想——及时排除掉对答案没有贡献的部分以减小时间复杂度:
以单调队列经典的滑动窗口为例:一个宽度固定的窗口滑过一个数列,求滑过时窗口中的最大值或最小值,要做的无非两点:①不断地删掉窗口的第一个数,然后加入新的数 ②判断新加入的数和删掉的数是否影响窗口内的最值
用一个队列来存放可能为答案的数。如果要求最小值,则维持一个单调递增的队列,则队列的第一个数即为最小值
以:4、7、5、8、6、3、4,宽度为4的窗口求最小值为例
第一个窗口:4、7、5、8
而我们维持的递增队列为:4、5、8
因为7在后续的窗口中绝对不可能再成为最小值,不会再对答案有贡献!所以可以直接从队列中删掉它
开始滑动窗口:
①右移一格,加入的数为6
则4不在这个窗口内了,从前端出队
8比6大,从后端出队,和前面的7同理,8不会再有贡献所以出队
则新队列:5、6
这个窗口的最小值为5
②再右移一格,加入的数为3
该出队的是7,但是因为之前已经把7踢出队列了,所以这一次没有数从前端出队
后面加入的数为3,比5、6都小,所以把5、6都踢掉,将队列更新为:3
这个窗口的最小值为3
③右移一格,加入4
该出队的5已经出队了,4加入队列末端
新队列:3、4
这个窗口的最小值为3
如上得到了四个窗口的最小值分别为:4、5、3、3
单调队列虽然看名称是队列,但严格来讲并不是队列:因为它有两种出队的方式:从队列的前端和后端都可以出队
A.滑动窗口 LibreOJ - 10175
题目链接:LibreOJ-10175
题目大意:
分析:
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
typedef struct{
int id,value;
}node;
const int maxn=1e6+10;
int a[maxn];
node q[maxn]; //单调队列
int head,tail;
int main()
{
int N,K;
scanf("%d%d",&N,&K);
for(int i=0;i<N;i++)
scanf("%d",&a[i]);
//维护单调递增队列求最小值
head=1,tail=0;
for(int i=0;i<K;i++){//将第一个窗口的值加入
node temp;
temp.id=i,temp.value=a[i];
while(tail>=head&&q[tail].value>=temp.value)//维护单调性
tail--;
q[++tail]=temp;
}
int cnt=1;
for(int i=K;i<N;i++){ //滑动窗口
printf("%d ",q[head].value); //输出队首
node temp;
temp.id=i,temp.value=a[i];
while(tail>=head&&q[tail].value>=temp.value)
tail--;
while(tail>=head&&q[head].id<cnt) head++; //将区间外的元素删除
cnt++;
q[++tail]=temp;
}
printf("%d\n",q[head].value);
//维护单调递减队列求最大值
head=1,tail=0;
for(int i=0;i<K;i++){
node temp;
temp.id=i,temp.value=a[i];
while(tail>=head&&q[tail].value<=temp.value)
tail--;
q[++tail]=temp;
}
cnt=1;
for(int i=K;i<N;i++){
printf("%d ",q[head].value); //输出队首
node temp;
temp.id=i,temp.value=a[i];
while(tail>=head&&q[tail].value<=temp.value)
tail--;
while(tail>=head&&q[head].id<cnt) head++;
cnt++;
q[++tail]=temp;
}
printf("%d\n",q[head].value);
return 0;
}