这题好几天以前就看了,不过一直有一些细节想不通就没写下来。。
首先发现棋盘矩阵上横纵坐标之和的奇偶性不同的点都是相反的,所以首先把横纵坐标之和为奇(或者是偶,这都不重要)的点取反,这样任务就变成了求一个最大全0或1的子矩阵。。
先考虑一维的情况,h[i]表示以i为终点的最长连续0的长度,有h[i]=a[i]==0? h[i-1]+1:0,这样可以O(n)轻松求出。。
拓展到高维,首先同样按照一维的方法,h[i][j]表示第i行以j为终点的最长连续0的长度,预处理出h[]。。接下来考虑一列一列来更新答案,对于单独的一列i,若以h[i][j]为子矩阵的一个边长,则它能往上下扩展的最大长度len就是另一个边长,所谓扩展就是向一个方向扫描知道碰到h值比自己小位置。。
如上图,第3行最多能向上扩展2,向下扩展1,len=4。。
可是暴力去扩展是O(n)的,总复杂度就会变成O(n^3),显然不行。。于是要用一种叫做单调栈的东西。。在这里栈中元素有2个域,一是压入时这个点对应的h,二是这个点向上能扩展到的最小下标i,h域在这个栈里是单调递增的。。从上往下依次将每个点压入栈中,若栈顶的h<=要压入的h,那么直接压入,否则将栈顶弹出,并计算以栈顶的h为一个边长的最大子矩阵面积。。不妨设当前要压入的点的行标为j,栈顶要弹出的点的最小下标域为i,那么显然i就是栈顶的点能向上扩展到的最大长度,又因为栈是单调的,所以[i,j-1]上所有点的h一定都<=栈顶的h(否则栈顶元素之前会被弹出),那么栈顶能向下扩展到的行标就是j-1了,所以以栈顶的h为一个边长的最大子矩阵面积就为h*(i-j),正方形的话只要取一下min就行了。。这样一直将栈顶弹出并更新答案,直到栈顶的h<=要压入的h或栈为空。。
还有最关键的一步,在最后要压入一个h为0的元素,以保证所有的点都能出栈。。由于所有的点都要出栈一次,而出栈的时候就会被更新,所以最优解一定会被取到。。
对于这题最大全0全1都要做一遍。。
还有个悬线法,我看的有点晕,就不写了。。
#include<cstdio>
#include<iostream>
#include<memory.h>
#define N 2005
#define clr(a) memset(a,0,sizeof(a))
using namespace std;
struct node{
int xu,h;
node(){}
node(int xu,int h):xu(xu),h(h){}
}st[N];
int n,m,i,j,ans1=0,ans2=0,top,a[N][N],h[N][N];
void push(int i,int h)
{
int now=i;
while (top&&st[top].h>h)
{
ans1=max(ans1,min(i-st[top].xu,st[top].h)*min(i-st[top].xu,st[top].h));
ans2=max(ans2,(i-st[top].xu)*st[top].h);
now=st[top--].xu;
}
st[++top]=node(now,h);
}
void cal()
{
int i,j;
for (i=1;i<=n;i++)
for (j=1,h[i][0]=0;j<=m;j++)
h[i][j]=a[i][j]? h[i][j-1]+1:0;
for (j=1;j<=m;j++)
{
top=0;
for (i=1;i<=n;i++)
push(i,h[i][j]);
push(n+1,0);
}
}
int main()
{
scanf("%d%d",&n,&m);
for (i=1;i<=n;i++)
for (j=1;j<=m;j++)
{
scanf("%d",&a[i][j]);
if ((i+j)%2) a[i][j]^=1;
}
cal();
for (i=1;i<=n;i++)
for (j=1;j<=m;j++)
a[i][j]^=1;
cal();
printf("%d\n%d",ans1,ans2);
}