A:最大矩形
题目:
给一个直方图,求直方图中的最大矩形的面积。例如,下面这个图片中直方图的高度从左到右分别是2, 1, 4, 5, 1, 3, 3, 他们的宽都是1,其中最大的矩形是阴影部分。
input:
输入包含多组数据。每组数据用一个整数n来表示直方图中小矩形的个数,你可以假定1 <= n <= 100000. 然后接下来n个整数h1, …, hn, 满足 0 <= hi <= 1000000000. 这些数字表示直方图中从左到右每个小矩形的高度,每个小矩形的宽度为1。 测试数据以0结尾。
output:
对于每组测试数据输出一行一个整数表示答案。
样例输入:
7 2 1 4 5 1 3 3
4 1000 1000 1000 1000
0
样例输出:
8
4000
思路:
求最大矩形,首先就应该是从当前柱向左或者向右进行拓展,而这个拓展的边界就是比当前柱低。如果直接暴力搜索的话,复杂度就是n2,显然不行,而造成这样暴力的原因就是很多元素被重复记录了。这样就可以用维护一个单调栈的方法减少重复记录;
单调栈的特性,(递增栈)从栈底到栈顶 ;
我们考虑标号为i 的矩形柱,从当前向右,只要比当前的矩形柱大都可以计算,比当前小的就会形成凹陷,不能计算。因此维护的是一个递增栈,是当前元素i被pop 的时候就说明这时候要加入的这个元素比当前元素小,也就是能够向右拓展的最大距离了。
向左拓展的距离类似,反向建立一个单增栈即可。
时间复杂度0(n);
总结:
这个需要注意的一点是数据范围: 0 <= hi <= 1000,000,000(10e9);用int 爆WA,需要用long long int;
这里记录一下数据类型对应的数据范围:
同时需要注意的是使用栈前,先进行对栈的清空操作,同理静态区的数组再下一次使用前也要清空。
目前为止对单调栈的初步印象是寻找比当前元素小或者大的元素时可以用到单调栈,能把当前元素pop 出去的肯定是找到了新的更大或更小元素。如有后续,继续补充。
记录一下单调栈的伪代码吧:
//递增栈
for(int i=0;i<n;i++)
{
if(s.size()&&a[s.top()]>a[i]) //栈不为空且栈顶元素大于当前比较元素
{
s.pop(); //取当前元素进行 相关操作
//......
}
s.push(i);
}
代码:
#include<stdio.h>
#include<stack>
#include<algorithm>
#include<string.h>
using namespace std;
int R[100010];
int L[100010];
stack<int> st;
void solve(long long int *a,int n) //n-1=0
{
while(!st.empty()) st.pop();
for(int i=1;i<=n-1;i++) //压入栈得都是下标 //n+1
{
while(st.size()&&a[i]<a[st.top()]) //当前元素值比栈顶小, 达到n
{
int index=st.top();
st.pop();
R[index]=i-index; //计算右拓展及自身的宽度
// printf("%d",R[index]);
}
//压栈 ,当前元素比栈顶元素值大、 栈为空
st.push(i);
}
while(!st.empty()) st.pop();
for(int i=n-2;i>=0;i--)
{
while(st.size()&&a[i]<a[st.top()])
{
int index=st.top();
st.pop();
L[index]=index-i-1;
}
st.push(i);
}
}
int main()
{
int n;
scanf("%d",&n);
while(n!=0)
{
long long int *a = new long long int [n+2];
for(int i=1;i<=n;i++)
{
scanf("%lld",&a[i]);
}
a[0]=0;
a[n+1]=0;
solve(a,n+2);
/*for(int i=1;i<=n;i++){
printf("%d %d\n",R[i],L[i]);
}*/
long long int ans=0;
for(int i=1;i<=n;i++)
{
ans=max(ans,a[i]*(R[i]+L[i]));
}
printf("%lld\n",ans);
memset(R,0,sizeof(R));
memset(L,0,sizeof(L));
scanf("%d",&n);
}
return 0;
}
B:TT’s Magic Cat
题目:
给定一个数组,在其(l,r)区间内的每个值都增加k,求q轮后的数组。
input:
第一行包含两个整数n,q(1≤n,q≤2⋅105)分别表示数组的长度和操作的轮数
第二行包含序列a的元素:整数a1、a2、…、an(-106≤ai≤106)。
接下来是q行,每一行代表一个操作。第i行包含用于第i操作的三个整数l、r和c(1≤l≤r≤n,-105≤c≤105)。
output:
打印出变化后的数组
样例输入:
4 2
-3 6 8 4
4 4 -2
3 3 1
样例输出:
-3 6 9 2
思路:
如果直接暴力求解,复杂度就是0(n2),显然不行,我们可以用前缀和和差分来实现这道题。
在这里简短的证明一下差分和前缀和在这道题里使用的正确性。
//对于原数组a[i] (1<=i<=n),求差分得sub 数组
sub[1] = a[1] i=1
sub[i] = a[i] - a[i-1] 1<=i<=n
//进行(l,r,k)操作时,
sub[l]+=k;
sub[r+1]-=k;
//再次求前缀和时:
sum[i] = sub[i]; i=1;
sum[i]= sum[i-1] + sub[i] ; 1<=i<=n
//(假设展开sum[3],sum[3] = sum[2] +sub[3] =sum[1] +sub[2] + sub[3]=sub[1]+sub[2]+sub[3] =a[1] +a[2] -a[1] +a[3]-a[2] =a[3]);
而当sub[l] +k 是,之后得每个元素都会加上k ,因此在应该加的区间结束时,即sub[r+1]-k,之后的区间又恢复正常。
时间复杂度是O(n)
代码:
#include<stdio.h>
using namespace std;
int main()
{
int n,q,l,r,k;
scanf("%d%d",&n,&q);
long long int *a = new long long int [n+1];
long long int *sub = new long long int [n+1];
long long int *sum = new long long int [n+1];
for(int i=0;i<n;i++) scanf("%lld",&a[i]);
sub[0]=a[0];
for(int i=1;i<n;i++) sub[i]=a[i]-a[i-1];
while(q--){
scanf("%d%d%d",&l,&r,&k);
sub[l-1]+=k;
sub[r]-=k;
}
sum[0]=sub[0];
for(int i=1;i<n;i++){
sum[i]=sum[i-1]+sub[i];
}
for(int i=0;i<n;i++){
printf("%lld ",sum[i]);
}
return 0;
}
C:平衡字符串
题目:
一个长度为 n 的字符串 s,其中仅包含 ‘Q’, ‘W’, ‘E’, ‘R’ 四种字符。
如果四种字符在字符串中出现次数均为 n/4,则其为一个平衡字符串。
现可以将 s 中连续的一段子串替换成相同长度的只包含那四个字符的任意字符串,使其变为一个平衡字符串,问替换子串的最小长度?
如果 s 已经平衡则输出0。
input:
一行字符表示给定的字符串s
output:
一个整数表示答案
样例输入:
QWER
样例输出:
0
样例输入:
QQWE
样例输出:
1
样例输入:
QQQE
样例输出:
2
样例输入:
QQQQ
样例输出:
3
思路:
这里的问题是寻找符合条件的最小区间,这种区间问题先分析一下尺取法可不可行。
这个题所求区间满足以下特点:
(1)区间连续
(2)如果满足条件则l++,看更小的区间是否能满足条件
(3)如果区间不满足条件则r++,拓展更长的区间
因此可以用尺取法计算,现在的问题就是这样判定这个区间满不满足。
对于整个数组n,在(l,r)区间内,是判断对于(l,r)区间外的元素,能否通过区间内的元素补齐区间外,使区间外平衡的同时,区间内的剩余点同样是4的倍数。
判断标准有下一步就是怎么使用这个标准。分析可能产生的两种情况,l++和r++。当l++,则是把位置为l的元素放在区间外;当r++,则是把原来r+1位置的元素放进了区间内。
据此顺序写出代码。
时间复杂度 0(n)
总结:
这个题还是应该掌握尺取法的一般特性和使用条件,简而言之就是双指针锁定区间,之后的就是各种判断。
同时这里记录map 的一个使用。这样讲QWER 字符转换成数字,在后面编程的时候也会更加便利,比如sum[mp[c]]--;
,这样表示c 所代表字符数量的时候也更加容易。
map<char,int> mp;
mp['Q']=0;
mp['W']=1;
mp['E']=2;
mp['R']=3;
填平操作:假设区间外的字符已经使所需要的字符按照最大值进行填平,并计算(l,r)区间内是否用空余。
int Max=max(max(sum[0],sum[1]),max(sum[2],sum[3]));
int free= total-((Max-sum[0])+(Max-sum[1])+(Max-sum[2])+(Max-sum[3]));
再记录一下尺取法的一般框架
for(int i=0;i<n;i++)
{
//当前区间是(l,r)
if(check(l,r)){
l++;
//可能会有更新操作
}
else {
r++;
//可能会有更新操作
}
代码:
// 平衡字符串
#include<stdio.h>
#include<map>
#include<string.h>
#include<iostream>
using namespace std;
map<char,int> mp;
int sum[4];
int main()
{
int ans=1000000;
mp['Q']=0;
mp['W']=1;
mp['E']=2;
mp['R']=3;
string s;
cin>>s;
int n=s.length();
memset(sum,0,sizeof(sum));
for(int i=0;i<n;i++)
{
if(s[i]=='Q') sum[0]++; //
if(s[i]=='W') sum[1]++;
if(s[i]=='E') sum[2]++;
if(s[i]=='R') sum[3]++;
}
if(sum[0]==sum[1]&&sum[1]==sum[2]&&sum[2]==sum[3]){
printf("0\n");
return 0;
}
char c=s[0];
sum[mp[c]]--;
int l=0,r=0;
while(l<n&&r<n){
int total=r-l+1;
bool check;
int Max=max(max(sum[0],sum[1]),max(sum[2],sum[3]));
int free= total-((Max-sum[0])+(Max-sum[1])+(Max-sum[2])+(Max-sum[3]));
if(free>=0&&free%4==0) check=true;
else check=false;
if(check){ //满足条件的话,l++
c=s[l];
sum[mp[c]]++;
if(total<ans) ans=total;
l++;
}
else{
r++;
c=s[r];
sum[mp[c]]--;
}
}
printf("%d",ans);
return 0;
}
D:滑动窗口
题目:
ZJM 有一个长度为 n 的数列和一个大小为 k 的窗口, 窗口可以在数列上来回移动. 现在 ZJM 想知道在窗口从左往右滑的时候,每次窗口内数的最大值和最小值分别是多少. 例如:
数列是 [1 3 -1 -3 5 3 6 7], 其中 k 等于 3.
input:
输入有两行。第一行两个整数n和k分别表示数列的长度和滑动窗口的大小,1<=k<=n<=1000000。第二行有n个整数表示ZJM的数列。
output:
输出有两行。第一行输出滑动窗口在从左到右的每个位置时,滑动窗口中的最小值。第二行是最大值。
样例输入:
8 3
1 3 -1 -3 5 3 6 7
样例输出:
-1 -3 -3 -3 3 3
3 3 5 5 6 7
思路:
这个相当于是找局部的最大值和最小值,不能够使用单调栈,因为单调栈只能从一端pop元素,因此考虑维持一个单调的双端队列。
(1)首先维护双端队列的单调性,(假设从队头到队尾递减)当前元素若比队尾元素值小,则pop 元素使得满足条件。
(2)同时要维护窗口内的元素个数,如果队首的元素已经在窗口外,就把他pop出去。
(3)每一次循环的队首元素就是当前窗口的最大值或者是最小值。
这里需要注意的是,在n 个元素的数组中,假如窗口是k ,那么窗口中的最值有n-k +1个,而在for 循环的前k-1次的队首元素对于最值是没有价值的,当第k次产生的时候,第一个窗口完成,之后for 循环中的队首值才是有价值的因此在输出的时候应该从k 开始输出。
时间复杂度0(n);
总结:
单调队列与单调栈的区别:
(1)单调栈只维护一端(栈顶),而单调队列可以维护两端(队首和队尾)
(2)单调栈通常维护全局的单调性,而单调队列通常维护局部的单调性
(3)单调栈大小没有上限,而单调队列通常有大小限制
代码:
//滑动窗口
#include<stdio.h >
#include<iostream>
#include<deque>
#include<string.h>
using namespace std;
int Max[1000100];
int Min[1000100];
int a[1000100];
void solve(int *a,int n,int k)
{
deque<int> q;
q.clear();
for(int i=0;i<n;i++){
while(q.size()&&a[i]>=a[q.back()]) //维护单调性 //单减
q.pop_back();
q.push_back(i);
while(q.size()&&q.front()<=i-k){ ///维护窗口内元素个数
q.pop_front();
}
Max[i]=q.front();
}
q.clear();
for(int i=0;i<n;i++){
while(q.size()&&a[i]<=a[q.back()])
q.pop_back();
q.push_back(i);
while(q.size()&&q.front()<=i-k)
q.pop_front();
Min[i]=q.front();
}
q.clear();
}
int main(){
ios::sync_with_stdio(false);
int n,k;
cin>>n>>k;
for(int i=0;i<n;i++){
cin>>a[i];
}
solve(a,n,k);
for(int i=k-1;i<n;i++){
cout<<a[Min[i]]<<' ';
}
cout<<endl;
for(int i=k-1;i<n;i++){
cout<<a[Max[i]]<<' ';
}
memset(Max,0,sizeof(Max));
memset(Min,0,sizeof(Min));
memset(a,0,sizeof(a));
return 0;
}