【20200226程序设计思维与实践 Week5 作业】


A - 最大矩形

题意

给一个直方图,求直方图中的最大矩形的面积。例如,下面这个图片中直方图的高度从左到右分别是2, 1, 4, 5, 1, 3, 3, 他们的宽都是1,其中最大的矩形是阴影部分。

在这里插入图片描述


思路

本题利用单调栈求解。

从左往右扫描直方图并入栈,维护一个单调递增栈,则当一个直方出栈时就知道以其高度能达到的最右边界位置。为了能区分直方,栈中的是直方的序号,比较时根据序号在高度数组中查询相应的高度信息。一遍扫描完成后,就能记录下每个直方最右边界位置。

同理,再从右往左扫描一遍,得到每个直方最左边界位置。

左右边界相减乘以高度,得到以每个直方为高的矩形的最大面积,输出其中的最大值。


总结

本题是对单调栈的基本运用,帮助更好地理解了单调栈的实现过程。

一个选择变量类型的教训:虽然输入的矩形高度int足以表示,但乘积结果需要long long储存,所以高度也要用long long储存,否则默认类型转换会将int溢出处理后传入long long。


代码

#include<stdio.h>
#include<algorithm>
using namespace std;

long long a[100050];//高度//不只计算要判断大小,传参也要!
int stackArray[100050];//栈 
int l[100050];//左边界 
int r[100050];//右边界 
long long area[100050];//最大面积 

class stack{
	private:
		int sp;
	
	public:
		stack(){sp=0;}
		bool empty(){return sp==0;}
	    int top(){return stackArray[sp-1];}
    	void pop(){sp--;}
    	void push(int e){stackArray[sp++]=e;}
}; 

stack S;

int main(){
	int n;
	while(1){
		//读取高度 
		scanf("%d",&n);
		if(n==0)break;
		for(int i=1;i<=n;i++){
			scanf("%lld",&a[i]);
		}
		
		//从左到右扫描高度数组并维护单调栈 
		int cot=0;
		S.push(1);
		for(int i=2;i<=n;i++){
			cot++;
			while((!S.empty())&&a[S.top()]>a[i]){//出栈 
				r[S.top()]=cot;//记录右边界 
				S.pop();
			}
			S.push(i);
		}
		cot++;
		while(!S.empty()){//为留在栈内的直方都赋右边界 
			r[S.top()]=cot;
			S.pop();
		}
		
		//从右到左扫描高度数组并维护单调栈  
		S.push(n);
		for(int i=n-1;i>=1;i--){
			cot--;
			while((!S.empty())&&a[S.top()]>a[i]){//出栈 
				l[S.top()]=cot;//记录左边界 
				S.pop();
			} 
			S.push(i);
		}  
		cot--;
		while(!S.empty()){//为留在栈内的直方都赋左边界 
			l[S.top()]=cot;
			S.pop();
		}
		
		//计算面积 
		for(int i=1;i<=n;i++){
			area[i]=a[i]*(r[i]-l[i]);
		}
		sort(area+1,area+n+1);
		printf("%lld\n",area[n]);//输出最大值 
	} 
}

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?


思路

本题暴力做法时间复杂度为O(qn),超时,所以通过构建差分数组来解。

首先建立差分数组b[],其中元素b[i]为原数组元素a[i]与a[i-1]的差(为简化代码在元素组中添加元素a[0]=0)。这样,对于每一次操作,原数组的区间修改变为差分数组的单点修改,只需相应改变b[l]b[r+1],时间复杂度减小到O(q)。全部操作结束后,最终每一个a[i]b[1]b[i]的累加和。


总结

本题是对前缀和与差分的简单运用,巩固了所学知识。


代码

#include<stdio.h>

int n,p;
long long a[200050];
long long b[200050];
long long l,r,c; 
 
int main(){
	a[0]=b[0]=0;
	scanf("%d %d",&n,&p);
	for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
	for(int i=1;i<=n;i++)b[i]=a[i]-a[i-1];//建立差分数组 
	
	for(int i=1;i<=p;i++){//对a的区间操作转化为对b的单点操作 
		scanf("%lld %lld %lld",&l,&r,&c);
		b[l]+=c;
		b[r+1]-=c;
	}
	
	for(int i=1;i<=n;i++){//累加b数组,还原a 
		a[i]=b[i]+a[i-1];
		printf("%lld ",a[i]);
	}
	printf("\n");
} 

C - 平衡字符串

题意

一个长度为 n 的字符串 s,其中仅包含 ‘Q’, ‘W’, ‘E’, ‘R’ 四种字符。

如果四种字符在字符串中出现次数均为 n/4,则其为一个平衡字符串。

