「学习笔记」单调栈

一.原理

单调栈,顾名思义,就是从栈底到栈顶有序的栈,很多人认为其只是一种思想,这种思想借助栈后进先出的特性,在一些特定场合应用起来能够有效的降低时间复杂度。实现起来很简单,主要是如何应用这种特性去解决实际问题。下面以几道例题来体会这种思想。

二.习题练习

1.Largest Rectangle in a Histogram

题目链接:http://poj.org/problem?id=2559

题意:有n个矩形,宽度都是1,给出这n个矩形的高度,求由这n个矩形组成的图形中的最大矩形面积。

解析:如果以每个矩形为中心,我们要的是它能向两边延伸的最大距离,但要求延伸之处矩形高度都要大于等于中心矩形的高度。如果暴力的话需要两重循环,一重定中心,一重确定延伸的距离,如果应用单调栈,就可以线性解决。下面是应用单调栈解题思路:
我们不妨定义一个从栈底到栈顶的严格递增栈,栈内元素为一个 p a i r < x , y > pair<x,y> pair<x,y>,其中x表示当前矩形高度,y表示其能向左边延伸的最大距离(包括本身)。这样我们从前往后遍历,如果栈为空或者当前元素 a [ i ] a[i] a[i]严格大于栈顶元素,说明其目前向左延伸的距离为 1 1 1(就是其本身),直接将 p a i r < a [ i ] , 1 > pair<a[i],1> pair<a[i],1>其压入栈中,不然,则会破坏单调栈性质,将栈顶元素依次弹出(在此维护矩形面积最大值,因为弹出的矩形其向右延伸的距离也已经确定),直到满足单调栈性质或者弹空。此时当前矩形向左延伸的距离也确定了,将其高度以及咱计算的其向左延伸的距离一起压入栈,如此操作,复杂度只有 O ( n ) O(n) O(n),已经很优秀了。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <stack>
#include <algorithm>
using namespace std;
typedef long long ll;
typedef pair<ll,int> P;
const int inf=0x3f3f3f3f;
const int maxn=1e5+7;
#define ft first
#define sd second
int n;
ll a[maxn];
stack<P> s;
int main()
{
    while(~scanf("%d",&n)){
        if(!n)break;
        while(!s.empty())s.pop();
        for(int i=1;i<=n;i++){
            scanf("%lld",&a[i]);
        }
        ll ans=0;
        for(int i=1;i<=n;i++){
            if(s.empty())s.push(P(a[i],1));//其向左延伸的最大距离为1
            else {
                if(a[i]>s.top().ft)s.push(P(a[i],1));//其向左延伸的最大距离为1
                else {
                    int len=0;//为栈顶元素向右延伸的最大距离
                    while(!s.empty()&&s.top().ft>=a[i]){
                        ans=max(ans,s.top().ft*(s.top().sd+len));
                        len=s.top().sd+len;
                        s.pop();
                    }
                    s.push(P(a[i],len+1));
                }
            }
        }
        int len=0;
        while(!s.empty()){
            ans=max(ans,s.top().ft*(s.top().sd+len));
            len=s.top().sd+len;
            s.pop();
        }
        cout<<ans<<'\n';
    }
    return 0;
}
2.Bad Hair Day

题目链接:http://poj.org/problem?id=3250

题意:一排奶牛,头朝右,奶牛 i i i能看到 “ i + 1 , i + 2.. ” “i+1,i+2..” i+1,i+2..奶牛的头顶,只有 " i + 1 , i + 2.. " "i+1,i+2.." "i+1,i+2.."奶牛的高度严格小于 i i i的才行,要求输出所有奶牛能看到的奶牛头的数量。换句话说,就是一组数,统计每个数相邻右边连续小于它的数的个数,相加输出。

解析:朴素做法,固定i,去右遍历找有几个连续小于i号奶牛身高的,贡献累加入答案中,复杂度 O ( n 2 ) O(n^2) O(n2),可以尝试用单调栈求解,定义一个由栈底到栈顶的单调递减栈,栈内元素为一个 p a i r < x , y > pair<x,y> pair<x,y>,其中x指奶牛的身高,y指奶牛的位置,因为在出栈时,预出栈奶牛所能看到的右边界已固定,为 i − 1 i-1 i1(参考代码),如果记录了栈内奶牛的位置,就可以很容易的做差求距离了。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <stack>
using namespace std;
typedef long long ll;
typedef pair<int,int> P;
const int inf=0x3f3f3f3f;
const int maxn=8e4+7;
#define pb push_back
#define ft first
#define sd second
int t;
int n;
int a[maxn];
stack<P> s;
int main()
{
    while(~scanf("%d",&n)){
        for(int i=1;i<=n;i++){
            scanf("%d",&a[i]);
        }
        while(!s.empty())s.pop();
        ll ans=0;
        for(int i=1;i<=n;i++){
            if(s.empty()||s.top().ft>a[i])s.push(P(a[i],i));//满足递减就进栈
            else {
                while(!s.empty()&&s.top().ft<=a[i]){//要出栈的奶牛能看的范围已确定
                    ans+=(ll)(i-s.top().sd-1);
                    s.pop();
                }
                s.push(P(a[i],i));
            }
        }
        while(!s.empty()){
            ans+=(ll)(n-s.top().sd);
            s.pop();
        }
        cout<<ans<<'\n';
    }
    return 0;
}
3.Largest Submatrix of All 1’s

