Week5 作业

A - 最大矩形

题目

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

在这里插入图片描述

解析

这是一道单调栈问题。怎么来寻找最大矩形呢?
我们的思路是,遍历每一个矩形,找出完整包含该矩形的最大的矩形,然后选出最大的那个。
模拟一下,假如说我们从第三个矩形开始,寻找包含第三个矩形的最大的矩形。我们向右找,发现右面的第四个矩形比它要高,那么显然两个矩形的部分可以形成上图中右图所示的大的矩形。我们继续向右,发现下一个矩形要比第三个矩形矮,同时第三个矩形的左边也比第三个矩形矮,这样我们就停止了。
(注意这里遇到比它矮的为什么不能选择降低高度呢,是因为如果降低高度,就相当于从第五个矩形开始,以它的高度寻找最大矩形了。这个情况我们遍历时会遇到。)
根据这个思路,我们可以用单调栈来做。利用单调栈,我们可以更快速的求出第一个比它小的元素。

代码

#include<iostream> 
#include<cstring>
using namespace std; 

int main(){
    int a[100005], st[100005], wid[100005];
	int n; 
	while (cin >> n && n) 
	{ 
		int cnt = 0; 
		long long ans = 0; 
		for(int i = 0 ; i <= n + 1 ; i++)
		{
			a[i] = 0;
			st[i] = 0;
			wid[i] = 0;
		}
		
		for (int i = 1 ; i <= n ; i++) 
			cin >> a[i]; 
		
		// 利用单调栈,找第一个比它小的元素	
		for (int i = 1 ; i <= n + 1 ; i++) 
		{
			// 如果比栈顶元素大就入栈 
			if (a[i] >= st[cnt]) 
			{
				cnt++;
				st[cnt] = a[i]; 
				wid[cnt] = 1; 
			} 
			// 比栈顶元素大的时候结束
			else 
			{
				int width = 0; 
				while (st[cnt] > a[i]) 
				{
					width += wid[cnt]; 
					long long tmp = (long long) width * st[cnt];
					if(tmp > ans)
						ans = tmp;
					cnt--; 
				}
				cnt++;
				st[cnt] = a[i];
				wid[cnt] = width + 1;
			}
		}
		cout << ans << endl;
	}
	return 0; 
}

回顾

这道题我在写的时候是WA了几次了,原因是当时看到hi的范围是十亿以内,小于int类型的上限的21亿,就用了int,WA了几次没有明白为什么,后来明白很有可能会出现几个好几亿的矩形挨在一起导致爆int…改成long long就对了。(不像后面B题完全没意识到用long long。)

B-TT的魔法猫

题目

多亏了上周大家的帮助,TT终于得到了一只可爱的猫。但没想到的是,这是一只神奇的猫。
有一天,神奇的猫决定调查TT的能力,给他一个问题。即从世界地图中选择n个城市,a[i]表示第i个城市拥有的资产价值。
然后,这只神奇的猫将执行几项操作。每轮选择[l,r]区间内的城市,并将其资产价值增加c。最后,需要给出q操作后各城市的资产价值。
你能帮我找到答案吗?

解析

暴力做的话很简单,会在第四个点超时。这题会进行超级多次的数据的区域性增加或减少操作,可以通过前缀和优化来大大降低复杂度。
我们把数组 a[ ] 的前缀和定义为 sum[ ](下面代码中是b[ ]),那么对于一段区间 [ l , r ],在这段区间内让a[ ] 集体加x,等价于让它的前缀和数组中 sum[ l ] += x , b[ r + 1] -= x。利用这个特点我们可以很快速的执行q条操作。最后再按照前缀和转化为原数组的公式转化后输出即可。

代码

#include<iostream> 
#include<cstring>
using namespace std; 
long long a[200005], b[200005];

int main(){
	int n, q;
	scanf("%d %d", &n, &q);
	for (int i = 1 ; i <= n ; i++) 
		scanf("%lld", &a[i]);
	b[1] = a[1];
	for (int i = 2 ; i <= n ; i++) 
		b[i] = a[i] - a[i - 1];
	int l, r, x;
	for (int i = 1 ; i <= q ; i++)
	{
		scanf("%d %d %d", &l, &r, &x);
		b[l] += x;
		b[r + 1] -= x;
	}
	a[1] = b[1];
	for (int i = 2 ; i <= n ; i++) 
		a[i] = b[i] + a[i - 1];
	cout << a[1];
	for (int i = 2 ; i <= n ; i++)
		printf(" %lld", a[i]); 
}

