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
思路
毫无疑问,这道题目暴力做不出来,但我们还是要暴力下,以贯彻落实程序设计的优化思想。
最终解决方案是单调栈,为啥叫单调栈呢,因为里面的元素只能按照递增或递减的顺序排序。
排序之后可以通过其单调性快速获取关于高度的信息。
步骤如下:
- 取高度数组h,保存各位置单调信息;单调栈s,保存遍历过的元素的索引。
- 对于每个元素,若此元素大于s栈顶索引指向的元素
收获
细节
题目有数据长度的提示,应该用LL,我用了LL还是报AV。
最后发现是 _for
宏定义和 stack<int>
里的int还没改。
这次只有这一点没注意到,进步了,努力。
对于题目思路
首先,这是一道基本的单调栈题目(虽然我一开始没写出来)
其次,这是一道烂大街的面试题(虽然我一开始没写出来)
再次,这是一道很好地诠释“逐步优化”思想的题目(诶就是这么麻烦你说气不气)
怎么生动理解单调栈呢?
-
“拯救”理解
我们要取最大矩形,肯定想让这个矩形尽可能“高”且“宽”。所以我们不断将递增的序列存入单调栈;忽然遍历到了一个更矮的,这高度就被他砍下来了,这块矮矩形把高度压下来,右边的高矩形对左侧没用了,于是我们见好就收,赶快计算递减前最高矩形的面积,好为这个倒行逆施的家伙腾出递增空间。
保存好这家伙后,之前保存的比他高的元素都不能用了,因为他们向右伸展的空间被挡住了。
如此,我们反复“拯救”当前最高矩形,一直拯救到结束,这样就可以获得所有矩形中最大的面积了。 -
“优化”理解
这个题最简单的做法是对每个高度都找一下他左右能延伸的最远距离。在单调栈中,因为所有元素递增,左侧没有比自己高的元素,所以每个元素都是自己的左边界。当遇到递减元素,这个元素就是栈顶元素的有边界+1正因单调栈的特性,我们这个题可以只遍历一遍就出结果。
B - 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 n cities 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?
a[i]代表第i个城市拥有的资源。每次从a[l,r]均加c,最终,需要给出q次操作后城市的值。
Input
The first line contains two integers n,q (1≤n,q≤2105) — the number of cities and operations.
The second line contains elements of the sequence a: integer numbers a1,a2,…,an (−106≤ai≤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.
Output
Print n integers a1,a2,…,an one per line, and ai should be equal to the final asset value of the i-th city.
Examples
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
思路
最简单的是暴力算,这样O(q*n),复杂度高。
我们通过差分数组来降低复杂度,将对[l,r]上所有元素的操作简化为对两头元素的操作。
收获
刚看到这个题的时候我傻了,这不是IO级别的题目吗 ?然后马上反应过来,原来要用上刚学的知识降低复杂度啊。
查分数组真的非常巧妙,人们为了提高程序效率果然可以无所不用其极。
#include<bits/stdc++.h>
#include<stack>
#include<cstdio>
using namespace std;
#define max(a,b) (a<b?b:a);
#define _for(i,s,e) for(LL i=s;i<e;i++)
#define LL long long
//大数组开到全局变量
LL a[200001],cf[200001];
int main() {
LL n,q;
scanf("%lld %lld",&n,&q);
//为了适应前缀和,我们将a数组0位留空
_for(i,1,n+1)scanf("%lld",a+i);
cf[1]=a[1];
//计算前缀和
_for(i,2,n+1)cf[i]=a[i]-a[i-1];
//使用前缀和进行[左加右减)的计算。
//注意减的话,要在本区间之后的那个元素上减
//还要注意末尾元素访问越界,但是因为我们数组开得大,所以无所谓
_for(i,0,q) {
LL l,r,c;
scanf("%lld %lld %lld",&l,&r,&c);
cf[l]+=c;
cf[r+1]-=c;
}
//计算完成,接下来累加输出即可
LL sum=0;
_for(i,1,n+1) {
sum+=cf[i];
printf("%lld ",sum);
}
printf("\n");
}
C - 平衡字符串 Leetcode 1234
一个长度为 n 的字符串 s,其中仅包含 ‘Q’, ‘W’, ‘E’, ‘R’ 四种字符。
如果四种字符在字符串中出现次数均为 n/4,则其为一个平衡字符串。
现可以将 s 中连续的一段子串替换成相同长度的只包含那四个字符的任意字符串,使其变为一个平衡字符串,问替换子串的最小长度?
如果 s 已经平衡则输出0。
Input
一行字符表示给定的字符串s
Output
一个整数表示答案
Examples
Input
QWER
Output
0
Input
QQWE
Output
1
Input
QQQW
Output
2
Input
QQQQ
Output
3
Note
1<=n<=10^5
n是4的倍数
字符串中仅包含字符 ‘Q’, ‘W’, ‘E’ 和 ‘R’.
思路
本题重点有两个:
- 双指针(尺取)获的最短连续子字符串。
- 怎样判断字符串划分是否合理。
接下来我们从头开始分析:最基本的想法是遍历所有子字符串,对每个子字符串进行判断,在所有能达到平衡条件的字符串中选最短的。
这里很容易想到嵌套循环,但那样复杂度太高,怎么办呢?其实所有需要降低复杂度的地方感觉就一个词:“减枝”,比如动态规划是对重复计算项剪枝
。这里如何剪?我们必须遍历所有子串,那能做的就是降低判断每个子串合规所花费的时间,这样的话,如果我们逐步的左右移动子串边界,就可以通过很少的操作来计算每一个新串是否合规了。这就是尺取法。
如何判断合规?如果有类字符在子串外出现len/4以上次,那子串内无论怎么变也不能让外面的字符减少。这是唯一一种不合规的情况,其他任何情况都合规。所以只要判断子串外字符是否都小于等于len/4就可以了。
心得
这道题比上一道题复杂一些,总体来说还行。
看了网上一些题解,同学们的和其他人的都看了,感觉同质化挺严重,那么多篇文章都没提到“符合条件”中的条件到底是什么。
#include<bits/stdc++.h>
#include<stack>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<string.h>
#include<algorithm>
using namespace std;
#define max(a,b) (a<b?b:a)
#define _for(i,s,e) for(LL i=s;i<e;i++)
#define LL long long
//如果区间外每种字符的出现次数都小于等于 n/4,则这个区间是合法的。
bool check(vector<int> &sum,vector<int> &inNum, int& target) {
for(int i=0; i<4; i++) {
if(sum[i]-inNum[i]>target)
return false;
}
return true;
}
char s[100010];
int main() {
scanf("%s",s);
int n = strlen(s);
int ans = INT_MAX;
int k=n/4;
unordered_map<char,int>mp;
//进行map映射
mp['Q']=0,mp['W']=1,mp['E']=2,mp['R']=3;
vector<int> sum(4);
//统计所有字符出现次数
for(int i=0; i<n; i++) sum[mp[s[i]]]++;
//如果符合条件,直接返回
if(sum[0]==sum[1] && sum[0]==sum[2] && sum[0]==sum[3]){
cout<<0<<endl;
return 0;
}
//并不需要在区间内花心思判断能不能使得整个字符串成为平衡字符串
vector<int>inNum(4);//记录区间内的数量
for(int i=0,j=0; i<n; i++) {
inNum[mp[s[i]]]++;
while(j<=i && check(sum,inNum,k)) {
ans = min(ans,i - j + 1);
inNum[mp[s[j]]]--;
j++;
}
}
printf("%d\n",ans);
return 0;
}
D - 滑动窗口(C++和G++都交一下)
ZJM 有一个长度为 n 的数列和一个大小为 k 的窗口, 窗口可以在数列上来回移动. 现在 ZJM 想知道在窗口从左往右滑的时候,每次窗口内数的最大值和最小值分别是多少. 例如:
数列是 [1 3 -1 -3 5 3 6 7], 其中 k 等于 3.
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
思路
这道题使用单调队列,和单调栈挺相似,都是要保持整个结构的有序性,只不过这里单调队列还要额外地时刻保持内部元素都是窗口内的元素。
- 不断入队列,保持单调性
- 当元素离开窗口,则将该元素弹出
心得
- 这道题我记得剑指offer上有类似的,不过忘记怎么做的,惭愧惭愧。
- 头一次遇到printf比cout慢的情况。
- 单调栈与单调队列的元素都是索引,而不是数据本身,这或许是个通用的思路。
- 按理说这道题很简单,但我却在一些小事情上浪费了大量时间,比如我提交了至少五次才发现在oj的G++里cout比printf快。
#include<iostream>
#include<cstdio>
#include<queue>
using namespace std;
deque<int>q;
int a[1000010];
int main() {
int n,k;
scanf("%d %d",&n,&k);
for(int i=1; i<=n; i++)scanf("%d",&a[i]);
if(k==1) {
for(int i=1; i<=n; i++)
cout<<a[i]<<" ";
cout<<endl;
for(int i=1; i<=n; i++)
cout<<a[i]<<" ";
return 0;
}
for(int i=1; i<=n; i++) {
//队列满,弹出前面的元素
if(i-q.front()==k) {
q.pop_front();
}
//对队列进行整理
if(q.empty()||a[i]>=a[q.back()]) {
q.push_back(i);
} else {
while(!q.empty()&&a[i]<a[q.back()])
q.pop_back();
q.push_back(i);
}
//为什么cout比printf快???
if(i>=k) cout<<a[q.front()]<<" ";
}
printf("\n");
q.clear();
for(int i=1; i<=n; i++) {
//队列满,弹出前面的元素
if(i-q.front()==k) {
q.pop_front();
}
//对队列进行整理
if(q.empty()||
a[i]<=a[q.back()]) {
q.push_back(i);
} else {
while(!q.empty()&&a[i]>a[q.back()])
q.pop_back();
q.push_back(i);
}
//
if(i>=k) cout<<a[q.front()]<<" ";
}
return 0;
}
后记
一些题目开始没有思路,于是想从网上找找灵感,结果发现大家写的东西都挺像的,
比如网上很多文章都讲不清楚“符合条件”这个关键逻辑到底是什么。
激情和兴趣太重要了,最近又重温了轮子哥的故事,他是真的面向兴趣编程。我现在有点面向GPA和money编程了。
俺也要面向兴趣编程,冲冲冲!