程序设计作业week5
To sum up
本周的作业主要聚焦于 单调栈,单调队列,尺取法,前缀和与查分。难度个人感觉依次递减。单调栈、队列在等号的取舍,循环结束的判断处理,原数组收尾元素的处理等细小地方容易出错。
虽然题目不难,但每道题都卡了挺长时间的(ACD题各卡了一天左右,基本节奏就是前一天晚上写出整体,然后当天晚上加第二天白天找错)。主要还是一些小的地方没有考虑到,每道题会具体说明。
以下习题按照上述知识点顺序展开。
一、Problem A 最大矩形(单调栈)
给一个直方图,求直方图中的最大矩形的面积。例如,下面这个图片中直方图的高度从左到右分别是2, 1, 4, 5, 1, 3, 3, 他们的宽都是1,其中最大的矩形是阴影部分。
1.Sample Input and Output
Input
输入包含多组数据。每组数据用一个整数n来表示直方图中小矩形的个数,你可以假定1 <= n <= 100000. 然后接下来n个整数h1, …, hn, 满足 0 <= hi <= 1000000000. 这些数字表示直方图中从左到右每个小矩形的高度,每个小矩形的宽度为1。 测试数据以0结尾
7 2 1 4 5 1 3 3
4 1000 1000 1000 1000
0
Output
对于每组测试数据输出一行一个整数表示答案。
8
4000
2.解题思路及代码
在课上刚拿到题的时候没有考虑用单调栈。只是在暴力的基础上做了一些优化。
首先创建point类,类中变量成员存储他的左右边界(左右第一个比它小的值),和他的高度。
在输入第i个元素时自动更新左边界:如果i-1元素的高度比它小,则左边界即为i-1。如果等于,则左边界等与i-1元素的左边界。如果i-1元素的高度比它高,则将i-1的左边界代替上述的i-1执行判断。
右边界的确定类似。
在确定最大面积时进行小小的剪枝,如果当前元素与前一个相同或高度为0,则直接跳过。
代码如下:
#include<iostream>
#include<vector>
using namespace std;
class Point
{
public:
Point(int x)
{
element=x;
Lband=0;
Rband=0;
}
public:
int element;//高度
int Lband=0;//左边第一个比它小的元素
int Rband=0;//右边第一个比它小的元素
};
vector<Point> List;
int n;
int result;
int main()
{
while(cin>>n)
{
List.clear();
if(n==0)
break;
int N;
cin>>N;
Point NElement(N);
NElement.Lband=-1;
List.push_back(NElement);
//初始化数组并确定每个点的左边界
for(int i=1;i<n;i++)
{
cin>>N;
Point newElement(N);
if(N>List[i-1].element)
newElement.Lband=i-1;
else
{
int tmp=List[i-1].Lband;
while(List[tmp].element>=N)
{
tmp=List[tmp].Lband;
if(tmp==-1)
break;
}
newElement.Lband=tmp;
}
List.push_back(newElement);
}
//确定每个点的右边界
List[n-1].Rband=n;
for(int i=n-2;i>=0;i--)
{
if(List[i].element>List[i+1].element)
List[i].Rband=i+1;
else {
int tmp=List[i + 1].Rband;
while(List[tmp].element>=List[i].element)
{
tmp=List[tmp].Rband;
if(tmp==n)
break;
}
List[i].Rband=tmp;
}
}
//确定最大面积
result=n;
for(int i=0;i<n;i++)
{
if(List[i].element==1)
continue;
else if(i>=1&&List[i].element==List[i-1].element)
continue;
int tmp=(List[i].Rband-List[i].Lband-1)*List[i].element;
if(tmp>result)
result=tmp;
}
cout<<result<<endl;
}
return 0;
}
使用单调栈:
维护一个单调递增栈,将数组一次插入栈中,插入时:
当栈为空或者待入栈元素大于栈顶元素时,进行入栈操作。
当栈顶元素大于待入栈元素时,栈顶元素的右边界即为带插入元素,栈顶元素出栈。
循环所有元素,即得到更新后的右边界数组,右边界数组中第i个元素储存的是a[i]的右边第一个比它小的元素的下标。
左边界数组类似,将数组倒序插入即可。
#include<iostream>
const int N=1e5+100;
using namespace std;
long long n,a[N],L[N],R[N],st[N];
//更新数组元素的右边界R【】
void solve1()
{
int l=1,r=0;
for(int i=0;i<n;i++)
{
while(l<=r && a[st[r]]>a[i]) //a[st[r]]即为栈顶元素
{
R[st[r]]=i-1;
r--;
}
st[++r]=i;
}
for(;r>=1;r--)
R[st[r]]=n-1;
R[n-1]=n-1;
}
//更新数组元素的左边界L【】
void solve2()
{
int l=1,r=0;
for(int j=n-1;j>=0;j--)
{
while(l<=r && a[st[r]]>a[j]) //a[st[r]]即为栈顶元素
{
L[st[r]]=j+1;
r--;
}
st[++r]=j;
}
for(;r>=1;r--)
L[st[r]]=0;
L[0]=0;
}
int main()
{
while(cin>>n)
{
if(n==0)
break;
for(int i=0;i<n;i++)
{
cin>>a[i];
}
solve1();
solve2();
/*
for(int i=0;i<n;i++)
cout<<R[i]<<' ';
cout<<endl;
for(int i=0;i<n;i++)
cout<<L[i]<<' ';
*/
long long result=0;
for(int i=0;i<n;i++)
{
long long tmp=(R[i]-L[i]+1)*a[i];
if(tmp>result)
result=tmp;
}
cout<<result<<endl;
}
return 0;
}
二、 Problem D 滑动窗口(单调队列)
一个长度为 n 的数列和一个大小为 k 的窗口, 窗口可以在数列上来回移动. 求在窗口从左往右滑的时候,每次窗口内数的最大值和最小值分别是多少.
1.Sample Input and Output
Input
输入有两行。第一行两个整数n和k分别表示数列的长度和滑动窗口的大小,1<=k<=n<=1000000。第二行有n个整数表示ZJM的数列。
8 3
1 3 -1 -3 5 3 6 7
Output
输出有两行。第一行输出滑动窗口在从左到右的每个位置时,滑动窗口中的最小值。第二行是最大值。
-1 -3 -3 -3 3 3
3 3 5 5 6 7
2.解题思路及代码
维护一个单调队列,维护过程主要是队列单调维护(当入队元素破坏单调性时,队尾元素弹出),和队列长度维护(当队列长度>k时,队首弹出)。
在确定区间最小元素时,维护一个单调递增队列,每次维护过程结束后的队首元素即为区间最小值。
在确定区间最大元素时,维护一个单调递增队列,每次维护过程结束后的队首元素即为区间最大值。
#include<iostream>
#include<cstdio>
using namespace std;
const int N=1e6+100;
int n,k;
int a[N],Max[N],Min[N];
int *q;
int l,r;
int main() {
cin >> n;
cin >> k;
q = new int[k + 10];
for (int i = 0; i < n; i++)
scanf("%d", &a[i]);
l = 1, r = 0;
for (int i = 0; i < n; i++) {
while (r >= l && a[q[r]] > a[i])
r--;
q[++r] = i;
if (q[r] - q[l] + 1 > k)
l++;
Min[i] = a[q[l]];
}
l = 1, r = 0;
for (int i = 0; i < n; i++) {
while (r >= l && a[q[r]] < a[i])
r--;
q[++r] = i;
if (q[r] - q[l] + 1 > k)
l++;
Max[i] = a[q[l]];
}
for (int i = k - 1; i < n; i++)
printf("%d ", Min[i]);
cout << endl;
for (int i = k - 1; i < n; i++)
printf("%d ", Max[i]);
return 0;
}
3.总结
这道题卡了挺长时间的,一开始的错误是数据长度用了long long但scanf和printf都用的%d。后来还是long long的原因,我在本地用CLION跑的时候输出均没有问题,但换win+Dev时候发现,用ll数据长度对自然数的数据是可以正确输出的,但当输入数据里有负数的时候,会产生错误输出 (猜想原因是CLION在当数据不满足的时候LL长时候会自动先优化为int长度)。最后报错是因为选择了G++编译器,换C++就A了。
三、 Problem C 平衡字符串(尺取法)
一个长度为 n 的字符串 s,其中仅包含 ‘Q’, ‘W’, ‘E’, ‘R’ 四种字符。
如果四种字符在字符串中出现次数均为 n/4,则其为一个平衡字符串。
现可以将 s 中连续的一段子串替换成相同长度的只包含那四个字符的任意字符串,使其变为一个平衡字符串,问替换子串的最小长度?
如果 s 已经平衡则输出0。
1.Sample Input and Output
Input
一行字符表示给定的字符串s
QQQW
Output
一个整数表示答案
2
2.解题思路及代码
主要算法是尺取法,核心是使用双指针lr标记当前区间的左右两个端点。然后对目前区间进行判断,如果满足条件,更新最优解大小,然后左指针右移,缩小区间长度以尝试获得更优答案。如果当前区间不满足,则右指针右移,尝试获得能够满足条件的区间。
就本题而言,首先统计四个字母出现的次数。如果出现次数均相等,则输出0后结束。如果不想等,则至少有一个出现的次数>n/4。统计各个字母超出n/4的个数并保存在a数组中,(不足n/4的设置为0)。
可置换的区间需满足的条件是,区间内各个字母的出现次数要大于a数组中的对应字母的值。(即可以将各字母超出n/4的部分抹掉换成不足n/4的字母)。
剩余过程用尺取法即可求得最优解。
#include<iostream>
#include<string>
using namespace std;
long long l,r;
string str;
long long a[4]={0,0,0,0}; //统计每个字母超出n/4的个数
long long b[4]={0,0,0,0}; //统计区间内的字母个数
long long result=0;
bool wh()
{
for(int i=0;i<4;i++)
{
if(a[i]==0)
continue;
else if(a[i]>b[i])
return false;
}
return true;
}
int letter(char x)
{
if(x=='Q')
return 0;
else if(x=='W')
return 1;
else if(x=='E')
return 2;
else if(x=='R')
return 3;
}
int main()
{
while(cin>>str)
{
//初始化
for(int i=0;i<4;i++)
{
a[i]=0;
b[i]=0;
}
result=0;
long long n = str.size();
//QWER计数
for (int i = 0; i < n; i++)
{
if (str[i] == 'Q')
b[0]++;
else if (str[i] == 'W')
b[1]++;
else if (str[i] == 'E')
b[2]++;
else //R
b[3]++;
}
//如果已经平衡
if (b[0] == b[1] && b[1] == b[2] && b[2] == b[3])
{
cout <<result<<endl;
continue;
}
//计算超出n/4的字母超出大小
for (int i = 0; i < 4; i++)
{
if (b[i] > n / 4) {
a[i] = b[i] - n / 4;
}
b[i] = 0;
}
l = 0;
r = 0;
result = n;
b[letter(str[0])]++;
while (r < n)
{
if (wh()) //如果满足条件,l右移
{
if(l==r)
{
result=1;
break;
}
result = min(result, r - l + 1);
b[letter(str[l])]--;
l++;
}
else { //不满足条件,r右移
r++;
b[letter(str[r])]++;
}
}
cout << result<<endl;
}
return 0;
}
3.总结
这道题也卡了挺长时间。最终发现的问题在这:
bool wh()
{
for(int i=0;i<4;i++)
{
if(a[i]==0)
continue;
//一开始写的是else if(a[i]!=b[i])
else if(a[i]>b[i])
return false;
}
return true;
}
一开始想的很简单,只要与要求出现次数不等就应该返回假。(不足的话无法调整到平衡,超过的话肯定不是最优解 )。但后来仔细想了一下,可能存在还未达到要求次数的字母前有过量已经达到要求的字母的情况。比如 QWQR,需求数组a需要1Q1R,但在这个情况中,要想使R达到1个,第一次未优化的解会有2Q1R。如果按照原来的处理会返回假,然后右指针右移,而实际是满足条件的。
四、 Problem B(前缀和与差分)
对一个n大小的数组元素执行m次操作,每次操作对数组的一个区间内的所有点加同一个数。
其中n和m的大小为2e5。
1.Sample Input and Output
Input
第一行为n和m,分别表示数组大小的后续执行次数。
后面m行有三个数,分别为左端点,右端点和增加的数值。
4 2
-3 6 8 4
4 4 -2
3 3 1
Output
输出操作后的数组
-3 6 9 2
2.解题思路及代码
感觉这是四道题里面最简单的。
开一个数组b存储原数组a中每个元素与前一个元素的差值。
在进行加操作的时候,区间左端点+tmp,右端点的下一个元素-tmp。
还原数组时输出就好了。
#include<iostream>
#include<cstdio>
using namespace std;
const long N=2e5+100;
int a[N],b[N],n,m;
int l,r,tmp;
int main()
{
while(cin>>n)
{
scanf("%d",&m);
a[0]=0;
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1;i<=n;i++)
b[i]=a[i]-a[i-1];
for(int i=0;i<m;i++)
{
scanf("%d",&l);
scanf("%d",&r);
scanf("%d",&tmp);
b[l]+=tmp;
b[r+1]-=tmp;
}
long long tmp2=0;
for(int i=1;i<=n;i++)
{
tmp2+=b[i];
cout<<tmp2<<' ';
}
}
return 0;
}