洛谷好题 P1950 长方形 详细思路题解 有图 附代码

4 篇文章 0 订阅
2 篇文章 0 订阅

在我的洛谷博客阅读

原题传送门(洛谷 P1950 长方形)

暴力做法

直接枚举矩形四条边的所有可能情况,判断每种情况是否符合要求(即长方形内部是否不含画过的方块),但显然这样枚举的情况数非常多,效率太低。

尝试优化思路

把所有剪出来的长方形情况总数划分为划分为互不重复、容易计数的小范围情况,再把所有小范围的情况总和加起来得出答案,其实就是dp的思想。

如何设计枚举方式呢?

我们需要找一个标准去枚举所有情况,而在此题中最容易想到的标准就是每个没有被画过的格子——因为所有符合要求的长方形一定是全由没有被画过的格子组成的,故我们可以通过枚举每一个格子能“扩展”形成的长方形(即这个格子被长方形包含)个数来计算总数。

但是,简单思考一下就能发现,直接按照每个格子枚举所有可扩展形成的长方形数目必然会出现重复,因为一个长方形可以被其中每一个格子扩展得到,而重复的情况数也不好直接计算。

设计不重不漏的枚举

因为每个长方形一定全部包含没画过的格子,一定能被没画过的格子扩展得到,枚举每个没画过的格子得到的情况中一定能包含所有长方形的情况,所以我们已经保证了不重复,那么如何保证不重复呢?

下面以下图这个例子来说明(红色表示涂过的格子)

对于每个白色格子,显然我们可以先从上到下枚举每一行,再从左到右枚举这一行的每个格子。

在竖直方向上枚举并避免重复

枚举到任意一行时,对于每个格子,我们可以发现,这个格子向上能到达的第一个红色格子会限制其继续向上枚举长方形,因为再向上形成的长方形必定包含这个红色格子,不满足要求,所以枚举的每一行,我们只考虑这一行上的每个格子被上面红色格子限制的高度这一范围内的情况,再上方的格子不需要考虑,同样地,我们也不考虑当前行向下扩展的长方形,每个长方形只能被底下的格子枚举到,这样就避免了在竖直方向上的重复。

举个例子,枚举到上面例子中的第四行时,只需要关心下图中这些蓝色方块:

同时,对于每一行,记录每个格子被上面红色格子限制的高度 h h h ,后面的计算会用到。

在水平方向上枚举并避免重复


以下说明中 ( a , b ) (a,b) (a,b) 表示第 a a a 行第 b b b


同上,以枚举到第四行为例。如果直接枚举第二个格子,则它的扩展除了会受到自身高度的限制,还会受到它的左边——第一个格子,和右边——第三个格子的限制高度 h h h 限制,即第二个格子左侧不能向高度为0的第一个格子的限制即格子 ( 4 , 1 ) (4,1) (4,1) 扩展, 右侧不能向高度为1的第三个格子的限制 即格子 ( 3 , 3 ) (3,3) (3,3) 扩展。下图中阴影部分即为第二个格子能扩展到的范围(即能扩展到9个不同长方形):

而出现重复的情况在于,不同的格子扩展到了相同的长方形。例如,第二个格子会向右扩展到第三个格子,枚举到这个由格子 ( 2 , 4 ) (2,4) (2,4) 和格子 ( 3 , 4 ) (3,4) (3,4) 组成的长方形,而到了第三个格子向左扩展时,也会扩展到第二个格子,枚举了相同的长方形。

如何避免这样的重复呢?由于重复情况是左边向右扩展和右边向左扩展发生重叠造成的,所以我们可以限定每个格子能向左和向右扩展的范围来保证其不发生重叠。

如何限定扩展的范围?我们的目标是让重复的长方形有且仅有一个格子能扩展到,而排除其他能扩展到该长方形的情况。由前面的分析,格子两侧第一个比该格子的 h h h 小的格子高度会限制该格子对应的长方形范围,所以我们只需要限制每一个长方形是被左边还是右边第一个 h h h 小的格子枚举到即可。