现可以将 s 中连续的一段子串替换成相同长度的只包含那四个字符的任意字符串,使其变为一个平衡字符串,问替换子串的最小长度?

如果 s 已经平衡则输出0。


思路

本题求解目标是一个连续区间,并且左右端点的移动有明确方向,故可用尺取法求解。

输入过程中预处理字符串,统计出4种字符的个数并保存。将其个数复制到4中字符当前个数数组,以备更新。特判是否已是平衡数组。

在字符串左端初始化长度为0区间,区间覆盖的区域即要替换的子串。当覆盖区间不能使字符串平衡时,右边界增1;当覆盖区间能使字符串平衡时,左边界增1。用一个辅助变量记录区间长度,每次区间边界变化且能使字符串平衡时,若新区间比记录值小,则更新记录。区间扫描完字符串时结束循环。

用如下过程判断字符串是否平衡:
计算当前区间长度减去能使4种字符当前值相等的数值后的差,若差为非负值且为4的倍数,则能使字符串平衡。

		int m=max(mi[0],mi[1],mi[2],mi[3]);
		int jdg=w-4*m+mi[0]+mi[1]+mi[2]+mi[3];
		if(jdg>=0&&jdg%4==0)平衡

总结

本题练习了尺取法。

一开始书写尺取过程时,将区间左右边界分别在尺取大循环的两个小循环中改变,发生WA,debug后进行了三个小修改,但依然无法通过。考虑到进行的三处修改都发生在在循环交接处,可能将循环分割书写造成了许多难以排查的特殊情况,加上debug已经消耗了大量时间并且可能还要消耗大量时间,选择重构代码。

重构后将区间左右边界的自增放在同一个判断分支中,极大地简化了判断情况,顺利AC。


代码

重构AC代码:

//AC代码 
#include<iostream>
using namespace std;

string s;
char str[100005];
int sum[5];//4种字符原始个数 
int mi[5];//4种字符当前个数 

int swi(char x){//将字符转化为0~3的数字 
	if(x=='Q')return 0;
	else if(x=='W')return 1;
	else if(x=='E')return 2;
	else if(x=='R')return 3;
}

int max(int a,int b,int c,int d){
	int x=a>b?a:b;
	int y=c>d?c:d;
	return x>y?x:y;
}

int main(){
	cin>>s;
	int len=s.length();
	for(int i=0;i<len;i++){//输入与预处理 
		str[i]=s[i];
		sum[swi(s[i])]++;
	}
	for(int i=0;i<=3;i++)mi[i]=sum[i];
	
	int n=len;
	if(sum[0]==n/4&&sum[1]==n/4&&sum[2]==n/4&&sum[3]==n/4){//特判是否已平衡 
		cout<<0<<endl;
		return 0;
	}
	
	int p,q,ans,w;
	p=q=0;
	ans=n;
	int lr=1;//标记上次变化是哪个边界,若0则为左边界,若1则为右边界,若2则为两边界一起动 
	
	//尺取 
	while(q<n){
		w=q-p+1;//当前选取区间长度 
		
		//更新4种字符未被覆盖个数 
		if(lr==1)mi[swi(str[q])]--;
		else if(lr==0)mi[swi(str[p-1])]++;
		else {
			mi[swi(str[q])]--;
			mi[swi(str[p-1])]++;
		}
		
		//判断是否平衡 
		int m=max(mi[0],mi[1],mi[2],mi[3]);
		int jdg=w-4*m+mi[0]+mi[1]+mi[2]+mi[3];
		if(jdg>=0&&jdg%4==0){//满足条件,平衡 
			ans=w<ans?w:ans;//更新答案 
			if(p==q){//若左右边界相等,右边界随左边界增1 
				p++;
				q++;
				lr=2;
			}
			else{//左边界增1 
				p++;
				lr=0;
			}
		}
		else{//不满足条件,右边界增1 
			q++;
			lr=1;
		}	
	}
	cout<<ans<<endl;
}

原WA代码:

//WA代码,长时间对拍依然未能解决,留待日后修改 
#include<stdio.h>

char c;
int n,aven,ans;
int sum[5];
int mi[5];
char str[100005];