回顾

这题试了一下暴力做法,果然超时了。
用前缀和优化之后,因为没有用 scanf 而用了 cin 导致在第15个点超时。
修改之后,却在第17个点WA掉了。我当时明白前面的都对了后面的出现了WA,很可能是在较大的数据下运行时出现了错误,在A题的教训下,我试着把 int 换成了 long long,然后就蒙过了。
回头分析一下,我发现当时看到q的数量级为1e6,加的数字x的数量级为1e5,均远小于int的数量级
2e9,就感觉 int 装得下,可实际上二者乘起来 1e11 是远大于int的,爆掉的也不奇怪。
以后更要仔细分析。

C - 平衡字符串

题目

一个长度为 n 的字符串 s,其中仅包含 ‘Q’, ‘W’, ‘E’, ‘R’ 四种字符。
如果四种字符在字符串中出现次数均为 n/4,则其为一个平衡字符串。
现可以将 s 中连续的一段子串替换成相同长度的只包含那四个字符的任意字符串,使其变为一个平衡字符串,问替换子串的最小长度?
如果 s 已经平衡则输出0。

解析

这题课上学长讲之前我想到了一种和学长不太一样的思路,不过我还是按照学长讲的方法写了代码。
先说一下我的另一种思路:
这道题其实等价于:在字符串中寻找一个最短子串,使除去这个子串的其它部分的各字母个数均小于等于n/4。
简单解释一下,既然要通过替换我们找到的子串使得其达到平衡字符串,那么我们必然无法通过替换这个子串内的字母使外面的字母数量减少,只能保持不变和增加。因为其最终目标是 n/4 ,所以其必须要小于等于 n/4,这是充分条件。然后我们找出所有满足的子串,继而找出最短的子串,我们就得到我们想要的结果。
言归正传。这是一道可以通过尺取法(双指针法)来在O(n)的复杂度之下实现的一道。我们先来看这题满不满足尺取法的条件:
在这里插入图片描述
在这里插入图片描述
我们选取一个子串,用sum1[ ], sum2[ ], sum3[ ], sum4[ ]四个数组分别记录子串之外的部分的各字母数量(实际上便于操作这里数组里是前缀和)。设选取的子串长度为total,采用尺取法,先通过替换串内部分字母,使串外四类字符数量一致。需要的字符数为(MAX - sum11) + (MAX - sum22) + (MAX - sum33) + (MAX - sum44) (式中sum11代表不含串内字母的其它部分中字母Q的数量,其它类推。MAX代表外面四个字母个数的最大值,因为所有字母都要向这个最大值标齐,所以要用MAX减去。)然后判断串内未被用来替换的剩余的字母个数FREE = total - (MAX - sum11) - (MAX - sum22) - (MAX - sum33) - (MAX - sum44)是否为4的倍数且大于零,否则不成立。这样我们通过尺取遍历,找到所有解的最小值,就是我们想要的结果。

代码

#include<iostream> 
#include<string>
#include<algorithm>
using namespace std; 

int sum1[100005], sum2[100005], sum3[100005], sum4[100005]; 
string s;

