题目
Description
_AP神牛准备给自己盖一座很华丽的宫殿。于是,他看中了一块N*M的矩形空地。空地中每个格子都有自己的海拔高度。AP想让他的宫殿的平均海拔在海平面之上(假设海平面的高度是0,平均数都会算吧?)。而且,AP希望他的宫殿尽量大,能够容纳更多的人来膜拜他。请问AP的宫殿最后会有多大?
Input Format
第一行为N和M。之后N行,每行M个数,描述的空地的海拔。
Output Format
输出一行,表示宫殿最大面积。
_
Sample Input
3 2
4 0
-10 8
-2 -2
Sample Output
4
Data Limit
对于30%的数据,N,M≤50;
对于100%的数据,N,M≤200;
分析
思路是一步一步走向正解的
乍一看还以为是最大子矩阵的题,然而发现这个这个限制与一般最大子矩阵的题不同,是需要平均数大于0,这个限制并不是规则的。动规显然不可行。
看数据范围,正解应该是一个n3或n3logn的算法
因为所求为平均数大于0的最大子矩阵,这个限制于矩阵的和有关,为了简便运算,可以用前缀和,方便求出区间的和,直接判断和是否大于零即可。
前置知识:二维前缀和(大佬们可以无视这个)
sum[i][j]表示(1,1)到(i,j)这个矩阵的和;
则(a,b)(左上角)到(c,d)(右下角)的和怎么表示?
50分做法
暴力的贪心是O(n4)的复杂度。
两种思路,一枚举点,二枚举边;
枚举点:
枚举矩阵的右下角与左上角顶点。
加了一个小优化(然而并没有什么用),不过最坏还是会退化到O(n4);
这个优化并不能做到面积严格单调,但是可以大致单调;枚举坐标改为枚举坐标和,则枚举的顺序变成了斜线:
细节请看代码注释
注意要开longlong,二维前缀和注意细节
1 long long ans = 0; 2 for(int i = 1;i <= n; ++ i) 3 for(int j = 1;j <= m; ++ j)//前两维枚举右下顶点 4 for(int s = 2;s <= i + j; ++ s){//这一维是左上顶点的坐标和 5 //一个小优化,i - k + 1 + j - l + 1是矩形的长宽和 6 //如果当前答案已经大于了剩下的最大矩形,则不必再枚举 7 if(ans > ((double)i + j - s + 2) * (i + j - s + 2) / (double)4) break; 8 for(int l = max(s - i,1);l <= min(s - 1,j); ++ l){//左上顶点的纵坐标 9 int k = s - l;//横坐标 10 if(k < 1 || k > i) continue; 11 long long cur = sum[i][j] - sum[k - 1][j] - sum[i][l - 1] + sum[k - 1][l - 1]; 12 //二维前缀和,注意细节 13 if(cur > 0) ans = max(ans,((long long)i-k+1) * (j-l+1));//更新答案 14 } 15 } 16 17 put(ans);
枚举边:
其实与枚举点差不多,不过要转换思维,就是枚举长宽,相对于这道题来说,枚举边的方法更容易优化。
直接枚举长度显然不太现实,为了便于确定矩阵,我们枚举长度 +下界
1 long long ans = 0; 2 for(int i = n;i >= 1; -- i){//枚举长 3 if(ans > i * m) break;//剪枝 4 for(int k = m;k >= 1; -- k){//枚举宽 5 if(ans > i * k) break;//剪枝 6 long long s = i * k; 7 for(int j = i;j <= n;++ j){//枚举长的下界 8 int j1 = j - i + 1; 9 for(int l = k;l <= m; ++ l){//枚举宽的下界 10 int l1 = l - k + 1; 11 long long cur = sum[j][l] - sum[j1 - 1][l] - sum[j][l1 - 1] + sum[j1 - 1][l1 - 1]; 12 if(cur > 0) ans = max(ans,s); 13 } 14 } 15 } 16 } 17 put(ans);
其实还有一种优化就是像上面枚举点的那样枚举长宽之和,但是没什么太大的用处,优化不明显,且难以进一步优化;
100分做法
我们仔细观察枚举边的那一分代码,其实也是做了很多无用功的。
用倒序就是为了避免一些不必要的枚举。
如果我们能在O(1)或者O(log)的时间内求出矩形的长所对应的最大宽,这题就可以切了。
你想到了什么?二分(单调栈)
二分宽度,判断这个宽度(及以上)是否可行。
有了这个想法是不错的,但该如何实现呢?
先看主模块:
1 int ans = 0; 2 3 for(int i = n;i >= 1; -- i){//枚举长 4 if(ans > i * m) break;//剪枝 5 for(int j = i;j <= n; ++ j){//枚举长的下界 6 for(int k = 1;k <= m; ++ k){//预处理出k之前的宽起点为1的矩阵最小和 7 t[k] = sum[j][k] - sum[j-i][k]; 8 if(k > 1) t[k] = Min(t[k],t[k - 1]); 9 } 10 int cur = erfen(j - i + 1,j); 11 ans = Max(ans,cur); 12 } 13 } 14 15 put(ans);
这个t数组在check的时候有妙用,它表示在k之前的宽起点为1的最小矩阵和 ,注意起点是1
看check与二分部分:
1 bool check(int up,int down,int mid){ 2 for(int i = mid;i <= m; ++i){ 3 long long s = sum[down][i] - sum[up - 1][i]; 4 if(s - t[i - mid] > 0) return 1;//如果有宽度大于等于mid的矩和大于零 5 } 6 return 0; 7 } 8 9 int erfen(int up,int down){ 10 int l = 1,r = m,ans = 0; 11 while(l <= r){ 12 int mid = (l + r) >> 1; 13 if(check(up,down,mid)) ans = mid,l = mid + 1; 14 else r = mid - 1; 15 } 16 return ans * (down - up + 1); 17 }
在check函数中s是宽的起点为1,长度为i的矩阵的和,s - t[i - mid]便可以表示宽以i结尾,长度大于等于mid的矩阵的最大和,直接判断是否大于零,就可以返回了
(是不是很妙)
满分代码
1 /***************************** 2 User:Mandy.H.Y 3 Language:c++ 4 Problem:luogu1565 5 Algorithm: 6 *****************************/ 7 8 #include<bits/stdc++.h> 9 #define Max(x,y) ((x) > (y) ? (x) :(y)) 10 #define Min(x,y) ((x) < (y) ? (x) :(y)) 11 12 using namespace std; 13 14 const int maxn = 205; 15 16 int n,m; 17 long long t[maxn]; 18 long long sum[maxn][maxn]; 19 20 char *TT,*mo,but[(1 << 15) + 2]; 21 #define getchar() ((TT == mo && (mo = ((TT = but) + fread(but,1,1 << 15,stdin)),TT == mo)) ? -1 : *TT++) 22 template<class T>inline void read(T &x){ 23 x = 0;char ch = getchar();bool flag = 0; 24 while(!isdigit(ch)) flag |= ch == '-',ch = getchar(); 25 while(isdigit(ch)) x = (x << 1) + (x << 3) + (ch ^ 48),ch = getchar(); 26 if(flag) x = -x; 27 } 28 29 template<class T>void putch(const T x){ 30 if(x > 9) putch(x / 10); 31 putchar(x % 10 | 48); 32 } 33 34 template<class T>void put(const T x){ 35 if(x < 0) putchar('-'),putch(-x); 36 else putch(x); 37 } 38 39 void file(){ 40 freopen("cow.in","r",stdin); 41 // freopen("cow.out","w",stdout); 42 } 43 44 void readdata(){ 45 read(n);read(m); 46 for(int i = 1;i <= n; ++ i) 47 for(int j = 1;j <= m; ++ j){ 48 long long x;read(x); 49 sum[i][j] = sum[i - 1][j] + sum[i][j - 1] 50 - sum[i-1][j-1] + x; 51 } 52 } 53 54 bool check(int up,int down,int mid){ 55 for(int i = mid;i <= m; ++i){ 56 long long s = sum[down][i] - sum[up - 1][i]; 57 if(s - t[i - mid] > 0) return 1;//如果有宽度大于等于mid的矩和大于零 58 } 59 return 0; 60 } 61 62 int erfen(int up,int down){ 63 int l = 1,r = m,ans = 0; 64 while(l <= r){ 65 int mid = (l + r) >> 1; 66 if(check(up,down,mid)) ans = mid,l = mid + 1; 67 else r = mid - 1; 68 } 69 return ans * (down - up + 1); 70 } 71 72 void work(){ 73 int ans = 0; 74 75 for(int i = n;i >= 1; -- i){//枚举长 76 if(ans > i * m) break;//剪枝 77 for(int j = i;j <= n; ++ j){//枚举长的下界 78 for(int k = 1;k <= m; ++ k){//预处理出k之前的宽起点为1的矩阵最小和 79 t[k] = sum[j][k] - sum[j-i][k]; 80 if(k > 1) t[k] = Min(t[k],t[k - 1]); 81 } 82 int cur = erfen(j - i + 1,j); 83 ans = Max(ans,cur); 84 } 85 } 86 87 put(ans); 88 89 } 90 91 int main(){ 92 // file(); 93 readdata(); 94 work(); 95 return 0; 96 }
至此,这份题解算是结束了,但是重要的并不是正解本身,而是得到正解的这个思维过程