最大矩形——单调栈
给一个直方图,求直方图中的最大矩形的面积。例如,下面这个图片中直方图的高度从左到右分别是2, 1, 4, 5, 1, 3, 3, 他们的宽都是1,其中最大的矩形是阴影部分。
输入包含多组数据。每组数据用一个整数n来表示直方图中小矩形的个数,你可以假定1 <= n <= 100000.
然后接下来n个整数h1, …, hn, 满足 0 <= hi <= 1000000000. 这些数字表示直方图中从左到右每个小矩形的高度,每个小矩形的宽度为1。 测试数据以0结尾。对于每组测试数据输出一行一个整数表示答案。
思路:
- 基本思路是计算每个高度其左右延伸的长度,从而最后得到所有的矩形的面积,从中挑出较大者即可。
- 计算向右延伸的长度:考虑使用递减的单调栈——即从栈顶到栈底单调递减。
- 枚举高度
- 判断栈是否为空:如果为空直接入栈,否则判断是否大于栈顶元素,若不小于,则入栈,否则栈中元素全部出栈,对于出栈元素,其向右延伸的长度即为当前高度的下标。(约定左端点为0,右端点为n)
- 同理,使用数组记录向左延伸的长度。
- 遍历三个数组即可得到所有的面积。
- 对于栈中元素,我们只知道其具体的值–高度,而不知道其下标,所以考虑使用结构体数组,在输入的同时记录其下标。
- 注意,我们约定最左边边界值为0,最右边边界值为n。
实现:
#include<iostream>
using namespace std;
int n;
struct hei
{
long long height;
int index;
hei(){};
bool operator=(hei & x)
{
this->height=x.height;
this->index=x.index;
}
};
hei h[100010];
hei st1[100010];
int top1=0,top2=0;
hei st2[100010];
long long r[100010],l[100010];
long long m;
int main()
{
while(scanf("%d",&n)!=EOF)
{
if(n==0)break;
for(int i=0;i<n;++i)
{
scanf("%llu",&h[i].height);
h[i].index=i;
l[i]=0;
r[i]=n;
}
top1=top2=0;
for(int i=0;i<n;++i)
{
while(top1!=0 && st1[top1].height>h[i].height)
r[st1[top1--].index]=i;
st1[++top1]=h[i];
}
for(int i=n-1;i>=0;i--)
{
while(top2!=0 && st2[top2].height>h[i].height)
l[st2[top2--].index]=i+1;
st2[++top2]=h[i];
}
m=0;
for(int i=0;i<n;++i)
m=max(m,h[i].height*(r[i]-l[i]));
cout<<m<<endl;
}
return 0;
}
反思:
- 对于数据范围不够敏感,刚开始直接使用
int
,但是自己测试大数据时出错才意识到数据范围不够,改成long long
。 - 对于每次循环没有将
top1 top2
重置为0,虽然自己测试了一些数据都通过了,但是很显然如果不清空栈,后面的数据会被之前的数据干扰。修改之后AC。
TT’s Magic Cat
Thanks to everyone’s help last week, TT finally got a cute cat. But what TT didn’t expect is that this is a magic cat.One day, the magic cat decided to investigate TT’s ability by giving a problem to him. That is select ncities from the world map, and a[i] represents the asset value owned by the i-th city. Then the magic cat will perform several operations. Each turn is to choose the city in the interval [l,r] and increase their asset value by c. And finally, it is required to give the asset value of each city after q operations. Could you help TT find the answer?
The first line contains two integers n,q(1≤n,q≤2⋅105) — the number of cities and operations.The second line contains elements of the sequence a: integer numbers a1,a2,…,an (−106≤a* i≤106).Then q lines follow, each line represents an operation. The i-th line contains three integers l,r and c(1≤l≤r≤n,−105≤c≤105) for the i-th operation.
Print n integers a1,a2,…,a**n one per line, and a**i should be equal to the final asset value of the i-th city.
思路:
- 比较明显的前缀和与差分的问题。因为需要对某段区间的数进行加减,我们通过计算差分从而转换为对区间边界值进行改变达到目的,最后只需要根据差分复原数组即可。
- 注意,如果我们令a[0]=0,那么可以减少判断:直接使用
b[i]=a[i]-a[i-1],i>=1;
而不用判断是否为第一个数。
实现:
#include<iostream>
using namespace std;
int n,q,l,r;
long long c;
long long a[200010],b[200010];
int main()
{
scanf("%d %d",&n,&q);
a[0]=0;
for(int i=1;i<=n;++i)
{
scanf("%lld",&a[i]);
b[i]=a[i]-a[i-1];
}
for(int i=1;i<=q;++i)
{
scanf("%d %d %lld",&l,&r,&c);
b[l]+=c;
b[r+1]-=c;
}
for(int i=1;i<=n;++i)
{
a[i]=a[i-1]+b[i];
cout<<a[i]<<" ";
}
return 0;
}
反思:
- 还是因为数据范围的问题导致wa——只看到了10^5,但是没有考虑到加上c再求和。
平衡字符串
一个长度为 n 的字符串 s,其中仅包含 ‘Q’, ‘W’, ‘E’, ‘R’ 四种字符。如果四种字符在字符串中出现次数均为 n/4,则其为一个平衡字符串。现可以将 s 中连续的一段子串替换成相同长度的只包含那四个字符的任意字符串,使其变为一个平衡字符串,问替换子串的最小长度?如果 s 已经平衡则输出0。
一行字符表示给定的字符串s
一个整数表示答案
思路:
- 使用尺取法。对于区间[L,R],我们关键是判断其能否满足要求。对此,遍历字符串的其余部分,计算各字符数目,记为sum1 sum2 sum3 sum4。记total = R-L+1。即total为子串中最多能替换的字符数目。从而一方面该数目必须不小于使sum1=sum2=sum3=sum4的最小值(考虑到不能通过替换使得字符数目减少,因此至少使得各字符数目等于max才有可能平衡),并且必须是4的倍数(否则将会有多余的字符)。
- 注意:刚开始比较暴力,在计算了所有字符的数目之后,对于区间[l,r]之外的部分进行遍历、计算字符数目。结果
TL
。效率更高的做法是直接根据所有字符的数目、区间内的字符数目,计算出区间之外字符的数目。因为显然区间内的字符数目一般比较少。 - 经过以上改进,结果还是
TL
。应该是因为使用了map
从而直接有map['Q']
,不用对输入字符进行手动的判断。改成if
判断之后AC。
实现:
#include<iostream>
#include<string>
using namespace std;
int ans=100000;
string s;
int main()
{
cin>>s;
bool flag=true;
int left=0,right=0;
int Q,W,E,R;
Q=W=E=R=0;
for(int i=0;i<s.size();++i)
{
if(s[i]=='Q')Q++;
if(s[i]=='E')E++;
if(s[i]=='W')W++;
if(s[i]=='R')R++;
}
if(Q==E && W==R && Q==W)
{
cout<<0;
return 0;
}
while(right < s.size())
{
int q,w,e,r;
q=w=e=r=0;
//根据总和以及l,r之间的数计算余下的数目
for(int i=left;i<=right;++i)
{
if(s[i]=='Q')q++;
if(s[i]=='E')e++;
if(s[i]=='W')w++;
if(s[i]=='R')r++;
}
q=Q-q;
e=E-e;
w=W-w;
r=R-r;
int total = right-left+1;
int max_num=max(q,e);
max_num=max(max_num,w);
max_num=max(max_num,r);
int free=total-(max_num-q+max_num-e+max_num-r+max_num-w);
if(free>=0 && (free%4==0))
{
ans=min(ans,total);
left++;
}
else right++;
}
cout<<ans;
return 0;
}
滑动窗口
ZJM 有一个长度为 n 的数列和一个大小为 k 的窗口, 窗口可以在数列上来回移动. 现在 ZJM 想知道在窗口从左往右滑的时候,每次窗口内数的最大值和最小值分别是多少.例如:数列是 [1 3 -1 -3 5 3 6 7], 其中k 等于 3.
输入有两行。第一行两个整数n和k分别表示数列的长度和滑动窗口的大小,1<=k<=n<=1000000。第二行有n个整数表示ZJM的数列。
输出有两行。第一行输出滑动窗口在从左到右的每个位置时,滑动窗口中的最小值。第二行是最大值。
思路:
- 其实和单调栈比较类似,这里考虑到需要对一个区间进行维护操作,应该使用单调队列:假设需要寻找每个窗口内的最小值,那么我们维护一个单调递增的队列。对于当前元素,判断队列是否为空;如果不为空,判断队尾元素是否比当前元素小,如果小,那么可以直接入队,否则需要将元素抛出,直到可以入队。当元素不属于当前窗口时,应当抛出。
- 注意本题对窗口的限制:长度为K。因此我们需要判断元素是否属于当前窗口:当前元素在数组中的下标为i,(看做当前窗口的最后一个元素)那么数组中当前窗口前一个元素下标为i-k,因此如果队首元素下标等于i-k,那么直接抛出。
- 对于输出,考虑到每个窗口都需要输出一个元素,因此当i+1>=k(出现第一个完整的窗口及之后),输出队首元素即可。(输出最大元素同理)。
- STL中双端队列
deque
比较适合这里的操作。另外直接将元素在数组中的下标入队显然比“最大矩形”中的方法要简洁自然。
实现:
#include<iostream>
#include<stdio.h>
#include<queue>
using namespace std;
int a[1000100];
deque<int> _min,_max;
int main()
{
int n,k;
cin>>n>>k;
for(int i=0;i<n;++i)
scanf("%d",&a[i]);
for(int i=0;i<n;++i)
{
while(!_min.empty() && a[i]<a[_min.back()])_min.pop_back();
_min.push_back(i);
if(_min.front()==i-k)_min.pop_front();
if(i+1>=k)cout<<a[_min.front()]<<" ";
}
cout<<endl;
for(int i=0;i<n;++i)
{
while(!_max.empty() && a[i]>a[_max.back()])_max.pop_back();
_max.push_back(i);
if(_max.front()==i-k)_max.pop_front();
if(i+1>=k)cout<<a[_max.front()]<<" ";
}
return 0;
}
总结:
-
这周作业因为数据范围的问题wa了很多次,没有养成看数据范围的习惯。而且不仅仅需要看单个数据的范围,还需要考虑到对数据进行操作之后可能达到的新的范围。
-
这周题目主要还是例题,是对课上数据结构、方法的直接应用,思路上面没有很多阻碍,因此实现起来感觉并不复杂。需要考虑真正做题当然不会告诉你什么方法、数据结构。