int main(){
	int ans = 100005;
	cin >> s;
	int sl = s.length();
	sum1[0] = 0;
	sum2[0] = 0;
	sum3[0] = 0;
	sum4[0] = 0;
	for(int i = 0 ; i < sl ; i++)
	{
		if(s[i] - 'Q' == 0)
		{
			sum1[i + 1] = 1 + sum1[i];
			sum2[i + 1] = sum2[i];
			sum3[i + 1] = sum3[i];
			sum4[i + 1] = sum4[i];
		}
		if(s[i] - 'W' == 0)
		{
			sum1[i + 1] = sum1[i];
			sum2[i + 1] = 1 + sum2[i];
			sum3[i + 1] = sum3[i];
			sum4[i + 1] = sum4[i];
		}
		if(s[i] - 'E' == 0)
		{
			sum1[i + 1] = sum1[i];
			sum2[i + 1] = sum2[i];
			sum3[i + 1] = 1 + sum3[i];
			sum4[i + 1] = sum4[i];
		}
		if(s[i] - 'R' == 0)
		{
			sum1[i + 1] = sum1[i];
			sum2[i + 1] = sum2[i];
			sum3[i + 1] = sum3[i];
			sum4[i + 1] = 1 + sum4[i];
		}
	}

	int r = 1, l = 1;
	while(l <= sl && r <= sl)
	{
		int sum11, sum22, sum33, sum44;
		sum11 = sum1[sl] - sum1[r] + sum1[l - 1];
		sum22 = sum2[sl] - sum2[r] + sum2[l - 1];
		sum33 = sum3[sl] - sum3[r] + sum3[l - 1];
		sum44 = sum4[sl] - sum4[r] + sum4[l - 1];
		int tmp1 = max(sum11, sum22);
		int tmp2 = max(sum33, sum44);
		int MAX = max(tmp1, tmp2);
		int total = r - l + 1;
		int FREE = total - (MAX - sum11) -  (MAX - sum22) -  (MAX - sum33) -  (MAX - sum44);
		if(FREE >= 0 && FREE % 4 ==0)
		{
			if(total < ans)
				ans = total;
			l++;
		}
		else 
			r++;
	}
	cout << ans;
}

回顾

刚开始理解学长的方法确实花费了一些时间,反反复复看了回放好几次…像理解这个做法最重要的是理解total、FREE这几个变量的含义。
mark一下,等写完手头的作业把自己的那种思路写一下。

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

解析

单调队列的问题,用deque的话大概会比较简单,用数组的话这个判断比较复杂。(本题是通过数组实现的)。
区别于单调栈一般用于求全局的最大最小值,单调队列往往是维护部分区间的单调性,也即求部分区间的最大最小值,正好符合题意。
我们以上面题干中的数组为例。我们首先找最大值。窗口处在第一个情况时,1入队;3比1大,1出队,3入队;-1比3小,-1入队;窗内所有元素遍历完毕,队首元素3为最大值。此时队首窗口滚动一格,3不在窗口内,出队;此时队首元素-1,-3小于-1,-3入队…依次类推,我们可以得出每个窗口的最大值。同样的,改判断条件为大于,则可以得到最小值。
单调队列会根据数字的位置是否已经在窗口之外选择将其出队,可以通过一个额外的数组记录每个数据在数组的位置或者把元素写成数据和位置的结构体。为了方便这里写的第二种(实则是边界已经判断不清了,实在是不想数组套数组了)。
注意这题输出队首时从第k个数字判断结束后开始(这时才遍历完第一个窗口)。

代码

#include<iostream> 
#include<string>
#include<algorithm>
using namespace std; 

struct node{
	int val;
	int pos;
} que[1000005];
 
int a[1000005];
 
int main()
{
	int n, k;
	cin >> n >> k;
	for(int i = 1 ; i <= n ; i++)
		scanf("%d", &a[i]);

	int head = 1, tail = 0;
	for(int i = 1 ; i <= n ; i++)
	{
		while(tail >= head && a[i] < que[tail].val)
			tail--;
		tail++;
		que[tail].val = a[i];
		que[tail].pos = i;
		while(head <= tail && que[head].pos + k <= i)
			head++;
		if(i >= k)
			printf("%d ", que[head].val);
	}
	cout << endl;
	
	head = 1, tail = 0;
	for(int i = 1 ; i <= n ; i++)
	{
		while(head <= tail && a[i] > que[tail].val)
			tail--;
		tail++;
		que[tail].val = a[i];
		que[tail].pos = i;
		while(head <= tail && que[head].pos + k <= i)
			head++;
		if(i >= k)
			printf("%d ", que[head].val);
	}
}

回顾

刚开始对单调队列实在是理解的不够好,就反反复复听了好几遍回放,然后又去搜了一些讲解的博客,总算是弄明白了 。弄明白了这题就很简单了。不过值得一提的是我在搜博客的时候看到了一个leetcode上的高分解(超过100%的人),然后我没有看懂。
/抱拳,才疏学浅,继续努力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值