题目链接:http://poj.org/problem?id=3494

题意:去一个01矩阵找最大1子矩阵。

解析:预处理后和上面第一题差不多,具体细节请点大佬博客,讲的很详细。本题也可以用悬线法做,此处不再解释,请自行查阅相关资料。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <stack>
using namespace std;
typedef long long ll;
typedef pair<int,int> P;
const int inf=0x3f3f3f3f;
const int maxn=2e3+7;
#define pb push_back
#define ft first
#define sd second
int t;
int n,m;
int a[maxn][maxn];
stack<P> s;
int main()
{
    while(~scanf("%d%d",&m,&n)){
        for(int i=1;i<=m;i++){
            for(int j=1;j<=n;j++){
                scanf("%d",&a[i][j]);
                if(a[i][j])a[i][j]=a[i-1][j]+1;//预处理
            }
        }
        int ans=0;
        for(int i=1;i<=m;i++){//针对每一行,跑单调栈,每一行跑的过程和第一题差不多
            while(!s.empty())s.pop();
            for(int j=1;j<=n;j++){
                if(s.empty()||s.top().ft<a[i][j])s.push(P(a[i][j],1));
                else {
                    int len=0;
                    while(!s.empty()&&s.top().ft>=a[i][j]){
                        ans=max(ans,s.top().ft*(s.top().sd+len));
                        len+=s.top().sd;
                        s.pop();
                    }
                    s.push(P(a[i][j],len+1));
                }
            }
            int len=0;
            while(!s.empty()){
                ans=max(ans,s.top().ft*(s.top().sd+len));
                len+=s.top().sd;
                s.pop();
            }
        }
        cout<<ans<<'\n';
    }
    return 0;
}
4.Feel Good

题目链接:http://poj.org/problem?id=2796

题意:简而言之,就是让找出一个区间,区间最小值乘上区间和最大。

解析:先想朴素做法,也就是将每个数作为最小值,求向两边能延申的最大距离,对每个数都求一遍,过程更新最大值即可,但复杂度 O ( n 2 ) O(n^2) O(n2),需要优化,可以用单调栈优化复杂度。
首先可以用单调栈求出,每个数能向左和右扩展的最远距离的位置,分别存入数组中,预处理出原数组的前缀数组,方便最后求区间和,最后直接遍历,确定最大值即可。用单调栈确定每个数向右延申的最大距离,也就是去其右边找比其小的第一个数的位置,确定每个数向左延申的最大距离就去其左边找。定义一个由栈底到栈顶的单调递增栈,栈内元素存数的下标,实则栈内下标所对应的数表现出递增性,保证所有元素都出栈,可以将两头空处设为-1,细节看代码吧。

#include <iostream>
#include <cstdio>
#include <stack>
using namespace std;
typedef long long ll;
const int inf=0x3f3f3f3f;
const int maxn=1e5+10;
#define pb push_back
#define ft first
#define sd second
int n,m;

ll a[maxn];
ll f[maxn];
int l[maxn],r[maxn];
stack<int> s;
int main()
{
    scanf("%d",&n);
    while(!s.empty())s.pop();
    for(int i=1;i<=n;i++){
        scanf("%lld",&a[i]);
        f[i]=f[i-1]+a[i];//前缀数组
    }
    a[n+1]=a[0]=-1;//保证所有元素都出栈
    int ln=-1,rn=-1;ll ans=-1;
    for(int i=1;i<=n+1;i++){//确定每个数的右边界
        while(!s.empty()&&a[i]<a[s.top()]){
            r[s.top()]=i-1;s.pop();
        }
        s.push(i);
    }
    while(!s.empty())s.pop();
    for(int i=n;i>=0;i--){//确定每个数的左边界
        while(!s.empty()&&a[i]<a[s.top()]){
            l[s.top()]=i+1;
            s.pop();
        }
        s.push(i);
    }
    for(int i=1;i<=n;i++){//确定最大值
        if(ans<a[i]*(f[r[i]]-f[l[i]-1])){
            ans=a[i]*(f[r[i]]-f[l[i]-1]);
            ln=l[i],rn=r[i];
        }
    }
    cout<<ans<<'\n';
    printf("%d %d\n",ln,rn);
    return 0;
}
5.Largest Common Submatrix

题目来源:2019 ICPC 银川站 K题,银牌题

题意:找出两个矩阵最大相同子矩阵,保证矩阵内元素各不相同。

