分田地
题目描述
牛牛和 15 个朋友来玩打土豪分田地的游戏,牛牛决定让你来分田地,地主的田地可以看成是一个矩形,每个位置有一个价值。分割田地的方法是横竖各切三刀,分成 16 份,作为领导干部,牛牛总是会选择其中总价值最小的一份田地, 作为牛牛最好的朋友,你希望牛牛取得的田地的价值和尽可能大,你知道这个值最大可以是多少吗?
输入描述:
每个输入包含 1 个测试用例。每个测试用例的第一行包含两个整数 n 和 m(1 <= n, m <= 75),表示田地的大小,接下来的 n 行,每行包含 m 个 0-9 之间的数字,表示每块位置的价值。
输出描述:
输出一行表示牛牛所能取得的最大的价值。
示例1
输入
复制
4 4 3332 3233 3332 2323
输出
复制
2
算法思路
难点:
暴力化简:枚举+二分,纯暴力会导致时间超限。
理解题意:将我们的矩阵划分成16部分,对于其中的划分矩阵a,求解一个矩阵a,使得在所有的枚举 a1,a2,a2....an中,ai的划分的16部分中的最小部分min,在所有的min(a1.....an)中是最大的。也就是说我们要找到在所有划分中权值最小的部分中的最大值,即max(min(a[0]),min(a[1]),....min(a[k]))。
此题目属于暴力美学中的化简,枚举+二分。
纯暴力:如果我们采用纯暴力的方式,对于每一刀都进行枚举,那么时间复杂度将达到 O(n^3m^3),很明显如此高的复杂度会导致时间超限。
枚举+二分:从宏观的角度看,令我们所有土地的权值和为r,那么我们的最小值resualt一定在(0,r)的闭区间之中,原因是我们不可能找到一个划分,使得其中的一块土地等于0或者说土地权值=r。有了如上思路,那么我们就可以来进行求解了。
1、首先我们来获得sum[i][j],代表从整个田地的左上角,到以land[i][j]土地为右下角的矩形土地权值之和。
2、接下来我们对 0 - sum[n][m]值来进行二分查找,寻找我们满足条件的最大mid。
3、如果说我们的mid为我们所找的土地和,那么根据题意,在划分的16个部分中,我们的mid一定是最小的,如果存在一部分要小于我们的mid,那么我们的这种切法是不满足该条件的,应该重新来切。
4、为了减少复杂度,我们枚举所有的竖切,然后来进行我们的横切,每切一刀我们判断产生的四个矩阵的权值与mid的关系,如果满足均大于或等于我们的mid,那么我们在该基础上,在该横row的下放再寻找横切。
5、当我们找到了横切的四刀满足了16部分都大于等于我们的mid时,证明我们找到了一种切法,该切法一定满足存在一个划分,使得最小值大于或等于mid,于是我们返回true,更新我们的l边界值,重复我们的3,4。如果没有,则证明不存在一个划分,使得最小的一部分全部大于或等于mid,那么我们需要减小我们的mid值,重新进行查找。
6、最后我们输出我们的resualt值即可。
解题代码
import java.util.Scanner;
/**
* 牛客网-划分田地
* 枚举+二分查找
* @author yangjieyu
*
*/
public class Main {
//土地
static int[][] lands=new int[76][76];
static int[][] sum=new int[76][76];
static int n;
static int m;
public static void main(String[] args) {
Scanner scanner=new Scanner(System.in);
n=scanner.nextInt();
m=scanner.nextInt();
for(int i=1;i<=n;i++){
String line=scanner.next();
for(int j=1;j<=m;j++){
lands[i][j]=line.charAt(j-1)-'0';
//从 左上角sum[0][0]到sum[i][j]的土地价值之和
sum[i][j]=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+lands[i][j];
}
}
int result=0;
// r是土地总和
int l=0,r=sum[n][m];
//二分查找
while(l<=r) {
int mid = (r+l)>>1;
if(judge(mid)) {
//能够找到一个土地使得mid是最小值
//增大mid的值继续查找
result = mid;
l = mid+1;
}else {
//不能找到一个土地是得mid不是最小值
//做查找mid
r = mid -1;
}
}
System.out.println(result);
scanner.close();
}
public static boolean judge(int mid){
//遍历我们的竖切
for(int i = 1 ;i<=m-3;i++) {
for(int j = i+1;j<=m-2;j++) {
for(int k = j+1;k<=m-1;k++) {
//每一刀切的行数的前一刀的位置,初始刀位置为0,也就是我们的边界
int startRow = 0;
//横切的刀数
int count = 0;
for(int r = 1;r<=n;r++) {
int s1 = getCol(r, i, startRow, 0);
int s2 = getCol(r, j, startRow, i);
int s3 = getCol(r, k, startRow, j);
int s4 = getCol(r, m, startRow, k);
//最大值中的最小值
if(s1>=mid&&s2>=mid&&s3>=mid&&s4>=mid) {
startRow = r;
count++;
}
}
//如果该mid土地情况下找到了我们的最小值,则可以直接返回,不需要再遍历了
if(count>=4) {
return true;
}
}
}
}
//不存在所有的划分中土地小于mid,我们需要减小mid的值
return false;
}
//获得划分土地的大小
public static int getCol(int x ,int y,int i,int j) {
return sum[x][y] - sum[x][j]-sum[i][y]+sum[i][j];
}
}