A - 最大矩形
题目
给一个直方图,求直方图中的最大矩形的面积。例如,下面这个图片中直方图的高度从左到右分别是2, 1, 4, 5, 1, 3, 3, 他们的宽都是1,其中最大的矩形是阴影部分。
Input
输入包含多组数据。每组数据用一个整数n来表示直方图中小矩形的个数,你可以假定1 <= n <= 100000.
然后接下来n个整数h1, …, hn, 满足 0 <= hi <= 1000000000. 这些数字表示直方图中从左到右每个小矩形的高度,每个小矩形的宽度为1。 测试数据以0结尾。
Output
对于每组测试数据输出一行一个整数表示答案。
样例
Sample Input
7 2 1 4 5 1 3 3
4 1000 1000 1000 1000
0
Sample Output
8
4000
题意
直方图的不同区域有不同的高度,要求出所有构成的矩形里面积最大的那个。
普通方法
遍历一遍,将能组成的左右矩形的面积都求出来,然后记录面积最大值,得出最大面积。
这个方法很好理解,但是时间复杂度很高,题目给出1 <= n <= 100000的数据范围有可能会超时。
单调栈
• 如果确定了一个矩形的左端点为l,右端点为r,那么矩形的高怎么确定?
• 根据题意,高度不能超过l到r中的最小值
• 要是整个矩形最大,那么高度就确定为l到r中的最小值
• 同理,如果确定了矩形的高度,那么左端点一定是越靠左,右端点越靠右,这个矩形的面积才可能最大
• 左端点可以确定为往左数第一个小于此高度的点
• 右端点可以确定为往右数第一个小于此高度的点
• 两遍单调栈处理出以每个点为高时的左右端点
代码解释
单调递增栈
如果栈为空或者栈顶元素大于入栈元素,则入栈。
否则,入栈则会破坏栈内元素的单调性,则需要将不满足条件的栈顶元素全部弹出后,将入栈元素入栈。
从左到右遍历高度,一旦发现下一个元素 i+1 的高度比栈顶元素高度小,则对每一个要被弹出栈的元素,我们都记它们右边第一个小于其高度的点为 i+1
同理,从右往左遍历,记录左边第一个小于其高度的点。
在这里要注意一点,如果往左或者往右没有更小的高度了呢?因为做这些工作要求的量是矩形的宽,所以,我们可以当做数组边界再下一个点就是更小的高度,也就是左边初始值记为 -1,右边初始值记为 n。(写代码的时候数组命名的时候不小心搞反了,l和r有点混乱,不重要hhhhhh)
这一题要AC还有一个关键点,就是数据范围的问题
1 <= n <= 100000
0 <= hi <= 1000000000
显然这个矩形最大最大的情况就是两个上端相乘,爆int了鸭。。。所以一定要用long long,要高乘宽得面积,都要用long long,这是一个坑hhhh。
完整代码
#include <iostream>
#include <cstdio>
#include <stack>
#include <cmath>
using namespace std;
struct height
{
long long k, h;
bool operator > (const height x) const
{
return x.h < h;
}
bool operator < (const height x) const
{
return x.h > h;
}
};
int main()
{
long long n;
while(scanf("%lld", &n) != EOF)
{
if(n == 0) break;
height a[100100];
long long l[100100], r[100100];
for(int i=0;i<n;i++)
{
scanf("%lld", &a[i].h);
a[i].k = i;
l[i] = n; r[i] = -1;
}
stack<height> s;
for(int i=0;i<n;i++)
{
while(!s.empty() && s.top()>a[i])
{
height hh = s.top();
l[hh.k] = i;
s.pop();
}
s.push(a[i]);
}
stack<height> ss;
for(int i=n-1;i>=0;i--)
{
while(!ss.empty() && ss.top()>a[i])
{
height hh = ss.top();
r[hh.k] = i;
ss.pop();
}
ss.push(a[i]);
}
long long max = -1;
for(int i=0;i<n;i++)
{
//cout<<a[i].h<<' '<<l[i]<<' '<<r[i]<<endl;
long long x = l[i] - r[i] - 1;
long long ans = x*a[i].h;
if(ans > max) max = ans;
}
printf("%lld\n", max);
}
return 0;
}
总结
这一题是对单调栈的一个应用,弄清楚栈操作的临界条件就没什么问题了。然后就是看题目一定一定要自己,看清楚时间空间,看看要用什么数据类型才能hold住范围,避免在这种问题上出错。
B - TT’s Magic Cat
题目
原题英文,简单说下题意和数据范围
长度为 n 的数组,一共 q 次操作;
每次操作给出 L, R , c,表示区间 [L, R] 中各个数均加
上 c ;
求 q 次操作结束后,数组中各个元素值?
输入
输出
Print n integers a1,a2,…,an one per line, and ai should be equal to the final asset value of the i-th city.
样例
Input
4 2
-3 6 8 4
4 4 -2
3 3 1
Output
-3 6 9 2
Input
2 1
5 -2
1 2 4
Output
9 2
Input
1 2
0
1 1 -8
1 1 -6
Output
-14
题解思路
首先暴力做法不可取,q次对着 r-l+1长度的数进行操作,两次遍历时间复杂度太高。
这题可以用差分数组
• 差分构造方式
• 原数组 A,差分数组 B, 数组范围 [1, n]
• B[1] = A[1]
• B[i] = A[i] - A[i-1]
• 差分特点
• B 数组前缀和 ⇔ A 数组元素值
• SUM{B[1~i]} = A[i]
• A 数组的区间加 ⇔ B 数组的单点修改
• A[L]~A[R] 均加上 c 等价于 B[L] += c, B[R+1] -= c
这里凭着自己的理解稍微解释一下为什么是B[L] += c, B[R+1] -= c:
因为要使A[L]~A[R] 均加上 c,而A[i]等于B[1]一直累加到B[i];
当B[L] += c时,从L开始(包括L)往后的所有数都被加上了c;
当B[R+1] -= c时,累加到R+1时就减去了c,那么这里的-c和前面的+c抵消,R+1之后(包括R+1)的数就没有变化;
所以总体来看,就是[L,R]的数都加上了c。
然后再次注意一下数据范围,最多可能有200000次操作,而每次最多可以加100000,如果每次加这么多显然int是装不下了,所以,再次使用long long
代码很简单,就不解释了。。。跟思路完全一致。六次WA的惨痛经历是因为纯眼瞎qwq暴击。
完整代码
#include <iostream>
#include <algorithm>
#include <string.h>
using namespace std;
int n,q;
long long a[200100];
long long b[200100];
int main()
{
cin>>n>>q;
memset (a, 0, sizeof(a));
memset (b, 0, sizeof(b));
for(int i=1;i<=n;i++)
{
cin>>a[i];
if(i == 1) b[1] = a[1];
else b[i] = a[i] - a[i-1];
}
int l, r;
long long k;
for(int i=0;i<q;i++)
{
cin>>l>>r>>k;
b[l] += k;
if(r < n) b[r+1] -= k;
}
long long sum = 0;
for(int i=1;i<=n;i++)
{
sum += b[i];
cout<<sum;
if(i != n) cout<<' ';
}
return 0;
}
C - 平衡字符串
题目
一个长度为 n 的字符串 s,其中仅包含 ‘Q’, ‘W’, ‘E’, ‘R’ 四种字符。
如果四种字符在字符串中出现次数均为 n/4,则其为一个平衡字符串。
现可以将 s 中连续的一段子串替换成相同长度的只包含那四个字符的任意字符串,使其变为一个平衡字符串,问替换子串的最小长度?
如果 s 已经平衡则输出0。
Input
一行字符表示给定的字符串s
Output
一个整数表示答案
样例
Input
QWER
Output
0
Input
QQWE
Output
1
Input
QQQW
Output
2
Input
QQQQ
Output
3
题解
先回顾一下上课讲的尺取法
基本思想
• 维护双指针(下标)
• 尺取法,又称双指针法,即在遍历数组过程中,用两个相同方向进行扫描
使用条件
• 什么情况下能使用尺取法?
• 所求解答案为一个连续区间
• 区间左右端点移动有明确方向
再来看这个题目
• 所求解答案为一个连续区间 ✓
• 区间左右端点移动有明确方向 ✓
• 当前 [L, R] 满足要求,则 L++
• 当前 [L, R] 不满足要求,则 R++
因此可以用尺取法
那么如何判断当前 [L, R] 是否满足要求呢?是否这个区域内的字符换掉就能使字符串平衡呢?
• 用 sum1, sum2, sum3, sum4 分别记录不包含区间 [L, R]
这一段时,字符 ‘Q’, ‘W’, ‘E’, ‘R’ 的个数
• 先通过替换使 4 类字符数量一致,再判断剩余空闲位置是否
为 4 的倍数
• MAX = max(sum1, sum2, sum3, sum4)
• TOTAL = R – L + 1 • FREE = TOTAL -[(MAX-sum1)+(MAX-sum2)+(MAX-sum3)+(MAX-sum4)]
• 若 FREE ≥ 0 且为 4 的倍数,则满足要求;否则不满足
其实在这里根本不需要管怎么替换,主要是满足字符数量的问题。
先计算出区域之外的四个字母各自有多少个,然后找出数量最多的那个。因为要使每个字母数量都一样,所以要让其他三个字母数量向最大值靠齐,这时候就要从区域内减了。靠齐过后,除了剩下的区域内字母已经平衡,所以只要剩下的数量是4的倍数就可以达到平衡。
代码解释
主要的思路上面已经说得很清楚了,代码也只要照着写就行,很简单。
但是有一个地方还要解释一下,就是怎么计算4个sum。
因为尺子在移动改变的过程中,区域在变化,所以外面的字符个数也在变化。
当L++时,外面会多一个字母,那么对应的sum要++;
当R++时,外面会少一个字母,那么少的那个字母的sum要 - -。
完整代码
#include <iostream>
#include <cstring>
#include <string.h>
#include <cmath>
#include <map>
using namespace std;
string s;
int l=0, r=0;
int Min = 1000000;
int sum[4];
map<char, int> ss;
bool judge(int sum1[],int tt)
{
int Max = max(max(sum1[0],sum1[1]), max(sum1[2],sum1[3]));
int free = tt -(Max-sum1[0])-(Max-sum1[1])-(Max-sum1[2])-(Max-sum1[3]);
if(free>=0 && free%4==0)
return true;
else return false;
}
int main()
{
cin>>s;
int n;
n = s.size();
ss['Q'] = 0; ss['W'] = 1; ss['E'] = 2; ss['R'] = 3;
memset(sum, 0, 4);
for(int i=0;i<n;i++)
{
char c = s[i];
sum[ss[c]]++;
}
if(sum[0]==sum[1]&&sum[2]==sum[3]&&sum[0]==sum[2])
{
cout<<0<<endl;
return 0;
}
char c = s[0];
sum[ss[c]]--;
while(l<n && r<n)
{
int total = r-l+1;
int k = judge(sum, total);
if(k)
{
char c = s[l];
sum[ss[c]]++;
if(total<Min) Min = total;
l++;
}
else
{
r++;
char c = s[r];
sum[ss[c]]--;
}
}
cout<<Min<<endl;
return 0;
}
总结
题目不难,但是要稍微注意一样初始状态,和尺子移动时的一些变化。
D - 滑动窗口
题目
ZJM 有一个长度为 n 的数列和一个大小为 k 的窗口, 窗口可以在数列上来回移动.
现在 ZJM 想知道在窗口从左往右滑的时候,每次窗口内数的最大值和最小值分别是多少. 例如:
数列是 [1 3 -1 -3 5 3 6 7], 其中 k 等于 3.
Window position Minimum value Maximum value
[1 3 -1] -3 5 3 6 7 -1 3
1 [3 -1 -3] 5 3 6 7 -3 3
1 3 [-1 -3 5] 3 6 7 -3 5
1 3 -1 [-3 5 3] 6 7 -3 5
1 3 -1 -3 [5 3 6] 7 3 6
1 3 -1 -3 5 [3 6 7] 3 7
Input
输入有两行。第一行两个整数n和k分别表示数列的长度和滑动窗口的大小,1<=k<=n<=1000000。第二行有n个整数表示ZJM的数列。
Output
输出有两行。第一行输出滑动窗口在从左到右的每个位置时,滑动窗口中的最小值。第二行是最大值。
样例
Sample Input
8 3
1 3 -1 -3 5 3 6 7
Sample Output
-1 -3 -3 -3 3 3
3 3 5 5 6 7
题解
要求窗口里的最小值,就维护一个单调递增队列,返回队首最小值;
要求窗口里的最大值,就维护一个单调递减队列,返回队首最大值。
单调队列的原理跟第一题单调栈差不多,但是这里有一个问题就是窗口的移动。窗口向右移动时,右边就判断下一个元素就行了,但是需要判断左边队首元素是否还是属于窗口内。
为了方便判断队首元素是否在窗口内,我们在进行入队操作时选择数组下标为对象,对下标进行操作。这样可以判断 dq.front() < i-k+1,如果小于则说明他是上一个窗口里的,需要弹出来更新队首。
通过以上分析,发现不仅要对队尾进行插入和删除,还要对队首进行弹出操作,所以这里可以使用数组模拟队列,还可以使用 deque双向队列。
这一题刚交上去TLE了,没想明白为什么超时。。。有同学说G++不行要换C++,试过依然不行,然后把所有的cin和cout换成scanf printf后用C++就过了。
完整代码
#include <iostream>
#include <deque>
#include <cstdio>
using namespace std;
int n, k;
int a[1000100];
int mi[1000100];
int ma[1000100];
deque<int> dq;
deque<int> qq;
int main()
{
scanf("%d %d", &n, &k);
for(int i=0;i<n;i++)
{
scanf("%d", &a[i]);
}
for(int i=0;i<k;i++)
{
while(!dq.empty() && a[dq.back()] >= a[i])
dq.pop_back();
dq.push_back(i);
while(!qq.empty() && a[qq.back()] <= a[i])
qq.pop_back();
qq.push_back(i);
}
mi[0] = a[dq.front()];
ma[0] = a[qq.front()];
int cnt = 1;
for(int i=k;i<n;i++)
{
while(!dq.empty() && a[dq.back()] >= a[i])
dq.pop_back();
while(!dq.empty() && dq.front() < i-k+1)
dq.pop_front();
dq.push_back(i);
while(!qq.empty() && a[qq.back()] <= a[i])
qq.pop_back();
while(!qq.empty() && qq.front() < i-k+1)
qq.pop_front();
qq.push_back(i);
mi[cnt] = a[dq.front()];
ma[cnt] = a[qq.front()];
cnt++;
}
for(int i=0;i<cnt;i++)
{
if(i<cnt-1) printf("%d ",mi[i]);
else if(i == cnt-1) printf("%d\n",mi[i]);
}
for(int i=0;i<cnt;i++)
{
if(i<cnt-1) printf("%d ",ma[i]);
else if(i == cnt-1) printf("%d\n",ma[i]);
}
return 0;
}