反过来,对于每一个格子,我们找到其左右两边第一个 h h h 小的格子的编号,分别存入数组 l l l r r r , 枚举时,根据每一个格子的 l l l r r r 规定该格子能向左和向右扩展到的长方形最大宽度,每个格子 i i i 能扩展到的长方形边界就被限制在 ( l [ i ] , r [ i ] ) (l[i],r[i]) (l[i],r[i]) 范围了,但显然,这样无法扩展到 l [ i ] l[i] l[i] r [ i ] r[i] r[i] 对应的格子本身,出现了遗漏,解决方法很简单,让任意一边计算 h h h 时取等即可,这里让左边取等, l [ i ] l[i] l[i] i i i 左边第一个满足 h [ p ] h[p] h[p] 小于等于 h [ i ] h[i] h[i] 的格子编号 p p p , r [ i ] r[i] r[i] i i i 右边第一个满足 h [ q ] h[q] h[q] 小于 h [ i ] h[i] h[i] 的格子编号 q q q 。这样所有连续的一开一闭区间加在一起,一定能刚好覆盖到所有的格子。

可以结合下图理解。

按照这样的思路,再回到例子,由于 r [ 2 ] = 3 r[2] = 3 r[2]=3,故格子 ( 2 , 4 ) (2,4) (2,4) 就不能扩展到格子 ( 3 , 4 ) (3,4) (3,4) 了,而 l [ 3 ] = 1 l[3] = 1 l[3]=1 ,故前面提到的重复长方形只由格子 ( 3 , 4 ) (3,4) (3,4) 枚举到。

计算 h h h

若当前格子 i i i 画过则 h [ i ] = 0 h[i]=0 h[i]=0 ,否则用同一列上面一行格子的 h h h 更新该格子的 h h h h [ i ] h[i] h[i] 的值就是同一列上一行 h h h 的值加一,类似于悬线法的悬线高度求法。

计算 l l l r r r

计算 l l l ,使用一个单调栈从右往左遍历 h [ i ] h[i] h[i] ,维护栈中元素从栈顶到栈底严格单调递减,如果 h [ i ] h[i] h[i] 小于栈顶 h [ t ] h[t] h[t] ,就一个一个出栈,直到栈顶 h [ t ] h[t] h[t] 小于等于 h [ i ] h[i] h[i] ,此时弹出的每一个元素右边第一个非严格小于该元素的 h h h 的格子对应的编号就是 h [ i ] h[i] h[i] ,用 i i i 更新每次弹出栈元素的 l l l 。注意遍历结束后栈中还有剩余元素,需要把它们的 l l l 更新为 0 0 0 ,即左边没有比它们的 h h h 小的格子了。

计算 r r r 同理,注意需要从左往右遍历 h [ i ] h[i] h[i] ,维护栈中元素从栈顶到栈底非严格单调递减,出栈时直到栈顶 h [ t ] h[t] h[t] 小于 h [ i ] h[i] h[i] ,此时弹出的每一个元素左边边第一个严格小于该元素的 h h h 的格子对应的编号为 h [ i ] h[i] h[i] ,遍历结束后栈中剩余元素应更新为 m + 1 m+1 m+1 ( m m m 即列数)。

计算每个格子扩展的长方形数量

已经得到了 h [ i ] , l [ i ] , r [ i ] h[i],l[i],r[i] h[i],l[i],r[i] 三个值,要计算每个格子能扩展的长方形数量,即计算在一个边长 h [ i ] ⋅ ( r [ i ] − l [ i ] ) h[i]\cdot(r[i]-l[i]) h[i](r[i]l[i]) 的矩形中,经过 i i i 对应的格子的矩形数量,以 i i i 为分界线分左右为两部分,由乘法原理(长方形数量即为从左右两部分分别任选一个格子的情况数的乘积),得该格子能扩展出的矩形数量是 h ⋅ ( r [ i ] − i ) ⋅ ( i − l [ i ] ) h\cdot(r[i]-i)\cdot(i-l[i]) h(r[i]i)(il[i])

如果计算到的格子是被涂过的,则它的 h = 0 h=0 h=0 ,算出的矩形数量也是 0 0 0 ,累加被涂过的格子时时自动忽略了这种情况。

