动态规划题解 D001 分田地

题目解读

原题链接: 牛客网在线编程题 2017年校招专题

题目描述
牛牛和 15 个朋友来玩打土豪分田地的游戏,牛牛决定让你来分田地,地主的田地可以看成是一个矩形,每个位置有一个价值。分割田地的方法是横竖各切三刀,分成 16 份,作为领导干部,牛牛总是会选择其中总价值最小的一份田地, 作为牛牛最好的朋友,你希望牛牛取得的田地的价值和尽可能大,你知道这个值最大可以是多少吗?

输入描述:
每个输入包含 1 个测试用例。每个测试用例的第一行包含两个整数 n 和 m(1 <= n, m <= 75),表示田地的大小,接下来的 n 行,每行包含 m 个 0-9 之间的数字,表示每块位置的价值

输出描述:
输出一行表示牛牛所能取得的最大的价值。

示例1
输入
4 4
3332
3233
3332
2323

输出
2

题意理解

一句话说明,给出一个矩阵,将这个矩阵切割成16个小矩形(横向切三次,纵向切三次),得到的目标是16个小矩形中值最小的一个矩形,求在不同切法中这个最小矩形的最大值。注意示例中给出的是一个方阵,但事实上矩阵不一定是长宽相等的。

算法分析

这是一个较为典型的最小值里求最大的问题。初看确实没有什么思路,最容易的想法当然是暴力搜索,但很显然会超时,时间复杂度达到O(n^6);这个时候,当正向找不到思路的时候,我们开始考虑反向的想法:不管最小矩阵的值是多少,它始终是存在一个范围的,即介于0-sum[n][m]。这里的sum[n][m]是指以(0,0)为矩阵左上角坐标,(n,m)为矩阵右下角坐标的矩阵值,也即最大的矩阵的值之和。那么,我们是不是可以猜测一个值value,使得value处理0-sum[n][m]内,这个value值就代表了我们能够得到的最小矩阵的最大值。在这个基础上,问题就转换为了能不能找到这样一种切法,使得切出的所有子矩阵的值都大于等于value。
由上面的分析可以看到,整个问题可以分解为两个阶段:
第一阶段:确定value值
value值的确定相当于是在0-sum[n][m]这个区间内进行搜索。通过遍历进行搜索会出现超时错误,因此想到进行二分查找。isValid()函数的作用是判断在当前这个value值的情况下是否能找到合适的切法。设置L初始为0,R初始为sum[n][m],mid为(L+R)/2。有一个合理的理解为,当value值足够小时,肯定能找到合适的切法,比如极端情况下,当设置value=0时,不管怎么切都是可以的。因此isValid()为假只可能出现在value值偏大的情况下。在这种情况下,二分函数的写法可以写为:

    while(l<=r){
        mid = (l+r)/2;
        if(isValid(mid)){
            l = mid+1 ;
            ans = mid ;
        }
        else{
            r = mid-1 ;
        }
    }

第二阶段:确定在当前value值下是否有合适的切法
在value值下寻找切割方法较为暴力,即首先利用遍历的方法寻找行的三个切割点,再在三个切割点的情况下,选择遍历列,遍历列的时候采用了贪心的思想。即当找到第一个可行的切割点时,就从这个切割点进行切割,以此向后类推:
设置cnt来记录某一列是否符合条件;对于每一次的列切割结束之后,我们需要统计,如果cnt的值大于等于4,则直接返回true。当遍历结束所有的情况后仍然没有返回true后,即返回false。
注意这里为什么将cnt的值设置为4,这是因为需要划分为16块,且在行上切三次,列上切三次,切三次意味着列被分为4列,故cnt的值被设置为4。

疑问:为什么这个地方对列进行处理的时候可以使用贪心算法?这样会导致比如说某一类的情况被忽略掉吗?这个问题我还没有想明白。

代码:

#include<iostream>
#include<stdio.h>
#include<string.h>

using namespace std ;

int n,m ;            //n代表行、m代表列
int a[100][100];     //记录矩阵
int sum[100][100];   //记录矩阵中各个子矩阵的和

//给定左上角和右下角的坐标值,计算这个矩阵内的所有元素的值
int calc(int x1,int y1,int x2,int y2)
{
    return (sum[x2][y2]-sum[x2][y1]-sum[x1][y2]+sum[x1][y1]);
}

int isValid(int k)
{
    for(int x1=1;x1<=n-3;x1++){
        for(int x2=x1+1;x2<=n-2;x2++){
            for(int x3=x2+1;x3<=n-1;x3++){
                int cnt = 0; //用来进行计数的变量
                int rec = 0;
                for(int y=1;y<=m;y++){
                    if(calc(0,rec,x1,y)>=k && calc(x1,rec,x2,y)>=k && calc(x2,rec,x3,y)>=k && calc(x3,rec,n,y)>=k){
                        cnt++;
                        rec = y;
                    }
                }
                if(cnt>=4){
                    return 1 ;
                }
            }
        }
    }
    return 0 ;
}

int main()
{
    scanf("%d%d",&n,&m);
    char input[100];
    //由于输入过程中数字之间没有空格,因此采用字符串的形式读入
    for(int i=1;i<=n;i++){
        scanf("%s",input+1);
        for(int j=1;j<=m;j++){
            a[i][j] = input[j]-'0';
        }
    }
    //sum矩阵的初始化
    memset(sum,0,sizeof(sum)) ;
    //计算各个小矩阵的值
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            sum[i][j]=sum[i][j-1]+sum[i-1][j]-sum[i-1][j-1]+a[i][j];
        }
    }
    int l = 0;
    int r = sum[n][m];
    int mid;
    int ans;
    //采用二分查找进行搜索,关键的核心在于isValid()函数
    while(l<=r){
        mid = (l+r)/2;
        if(isValid(mid)){
            l = mid+1 ;
            ans = mid ;
        }
        else{
            r = mid-1 ;
        }
    }
    printf("%d\n",ans);
    return 0;
}

猜你喜欢

转载自blog.csdn.net/ComeTender/article/details/81384946