int swi(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 max(int a,int b,int c,int d){
	int x=a>b?a:b;
	int y=c>d?c:d;
	return x>y?x:y;
}

int main(){
	while(scanf("%c",&c)!=EOF){
		sum[swi(c)]++;
		str[++n]=c;
	}
	n--;
	aven=n/4;
	for(int i=0;i<=3;i++){
		mi[i]=sum[i];
	}
	
    if(sum[0]==aven&&sum[1]==aven&&sum[2]==aven&&sum[3]==aven){
    	printf("0\n");
    	return 0;
	}

	int p,q,w,m;
	p=1;
	q=1;
	ans=n-1;
	int cot=0;
	while(p<=q&&q<=n){
		
		//右边界变动 
		while(q<=n){
			w=q-p+1;
			mi[swi(str[q])]--;
			
			m=max(mi[0],mi[1],mi[2],mi[3]);
			int jdg=w-4*m+mi[0]+mi[1]+mi[2]+mi[3];
			if(jdg>=0&&jdg%4==0){
				ans=w<ans?w:ans;
				break;
			}
			q++;
		}
		if(q>n)q=n;
		
		p++;
		//左边界变动 
		while(p<=n&&p<=q){
			w=q-p+1;
			mi[swi(str[p-1])]++;
			
			m=max(mi[0],mi[1],mi[2],mi[3]);
			int jdg=w-4*m+mi[0]+mi[1]+mi[2]+mi[3];
			if(jdg>=0&&jdg%4==0){
				p++;
				ans=w<ans?w:ans;
				continue;
			}
			else {
				break;
			}
		}
		q++;
	}
	printf("%d\n",ans);	
}


D - 滑动窗口

题意

ZJM 有一个长度为 n 的数列和一个大小为 k 的窗口, 窗口可以在数列上来回移动. 现在 ZJM 想知道在窗口从左往右滑的时候,每次窗口内数的最大值和最小值分别是多少.


思路

本题利用单调队列求解。

与A题一样,入队的是数组索引而非值本身。

从左往右扫描数组并入队,维护一个单调递减队列。每当访问到下一个新元素,首先检查其与对手元素的索引距离是否已超出窗口长度,若是则队首出队,直到窗口长度可以容纳距离;接着新元素与队尾比较,若队尾元素小于新元素,则出队,直到新元素遇到第一个大于它的元素或队空,入队,这是单调队列的维护过程。记录下此时的队首元素,这是当前窗口内的最大值。扫描完整个数组,就得到全部窗口的最大值。

同理,再从左往右扫描数组并入队,维护一个单调递增队列,此时扫描完就得到了全部窗口的最小值。

按题目要求输出。


总结

本题是对单调队列的简单运用,巩固了所学知识。

debug疑问:第39行处,窗口只会单步推进,队首出队只会发生一次,即使用while也不会超过一次循环,为什么只能用if判断不能用while?队首出队条件考虑不全面吗?


代码

#include<stdio.h>

int A[1000050];
int k,n;
int MIN[1000050];
int MAX[1000050];
int que[1000050];

struct queue{
	int p;
	int q;
	
	queue(){p=0;q=0;}
	bool empty(){return p==q;}
	int size(){return q-p;}
	void push(int v){que[q++]=v;}
	int front(){return que[p];}
	void pop(){p++;}
	int last(){return que[q-1];}
	void erase(){q--;}
};

queue Q;

void min(){
	Q.p=Q.q=0;
	Q.push(1);
	//队列初始化 
	for(int i=2;i<=k;i++){
		while(!Q.empty()){//维护队列的单调增性 
			if(A[Q.last()]>A[i]) Q.erase();
			else break;
		}
		Q.push(i);//入队 
	}
	MIN[1]=A[Q.front()];//记下最小值
	//扫描 
	for(int i=k+1;i<=n;i++){
		if(i-Q.front()>=k) Q.pop();//保证不超过窗口大小 //为什么用while不对? 
		while(!Q.empty()){
			if(A[Q.last()]>A[i]) Q.erase();//维护队列单调增性 
			else break;
		}
		Q.push(i);//入队 
		MIN[i-k+1]=A[Q.front()];
	}
}

void max(){
	Q.p=Q.q=0;
	Q.push(1);
	for(int i=2;i<=k;i++){
		while(!Q.empty()){
			if(A[Q.last()]<A[i]) Q.erase();
			else break;
		}
		Q.push(i);
	}
	MAX[1]=A[Q.front()];
	for(int i=k+1;i<=n;i++){
		if(i-Q.front()>=k) Q.pop();
		while(!Q.empty()){
			if(A[Q.last()]<A[i]) Q.erase();
			else break;
		}
		Q.push(i);
		MAX[i-k+1]=A[Q.front()];
	}
}

int main(){
	scanf("%d %d",&n,&k);
	for(int i=1;i<=n;i++)scanf("%d",&A[i]);
	
	min();
	max();
	
	for(int i=1;i<=n-k+1;i++)printf("%d ",MIN[i]);
	printf("\n");
	for(int i=1;i<=n-k+1;i++)printf("%d ",MAX[i]);
	printf("\n");
}



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值