题解 P1565 【牛宫】

· · 题解

同步于我的blog

题目

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,这个限制并不是规则的。动规显然不可行。

看数据范围,正解应该是一个n^3或n^3logn的算法

因为所求为平均数大于0的最大子矩阵,这个限制于矩阵的和有关,为了简便运算,可以用前缀和,方便求出区间的和,直接判断和是否大于零即可。

前置知识:二维前缀和(大佬们可以无视这个)

则(a,b)(左上角)到(c,d)(右下角)的和怎么表示? $sum[c][d] - sum[c][b-1] - sum[a-1][d]+sum[a-1][b-1]

50分做法

暴力的贪心是O(n^4)的复杂度。

两种思路,一枚举点,二枚举边;

枚举点:

枚举矩阵的右下角与左上角顶点。

加了一个小优化(然而并没有什么用),不过最坏还是会退化到O(n^4);

这个优化并不能做到面积严格单调,但是可以大致单调;枚举坐标改为枚举坐标和,则枚举的顺序变成了斜线:

细节请看代码注释

注意要开longlong,二维前缀和注意细节

long long ans = 0;
    for(int i = 1;i <= n; ++ i)
    for(int j = 1;j <= m; ++ j)//前两维枚举右下顶点 
        for(int s = 2;s <= i + j; ++ s){//这一维是左上顶点的坐标和 
        //一个小优化,i - k + 1 + j - l + 1是矩形的长宽和
        //如果当前答案已经大于了剩下的最大矩形,则不必再枚举 
            if(ans > ((double)i + j - s + 2) * (i + j - s + 2) / (double)4) break;
            for(int l = max(s - i,1);l <= min(s - 1,j); ++ l){//左上顶点的纵坐标 
                int k = s - l;//横坐标 
                if(k < 1 || k > i) continue; 
                long long cur = sum[i][j] - sum[k - 1][j] - sum[i][l - 1] + sum[k - 1][l - 1];
                //二维前缀和,注意细节 
                if(cur > 0) ans = max(ans,((long long)i-k+1) * (j-l+1));//更新答案 
            }
        }

    put(ans);

枚举边:

其实与枚举点差不多,不过要转换思维,就是枚举长宽,相对于这道题来说,枚举边的方法更容易优化。

直接枚举长度显然不太现实,为了便于确定矩阵,我们枚举长度 +下界

long long ans = 0;
    for(int i = n;i >= 1; -- i){//枚举长 
        if(ans > i * m) break;//剪枝 
        for(int k = m;k >= 1; -- k){//枚举宽 
            if(ans > i * k) break;//剪枝 
            long long s = i * k;
            for(int j = i;j <= n;++ j){//枚举长的下界 
                int j1 = j - i + 1;
                for(int l = k;l <= m; ++ l){//枚举宽的下界 
                    int l1 = l - k + 1;
                    long long cur = sum[j][l] - sum[j1 - 1][l] - sum[j][l1 - 1] + sum[j1 - 1][l1 - 1];
                    if(cur > 0) ans = max(ans,s);
                }
            }
        } 
    }
    put(ans);

其实还有一种优化就是像上面枚举点的那样枚举长宽之和,但是没什么太大的用处,优化不明显,且难以进一步优化;

100分做法

我们仔细观察枚举边的那一分代码,其实也是做了很多无用功的。

用倒序就是为了避免一些不必要的枚举。

如果我们能在O(1)或者O(logn)的时间内求出矩形的长所对应的最大宽,这题就可以切了。

你想到了什么?二分,单调栈

二分宽度,判断这个宽度(及以上)是否可行。

有了这个想法是不错的,但该如何实现呢?

先看主模块:

int ans = 0;

    for(int i = n;i >= 1; -- i){//枚举长 
        if(ans > i * m) break;//剪枝 
        for(int j = i;j <= n; ++ j){//枚举长的下界 
            for(int k = 1;k <= m; ++ k){//预处理出k之前的宽起点为1的矩阵最小和 
                t[k] = sum[j][k] - sum[j-i][k];
                if(k > 1) t[k] = Min(t[k],t[k - 1]);
            }
            int cur = erfen(j - i + 1,j);
            ans = Max(ans,cur);
        }
    }

    put(ans);

