算法:dfs和动规在效率上差别有多大?

有这样一道问题:

描述

将一个长和宽分别是整数w、h的矩形蛋糕,切成m块矩形小块打包走,分给自己的朋友(每块都必须是矩形、且长和宽均为整数)。

计算:最后得到的m块小蛋糕中,最大的那块蛋糕的面积下限。

假设w= 4,h= 4,m= 4,则下面的斩击可使得其中最大蛋糕块的面积最小。

outside_default.png

假设w= 4,h= 4,m= 3,则下面的斩击可使得其中最大蛋糕块的面积最小:

outside_default.png

输入 

共有多行,每行表示一个测试案例。

每行是三个用空格分开的整数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。

outside_default.png

于是我尝试扩充了数据量,将每一次判题的数据规模提高到万级。

差别也就展示了出来:

对于DP算法,时间几乎没有变化,而对于DFS算法,就已经无法在规定时间内完成了,最终可能需要超过10分钟才能完成。

outside_default.png

所以,虽然在本题的DP算法中,存在最高5层循环,且之所以能在oj规定的时间复杂度内完成,与所给蛋糕较小关系密切,但是,DP算法的时间是稳定的,无论测试组数的多少,几乎不会影响到总的时间,而DFS在较小的询问次数下能够较快得出答案,但在蛋糕大小不变,询问次数增加时,由于出现的情况越来越多,DFS算法时间会迅速增加。以至于在20大小的蛋糕满询问时,DP能在半秒内完成,而DFS却需要十分钟开外。可见,DFS和DP在效率上差别在大测试数据下还是非常明显的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值