Tips

  1. 计算 l l l r r r 时不能用栈顶元素更新当前元素 i i i l l l ,必须出栈时才反过来用当前元素更新栈顶出栈了的元素的 l l l ,因为前者无法保证栈顶元素一定是左边或右边第一个 h h h 小于(等于)当前元素的格子,栈顶只是维护了一个方向上所有 h h h 的最大值,与所求无关,这里要充分理解单调栈的性质。我第一次就是在这里错了

  2. 因为每一行的计算都与上一行相互独立,所以没有必要用二维数组存所以格子的 l , r , h l,r,h l,r,h ,只用存一行的,一行一行处理。更新 h h h 时直接在上一行得到的 h h h 基础上滚动覆盖即可(类似于dp的滚动数组优化),可以小小节省一些空间。

  3. 为方便使用,计算 l l l r r r 的单调栈中直接存元素编号,没有存实际值,但根据实际值的大小判断是否出栈。

  4. 考虑 n = m = 1000 n=m=1000 n=m=1000 且纸上没有任何画过的地方的极端情况长方形的数量为 n 2 ⋅ m 2 = 1 0 12 n^2\cdot m^2=10^{12} n2m2=1012 的数量级,会爆int,ans需要开 long long。


参考代码

#include<cstdio>
using namespace std;
const int N=1010;
int n,m,h[N],l[N],r[N],stk[N];
bool line[N];//当前行的每个格子是否空白(0表示被画过)
char a[N];
long long ans;
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
	{
		scanf("%s",a);
		for(int j=1;j<=m;j++)
		{
			line[j]=(a[j-1]=='.')?1:0;
			if(!line[j]) h[j]=0;
			else h[j]++;
		}
		int top=0;
		//注意 stk存元素编号而非值,因为l和r需要编号而非值 
		for(int j=m;j>=1;j--)
		{
			while(top>0&&h[j]<=h[stk[top]]) l[stk[top--]]=j;//只要当前值小于等于栈顶值就进行出栈操作,并记录 栈顶的值的r 是当前位置 注意先改再减,不然第一个改不掉 
			stk[++top]=j;//j入栈
		}
		while(top>0) l[stk[top--]]=0;//剩余在栈中的元素左侧没有障碍了,l设为0 注意先改再减,不然第一个改不掉 
		top=0;
		for(int j=1;j<=m;j++)
		{
			while(top>0&&h[j]<h[stk[top]]) r[stk[top--]]=j;
			stk[++top]=j;
		}
		while(top>0) r[stk[top--]]=m+1;
		for(int j=1;j<=m;j++) 
			ans+=(j-l[j])*(r[j]-j)*h[j];
	}
	printf("%lld",ans);
	return 0;
}
  • 8
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
这里分享一下自己的最小系统板,K60P144脚的,只要是LQFP封装的144脚K60都可以用 画这个的初衷是弄一块自己满意的板子,但是找遍了整个淘宝也没有让我满意的。像某宙、某外家的最小系统板都特别贵,一块K60FX512VLQ15的都得160,其实成本估计也就是80块钱顶天了,这破玩意也没啥技术含量(我自己买块K60 55块钱包邮,云某汉某芯城做个板子有活动14加一起70块,电容啥的社团都有),这让我非常不爽。某龙邱家还算良心,130就能买到最高型号芯片的最小系统。 如果仅仅是贵也就算了还有很多槽点: 一、丑,全都是长方形的,你就不能做一个正方形的出来??? 二、下载器贵,都得用Jlink,一个好几十,不同家的下载器还不一定兼容。本来最小系统就得140多,来个下载器还有邮费加一起200多了。这个跟第一点也是相辅相成的,因为他们的下载接口全都是Jtag,至少10个引脚。 三、外设少,绝大多数最小系统上除了一个LED灯啥都没有,少数有蓝牙或者是OLED的,基本没有两者都有的 于是乎根据网上的原理图,做了个最小系统,没想到第一次测试就成功了 自吹自擂有几个优点: 1、好看!!!这个是最重要的,标准的正方形。3D封装有点小问题,大家凑合看。 2、接口丰富,有OLED,有蓝牙(串口),平时调试足够用了。有个缺陷就是蓝牙跟显示屏不能同时使用,放不下。其实这个问题也不大,因为蓝牙一般是在跑车的时候传数据用的,OLED是调试时候用的,基本没有要一起用的时候。 3、下载器用的SWD协议,淘宝上一个SWD的下载器20搞定,而且高速下载时候更加稳定。 4、焊接时候完全不用原理图,所有的信息都用丝印层标注出来了 K60最小系统工程文件截图:

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值