这个t数组在check的时候有妙用,它表示在k之前的宽起点为1的最小矩阵和 ,注意起点是1

看check与二分部分:

bool check(int up,int down,int mid){
    for(int i = mid;i <= m; ++i){
        long long s = sum[down][i] - sum[up - 1][i];
        if(s - t[i - mid] > 0) return 1;//如果有宽度大于等于mid的矩和大于零 
    }
    return 0;
}

int erfen(int up,int down){
    int l = 1,r = m,ans = 0;
    while(l <= r){
        int mid = (l + r) >> 1;
        if(check(up,down,mid)) ans = mid,l = mid + 1;
        else r = mid - 1;
    }
    return ans * (down - up + 1);
}

在check函数中s是宽的起点为1,长度为i的矩阵的和,s - t[i - mid]便可以表示宽以i结尾,长度大于等于mid的矩阵的最大和,直接判断是否大于零,就可以返回了

(是不是很妙)

满分代码

/*****************************
User:Mandy.H.Y
Language:c++
Problem:luogu1565
Algorithm: 
*****************************/

#include<bits/stdc++.h>
#define Max(x,y) ((x) > (y) ? (x) :(y))
#define Min(x,y) ((x) < (y) ? (x) :(y))

using namespace std;

const int maxn = 205;

int n,m;
long long t[maxn]; 
long long sum[maxn][maxn];

char *TT,*mo,but[(1 << 15) + 2];
#define getchar() ((TT == mo && (mo = ((TT = but) + fread(but,1,1 << 15,stdin)),TT == mo)) ? -1 : *TT++)
template<class T>inline void read(T &x){
    x = 0;char ch = getchar();bool flag = 0;
    while(!isdigit(ch)) flag |= ch == '-',ch = getchar(); 
    while(isdigit(ch)) x = (x << 1) + (x << 3) + (ch ^ 48),ch = getchar();
    if(flag) x = -x; 
}

template<class T>void putch(const T x){
    if(x > 9) putch(x / 10);
    putchar(x % 10 | 48);
}

template<class T>void put(const T x){
    if(x < 0) putchar('-'),putch(-x);
    else putch(x);
}

void file(){
    freopen("cow.in","r",stdin);
//    freopen("cow.out","w",stdout);
}

void readdata(){
    read(n);read(m);
    for(int i = 1;i <= n; ++ i)
        for(int j = 1;j <= m; ++ j){
            long long x;read(x);
            sum[i][j] = sum[i - 1][j] + sum[i][j - 1] 
                      - sum[i-1][j-1] + x;
        }
}

bool check(int up,int down,int mid){
    for(int i = mid;i <= m; ++i){
        long long s = sum[down][i] - sum[up - 1][i];
        if(s - t[i - mid] > 0) return 1;//如果有宽度大于等于mid的矩和大于零 
    }
    return 0;
}

int erfen(int up,int down){
    int l = 1,r = m,ans = 0;
    while(l <= r){
        int mid = (l + r) >> 1;
        if(check(up,down,mid)) ans = mid,l = mid + 1;
        else r = mid - 1;
    }
    return ans * (down - up + 1);
}

void work(){
    int ans = 0;

    for(int i = n;i >= 1; -- i){//枚举长 
        if(ans > i * m) break;//剪枝 
        for(int j = i;j <= n; ++ j){//枚举长的下界 
            for(int k = 1;k <= m; ++ k){//预处理出k之前的宽起点为1的矩阵最小和 
                t[k] = sum[j][k] - sum[j-i][k];
                if(k > 1) t[k] = Min(t[k],t[k - 1]);
            }
            int cur = erfen(j - i + 1,j);
            ans = Max(ans,cur);
        }
    }

    put(ans);

} 

int main(){
//    file();
    readdata();
    work();
    return 0;
}

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