動態規劃題解 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;
}