题目解读
原题链接: 牛客网在线编程题 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;
}