有这样一道问题:
描述
将一个长和宽分别是整数w、h的矩形蛋糕,切成m块矩形小块打包走,分给自己的朋友(每块都必须是矩形、且长和宽均为整数)。
计算:最后得到的m块小蛋糕中,最大的那块蛋糕的面积下限。
假设w= 4,h= 4,m= 4,则下面的斩击可使得其中最大蛋糕块的面积最小。
假设w= 4,h= 4,m= 3,则下面的斩击可使得其中最大蛋糕块的面积最小:
输入
共有多行,每行表示一个测试案例。
每行是三个用空格分开的整数w, h, m ,其中1 ≤ w, h, m ≤ 20 , m ≤ wh.
当 w = h = m = 0 时不需要处理,表示输入结束。
输出
每个测试案例的结果占一行,输出一个整数,表示最大蛋糕块的面积下限。
对于该问题,和折腾了一个学期的石板切割问题非常像,看到这题,瞬间感觉PTSD,但这一题相较于石板切割还是较容易。
首先,研究DFS解法:
对于每一个问题都可以划分为更小的子问题解决,核心在于枚举所有可能的切割方式。可能的切割方式分为两种,竖切,左边宽c,右边宽w-c,高h,左边分配k块,右边分配n-k块,枚举k和c以及横切,上边高c,下边高h-c,宽w,上边分配k块,下边分配n-k块,枚举k和c。
为提高DFS速度,可以做备忘录优化,使用一个三维数组,将中间过程得到的子块结果保存下来,再第二次访问时,不需要再次递归,加快速度。
于是可以写出DFS解法代码:
// by andy
#include <algorithm>
#include <cmath>
#include <cstring>
#include <iostream>
using namespace std;
#define For(a, b, c) for (int a = b; a < c; ++a)
#define Clear(a, b) memset(a, b, sizeof(a))
const int Inf = 0x3f3f3f3f, Inf2 = 0x7fffffff;
int W, H, M; //宽、高、切成M块(只需要M-1斩)
int minMax[27][27][407]; // W,H最高是20,所以M最多斩成400块
int dfs(int w, int h, int cnt) {
if (w * h < cnt + 1) //不够斩
return Inf;
if (cnt == 0) //斩完毕
return w * h;
if (minMax[w][h][cnt] != -1) // Visited?
return minMax[w][h][cnt];
int minMArea = 0;
minMArea = Inf;
For(i, 1, w) //第一斩是竖斩,产生的各种状态
For(k, 0, cnt) { //枚举左右两半的各种切法,若左边切为k块,右边就是cnt-1-k;
int m1 = dfs(i, h, k); //搜索左侧的切法
int m2 = dfs(w - i, h, cnt - 1 - k); //搜索右侧的切法
minMArea = min(minMArea, max(m1, m2)); //取最小值
}
For(j, 1, h) //第一斩是横斩,产生的各种状态
For(k, 0, cnt) {
int r1 = dfs(w, j, k); //枚举上下两半的各种切法
int r2 = dfs(w, h - j, cnt - 1 - k);
minMArea = min(minMArea, max(r1, r2));
}
return minMax[w][h][cnt] = minMArea;
}
int main() {
while (true) {
cin >> W >> H >> M;
if (W == 0 && H == 0) break;
Clear(minMax, -1);
cout << dfs(W, H, M - 1) << endl;
}
return 0;
}
但是这一题让我DNA动了,因为对于石板切割问题,是存在动态规划解法的,因此,我试图探索本题的动态规划解法。
而且确实探索到了。
①状态转移方程
对于每一个大块的最优解,总是可以被划分为两个小块的最优解组合,即
f[i,j][k] = min(f[i,j][k], max(f[c,j][d], f[i-c,j][k-d]))
宽高为i,j分为k个蛋糕的最大面积的最小值 = 宽高c,j分为d个蛋糕和宽高i-c,j分为k-d个蛋糕的最大面积的最小值的较大者与当前面积取较小者
非常拗口,但可以如下理解:
分为两边d、k-d后,蛋糕的面积最大值的最小值必然不可能同时满足
此时的最大值的最小值就应该为左右两边最大值的最小值的较大者
再与当前所存的f[i,j][k]做比较,取较小者,最终保存下的就是最优解
②状态表格设计
存放:宽高为i,j分为k个蛋糕的最大面积的最小值 | 分为k个蛋糕+ |
[i,j] |
实现中,用三维数组表示[i,j];
③初始条件
当 k == 1 时,显然不需要切割,最大面积的最小值就等于当前面积;
填表方向:横向。
有了以上分析,就可以写出DP算法代码:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 25, M = 405;
int f[N][N][M];
int main()
{
for(int i = 1; i < N; i++)
for(int j = 1; j < N; j++)
f[i][j][1] = i * j;
for(int i = 2; i < M; i++)
for(int j = 1; j < N; j++)
for(int k = 1; k < N; k++)//对矩阵 j * k
if(j * k < i) {f[j][k][i] = 0x3f3f3f3f;continue;}
else
for(int c = 1; c < max(k, j); c++)
for(int d = 1; d < i; d++)
{
if(c < j)
if(!f[j][k][i])f[j][k][i] = max(f[c][k][d], f[j - c][k][i - d]);
else f[j][k][i] = min(f[j][k][i], max(f[c][k][d], f[j - c][k][i - d]));
if(c < k)
if(!f[j][k][i])f[j][k][i] = max(f[j][c][d], f[j][k - c][i - d]);
else f[j][k][i] = min(f[j][k][i], max(f[j][c][d], f[j][k - c][i - d]));
}
int w, h, m;
cin >> w >> h >> m;
while(w || h || m)
{
cout << f[w][h][m] << endl;
cin >> w >> h >> m;
}
}
两算法分析:
当输入数据量较小时,DFS算法由于不需要将DP表格中对应的所有情况完全算出,在实际执行时间上,会更快,这也是为什么我在oj上提交代码后,显示DP算法需要使用400+ms完成,而DFS算法仅需要20ms。
于是我尝试扩充了数据量,将每一次判题的数据规模提高到万级。
差别也就展示了出来:
对于DP算法,时间几乎没有变化,而对于DFS算法,就已经无法在规定时间内完成了,最终可能需要超过10分钟才能完成。
所以,虽然在本题的DP算法中,存在最高5层循环,且之所以能在oj规定的时间复杂度内完成,与所给蛋糕较小关系密切,但是,DP算法的时间是稳定的,无论测试组数的多少,几乎不会影响到总的时间,而DFS在较小的询问次数下能够较快得出答案,但在蛋糕大小不变,询问次数增加时,由于出现的情况越来越多,DFS算法时间会迅速增加。以至于在20大小的蛋糕满询问时,DP能在半秒内完成,而DFS却需要十分钟开外。可见,DFS和DP在效率上差别在大测试数据下还是非常明显的。