• luogu1565 牛宫


    题目

    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 }

    至此,这份题解算是结束了,但是重要的并不是正解本身,而是得到正解的这个思维过程

    非做顽石不可,哪管他敬仰暗唾
  • 相关阅读:
    vue检查用户名是否重复
    后端注册接口完善
    django添加检查用户名和手机号数量接口
    Vue联调,图片及短信验证码
    swift webView 提出这样的要求你能忍吗?
    iOS 如何给Xcode7项目添加“.pch”文件
    swift 定制自己的Button样式
    Swift 为你的webView定制标题
    swift 如何获取webView的内容高度
    如何在MAC上使用SVN,简单几行命令搞定
  • 原文地址:https://www.cnblogs.com/Mandy-H-Y/p/11355861.html
Copyright © 2020-2023  润新知