解析:和第三题差不多,预处理后,和第一题相似,预处理的方式和第三题一样,先确定每一列每个数能向上延申的最大距离,然后对每行跑单调栈,跑的过程和第一题一样,但是需要注意,有一个限制就是除了延申距离作为高度跑单调栈外,需要先确定合法区间段,也就是对每一行划分成很多合法的区间段,对这些区间段跑单调栈,细节看代码吧,建议先解决第一和第三题,这题就容易理解了。

#include <iostream>
#include <cstdio>
#include <stack>
using namespace std;
typedef long long ll;
typedef pair<int,int> P;
const int inf=0x3f3f3f3f;
const int maxn=1e3+7;
const int N=1e6+10;
#define pb push_back
#define ft first
#define sd second
int n,m,ans;

int a[maxn][maxn],b[maxn][maxn];
P pos[N];//确定n矩阵每个数的位置,方便后面预处理和求合法区间段
int dp[maxn][maxn];//预处理数组
stack<P> s;
bool judge(int x,int y){//判断这行这两个相邻数字是否合法
    P p1=pos[a[x][y-1]],p2=pos[a[x][y]];
    if(p1.ft==p2.ft&&p1.sd+1==p2.sd)return true;
    return false;
}
void solve(int ln,int rn,int p[]){//跑单调栈
    for(int i=ln;i<=rn;i++){
        if(s.empty())s.push(P(p[i],1));//其向左延伸的最大距离为1
        else {
            if(p[i]>s.top().ft)s.push(P(p[i],1));//其向左延伸的最大距离为1
            else {
                int len=0;//为栈顶元素向右延伸的最大距离
                while(!s.empty()&&s.top().ft>=p[i]){
                    ans=max(ans,s.top().ft*(s.top().sd+len));
                    len+=s.top().sd;
                    s.pop();
                }
                s.push(P(p[i],len+1));
            }
        }
    }
    int len=0;
    while(!s.empty()){
        ans=max(ans,s.top().ft*(s.top().sd+len));
        len=s.top().sd+len;
        s.pop();
    }
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            scanf("%d",&a[i][j]);
            dp[1][j]=1;//第一行先设为1
        }
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            scanf("%d",&b[i][j]);
            pos[b[i][j]]=P(i,j);
        }
    }
    //预处理数组,类似于确定每一行每个数的高度
    for(int i=2;i<=n;i++){
        for(int j=1;j<=m;j++){
            P p1=pos[a[i-1][j]],p2=pos[a[i][j]];
            if(p1.sd==p2.sd&&p1.ft+1==p2.ft){
                dp[i][j]=dp[i-1][j]+1;
            }
            else dp[i][j]=1;
        }
    }
    while(!s.empty())s.pop();
    for(int i=1;i<=n;i++){
        for(int j=2;j<=m+1;j++){
            if(judge(i,j)){
                int x=j;
                while(x<=m&&judge(i,x))x++;//确定合法区间
                solve(j-1,x-1,dp[i]);//跑单调栈
                j=x-1;
            }
            else solve(j-1,j-1,dp[i]);//不合法只跑一个数
        }
    }
    cout<<ans<<'\n';
    return 0;
}
6.矩阵

题目来源:2020 计蒜之道 线上决赛 E

题意:有一个 n × n n×n n×n 的 01 矩阵,求有多少个全 1 子矩阵,满足其一边长为另一边长的倍数。

解析:预处理的方式和第三题一样,先确定每一列每个数能向上延申的最大距离,然后对每行跑单调栈,确定每个全1子矩阵的一边长为另一边长的倍数满足递推式(详见代码),可以预处理。

#include <bits/stdc++.h>
#define ft first
#define sd second
using namespace std;
typedef long long ll;
typedef pair<int,int> P;
const int maxn=1010;
const ll mod=998244353;
int n;
char a[maxn][maxn];
int st[maxn],top=0;//严格单调递增栈
int b[maxn][maxn];
//任一全1矩阵包含右下端点的一边长为另一边长倍数的子矩阵个数
ll sum[maxn][maxn];
int l[maxn];//向左延申距离
void init(){
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            sum[i][j]+=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+(i%j==0||j%i==0);
        }
    }
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>(a[i]+1);
        for(int j=1;j<=n;j++){
            b[i][j]=(a[i][j]=='1')?b[i-1][j]+1:0;
        }
    }
    init();
    ll ans=0;
    for(int i=1;i<=n;i++){
        top=0;
        for(int j=1;j<=n;j++)l[j]=0;
        for(int j=1;j<=n;j++){
            while(top&&b[i][st[top]]>=b[i][j])top--;
            st[++top]=j;
            l[j]=j-st[top-1];
            if(!b[i][j])continue;
            int ss=0;
            for(int k=top;k>=1;k--){
            	int h=b[i][st[k]],len=ss+l[st[k]];
                ans+=sum[h][len];
                ans-=sum[h][ss];//多余的贡献要减去
                ss=len;
            }
        }
    }
    cout<<ans<<endl;
    return 0;
}
/*
4
1111
1001
1001
1111
->36

3
100
010
001
->3

*/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值