题目:
如图所示,在一条水平线上有n个宽为1的矩形,求包含于这些矩形的最大子矩形面积(图中的阴影部分的面积即所求答案)。
输入格式:
有多组测试数据,每组数据占一行。输入零时读入结束。
每行开头为一个数字n(1<=n<=100000),接下来在同一行给出n个数字h1h2…hn(0<=hi<=1000000000)表示每个矩形的高度。
输出格式:
对于每组数据,输出最大子矩阵面积,一组数据输出一行。
做法一:我们预处理出每一个矩形可以最多向左右延伸的终点
l,r数组分别代表第i个矩形最多可以向左右扩展的矩形位置,也就是找到第一个比它矮的矩形,核心代码就是那个while循环。
预处理出来这两个数组之后我们就可以总结答案。
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
long long read(){
char s;
long long f=1,x=0;
s=getchar();
while(s<'0'||s>'9'){
if(s=='-')f=-1;
s=getchar();
}
while(s>='0'&&s<='9'){
x*=10;
x+=s-'0';
s=getchar();
}
return x*f;
}
int n;
long long l[1000010],r[1000010];
long long h[1000010];
int main(){
while(1){
n=read();
if(n==0)return 0;
memset(h,0,sizeof(h));
memset(l,0,sizeof(l));
memset(r,0,sizeof(r));
for(int i=1;i<=n;i++){
h[i]=read();
}
for(int i=1;i<=n;i++){
l[i]=i;
while(l[i]-1>=0&&h[l[i]-1]>=h[i]){
l[i]=l[l[i]-1];
}
}
for(int i=n;i>0;i--){
r[i]=i;
while(r[i]+1<=n&&h[r[i]+1]>=h[i]){
r[i]=r[r[i]+1];
}
}
long long ans=0;
for(int i=1;i<=n;i++){
ans=max(ans,h[i]*(r[i]-l[i]+1));
}
cout<<ans<<endl;
}
}
做法二:我们可以用单调栈来更快地找到法一中的左右延伸的终点
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
long long read(){
char s;
long long f=1,x=0;
s=getchar();
while(s<'0'||s>'9'){
if(s=='-')f=-1;
s=getchar();
}
while(s>='0'&&s<='9'){
x*=10;
x+=s-'0';
s=getchar();
}
return x*f;
}
long long n;
long long ans=0;
long long st[1000010];
long long h[1000010];
long long w[1000010];
int main(){
while(1){
n=read();
if(n==0)return 0;
memset(h,0,sizeof(h));
int top=-1;
ans=0;
for(int i=1;i<=n;i++){
h[i]=read();
}
h[n+1]=0;
for(int i=1;i<=n+1;i++){
if(top==-1||h[st[top]]<=h[i]){
st[++top]=i;
w[st[top]]=1;
}
else{
long long wide=0;
while(top>=0&&h[st[top]]>h[i]){
wide+=w[st[top]];
ans=max(ans,wide*h[st[top]]);
top--;
}
st[++top]=i;
w[st[top]]=wide+1;
}
}
cout<<ans<<endl;
}
}
做法三:笛卡尔树
1.介绍一下笛卡尔树
笛卡尔树的每一个节点都有两个值,val与key。val是原数组的值,key是下标
笛卡尔树有两个特点:1.任意一颗子树的中序遍历结果表示数组的一段区间
2.笛卡尔树的val值满足大(小)根堆的性质
首先关于建树:
我们用一个stack来记录最右端的一条链中的节点。
这个栈是一个单调栈,我们从左向右遍历每一个节点。
有以下几条规则:1.由于第一个特点(任意一颗子树的中序遍历结果表示数组的一段区间),我们每新加入一个节点它目前只能有左儿子,且只能作为右儿子或者根节点。
具体步骤如下:1.在栈中找到第一个val值小于(这道题维护的是小根堆)的节点,把该节点以下的节点均踢出栈。2.这个新加节点就是它的右儿子,如果有被踢出的节点,则最后一个被踢出的就是新加节点的左二子。由于STL中的栈很难记录被踢出的上一个元素,所以我们用一个数组来维护一个栈。3.将这个新节点入栈
核心代码:
void build(){
memset(st,0,sizeof(st));
int top=-1;
int k;
for(int i=1;i<=n;i++){
k=top;
while(k>=0&&t[st[k]].val>=t[i].val)k--;
if(k!=-1){//如果还有元素,也就是它有父亲节点
t[st[k]].rs=i;
t[i].fa=st[k];
}
if(k<top){//如果有儿子
t[i].ls=st[k+1];
t[st[k+1]].fa=i;
}
st[++k]=i;//入栈
top=k;
}
root=st[0];
}
以下为完整代码:
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
long long read(){
char s;
long long x=0,f=1;
s=getchar();
while(s<'0'||s>'9'){
if(s=='-')f=-1;
s=getchar();
}
while(s>='0'&&s<='9'){
x*=10;
x+=s-'0';
s=getchar();
}
return x*f;
}
int n;
struct tree{
int key;
long long val;
int ls,rs,fa;
long long size;
}t[500050];
long long st[500050];
int root=0;
void build(){
memset(st,0,sizeof(st));
int top=-1;
int k;
for(int i=1;i<=n;i++){
k=top;
while(k>=0&&t[st[k]].val>=t[i].val)k--;
if(k!=-1){
t[st[k]].rs=i;
t[i].fa=st[k];
}
if(k<top){
t[i].ls=st[k+1];
t[st[k+1]].fa=i;
}
st[++k]=i;
top=k;
}
root=st[0];
}
long long ans=-1;
void dfs(int rt){
if(t[rt].ls!=-1){
dfs(t[rt].ls);
t[rt].size+=t[t[rt].ls].size;
}
if(t[rt].rs!=-1){
dfs(t[rt].rs);
t[rt].size+=t[t[rt].rs].size;
}
ans=max(ans,t[rt].size*t[rt].val);
return;
}
int main(){
while(1){
n=read();
if(n==0)return 0;
ans=-1;
for(int i=1;i<=n;i++){
t[i].val=read();
t[i].key=i;
t[i].ls=t[i].rs=-1;
t[i].size=1;
t[i].fa=-1;
}
build();
dfs(root);
cout<<ans<<endl;
}
}