在我的洛谷博客阅读
原题传送门(洛谷 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)⋅(i−l[i])。
如果计算到的格子是被涂过的,则它的 h = 0 h=0 h=0 ,算出的矩形数量也是 0 0 0 ,累加被涂过的格子时时自动忽略了这种情况。
Tips
-
计算 l l l 和 r r r 时不能用栈顶元素更新当前元素 i i i 的 l l l ,必须出栈时才反过来用当前元素更新栈顶出栈了的元素的 l l l ,因为前者无法保证栈顶元素一定是左边或右边第一个 h h h 小于(等于)当前元素的格子,栈顶只是维护了一个方向上所有 h h h 的最大值,与所求无关,这里要充分理解单调栈的性质。
我第一次就是在这里错了 -
因为每一行的计算都与上一行相互独立,所以没有必要用二维数组存所以格子的 l , r , h l,r,h l,r,h ,只用存一行的,一行一行处理。更新 h h h 时直接在上一行得到的 h h h 基础上滚动覆盖即可(类似于dp的滚动数组优化),可以小小节省一些空间。
-
为方便使用,计算 l l l 和 r r r 的单调栈中直接存元素编号,没有存实际值,但根据实际值的大小判断是否出栈。
-
考虑 n = m = 1000 n=m=1000 n=m=1000 且纸上没有任何画过的地方的极端情况长方形的数量为 n 2 ⋅ m 2 = 1 0 12 n^2\cdot m^2=10^{12} n2⋅m2=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; }
今天的文章
洛谷题目难度对应的颜色_洛谷题目难度对应的颜色分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/81078.html