[BZOJ1057][ZJOI2007]棋盘制作 (单调栈/悬线法)

这道题是很久很很久以前 jrfdl 考的,今天我猛然看到了我的代码,便水个题解。

题目描述

小Q找到了一张由N*M个正方形的格子组成的矩形纸片,每个格子被涂有黑白两种颜色之一。小Q想在这种纸中裁减一部分作为新棋盘,当然,他希望这个棋盘尽可能的大。
不过小Q还没有决定是找 一个正方形的棋盘还是一个矩形的棋盘(当然,不管哪种,棋盘必须都黑白相间,即相邻的格子不同色),所以他希望可以找到最大的正方形棋盘面积和最大的矩形棋盘面积,从而决定哪个更好一些。于是小Q找到了即将参加全国信息学竞赛的你,你能帮助他么?

输入描述:
第一行包含两个整数N和M,分别表示矩形纸片的长和宽。
接下来的N行包含一个N * M的01矩阵,表示这张矩形纸片的颜色(0表示白色,1表示黑色)。

输出描述:
包含两行,每行包含一个整数。
第一行为可以找到的最大正方形棋盘的面积,
第二行为可以找到的最大矩形棋盘的面积(注意正方形和矩形是可以相交或者包含的)。


解析

首先,棋盘是黑白相间的,所以将 (行数+列数)为奇数的格子反色,就转换成了求最大全0或全1矩阵,这就简化了问题。接下来怎么做呢?网上有2种做法:单调栈和悬线法。单调栈法比较常见,我们最可能想到这种方法,看一下可以知道细节该怎么写;悬线法我第一次见到,大开眼界,也觉得非常有道理,值得学习。

这两种方法,一个循环是 rep(i,1,m) rep(j,1,n),一个是 rep(i,1,n) rep(i,1,m),时间复杂度都是 O(nm),但悬线法常数小,比单调栈更快。

1.单调栈

递推,单调栈,思维很巧
用 ri[i][j] 记录在 (i,j) 位置它前面连同它最多有几个连续的 1 ,对于每一列用一个单调不下降栈,记录边长,每一次弹出边时,更新最大正方形面积和矩形面积 ,这样就求出了最大 1 子矩阵 。
细节不多,看代码

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

const int N=2010, M=2010;
int n,m;
int a[N][M],ri[N][M];
int ans1,ans2;
int top, stack[N], up[N];

inline void getR(){
    int i,j;
    for(i=1; i<=n; ++i){
        for(int j=1; j<=m; ++j){
            if(a[i][j]) ri[i][j]=ri[i][j-1]+1;
            else ri[i][j]=0;
        }
    }
} 

inline void getans(){
    int i,j,to,lin;
    for(j=1; j<=m; ++j){
        top=0;
        for(i=1; i<=n; ++i){
            to = i;
            while(top>0 && stack[top]>ri[i][j]){
                lin = min(stack[top], i-up[top]);
                ans1 = max(ans1,lin*lin);                   // 更新答案 
                ans2 = max(ans2, stack[top]*(i-up[top]));   
                to = min(to, up[top]);                      // 更新当前这一元素的极大全 1 矩阵,up是这一元素最多能往上延伸多少行  
                top--;
            }
            stack[++top] = ri[i][j]; up[top]=to;
        }
        // 清空栈
        while(top>0 && stack[top]>ri[i][j]){
	    	lin = min(stack[top], i-up[top]);
	        ans1 = max(ans1,lin*lin);                   // 更新答案 
	        ans2 = max(ans2, stack[top]*(i-up[top]));   
	        to = min(to, up[top]);                      // 更新当前这一元素的极大全 1 矩阵,up是这一元素最多能往上延伸多少行  
	        top--;
	    }
    }
}

int main(){
    int i,j;
    scanf("%d%d",&n,&m);
    for(i=1; i<=n; ++i){
        for(j=1; j<=m; ++j){
            scanf("%d",&a[i][j]);
        }
    }
    // 改变棋盘,把行数+列数为奇数或偶数的位置异或  多谢 dcx%dl 提醒 
    for(i=1; i<=n; ++i){
        for(j=1; j<=m; ++j){
            if((i+j)&1) a[i][j]^=1; 
        }
    }
    // 求一下全 1 子矩阵,求一下全 0 子矩阵 
    getR(); getans();
    for(i=1; i<=n; ++i){
        for(j=1; j<=m; ++j){
            a[i][j] = !a[i][j];
        }
    }
    getR(); getans();
    printf("%d\n%d\n",ans1,ans2);
    return 0;
}

2.悬线法

悬线法,思维巧妙
想象出一个帘子,他有他能达到的最长距离 up,最右端 topr,最左端 topl,每次算一下最大全 0 子矩阵

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

const int N=2010, M=2010;
int n,m;
int a[N][M],ri[N][M];
int ans1,ans2;
int topl[M],topr[M],up[M];

inline void getans(){
	memset(up,0,sizeof(up));
	int i,j,nowl,nowr,lin;
	for(j=1; j<=m; ++j){ topl[j]=1; topr[j]=m; }
	for(i=1; i<=n; ++i){ 	// 一行一行地刷新帘子  
		nowl=0; nowr=m+1;
		for(j=1; j<=m; ++j){	// 从左到右更新左端点  
			if(a[i][j]){
				up[j]=0;
				topl[j]=1;
				nowl = j;
			}else{
				up[j]++;
				topl[j] = max(topl[j], nowl+1);
			}
		}
		for(j=m; j>=1; --j){	// 从右到左更新右端点  
			if(a[i][j]){
				topr[j]=m;
				nowr=j;
			}else{
				topr[j] = min(topr[j], nowr-1);
				// 更新答案  
				lin = min(up[j], topr[j]-topl[j]+1);
				ans1 = max(ans1, lin*lin);
				ans2 = max(ans2, up[j]*(topr[j]-topl[j]+1));
			}
		}
	}
}

int main(){
	int i,j;
	scanf("%d%d",&n,&m);
	for(i=1; i<=n; ++i){
		for(j=1; j<=m; ++j){
			scanf("%d",&a[i][j]);
		}
	}
	// 改变棋盘,把行数+列数为奇数或偶数的位置异或  多谢 dcx%dl 提醒 
	for(i=1; i<=n; ++i){
		for(j=1; j<=m; ++j){
			if((i+j)&1) a[i][j]^=1; 
		}
	}
	// 求一下全 1 子矩阵,求一下全 0 子矩阵 
	getans();
	for(i=1; i<=n; ++i){
		for(j=1; j<=m; ++j){
			a[i][j] = !a[i][j];
		}
	}
	getans();
	printf("%d\n%d\n",ans1,ans2);
	return 0;
}

在这里可以学习悬线法


感谢评论区的指正,现已更改。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值