动态规划 钢条切割

动态规划 钢条切割

《算法导论》第15章讲动态规划,写着属于高级设计和分析技术内容。

前言

动态规划和分治像而不同,像体现在都是组合子问题的解来求解原问题,不同体现在:分治是原问题分解为互不相交的子问题(可以回想归并排序和快速排序过程),分别递归地求解子问题,再将子问题的解组合起来,求出原问题的解;动态规划中分解原问题是分解为子问题重叠的情况,即不同的子问题具有公共的子子问题,这种情况,分治算法会重复计算公共子问题的解,做了很多不必要的工作,而动态规划是对每个子问题只求解一次,将其解保存在一个表格中,从而不需要每次求解子问题都重新计算,避免了这种不必要的计算工作。

通常按如下4个步骤来设计一个动态规划算法:

  1. 刻画一个最优解的结构特征;
  2. 递归地定义最优解的值;
  3. 计算最优解的值,通常采用自底向上的方法;
  4. 利用计算出的信息构造一个最优解。

如果我们仅仅求一个最优解的值,而非解本身,可以忽略步骤4;如果是求解本身,需要执行步骤4,同时需要再步骤3中维护一些额外信息,以便来构造最优解。

最优子结构性质:问题的最优解由相关子问题的最优解组合而成,而这些子问题可以独立求解。

最优子结构性质和子问题重叠性质是应用动态规划的两个标志。

备注:快速排序本质思想也是分治,因为是原地排序,子问题递归求解后不需要再合并。

1.钢条切割

通过钢条切割问题引出动态规划。
长度为n的钢条求能使公司收益最大的切割方案。给的条件也有限制,长度为n的钢条,price[n]。

1.1 朴素的递归解法

将钢条从左边切割下长度为i的一段,只对右边剩下的长度为n-i的一段进行再切割(递归求解),对左边的一段则不再进行切割。这样可以得出收益r的公式:

在这个公式中,原问题的最优解只包含一个相关子问题(右端剩余的部分)的解,而不是两个。

int CutRod(vector<int> price, int n) {
    if (n == 0)
        return 0;
    int res = -1;
    /* 也可以i从0开始到n-1 */
    for(i = 1; i <= n; i ++) {
        res = max(res, price[i] + CutRod(price, n - i));
    }
    return res;
}

显然递归的效率是很差的,当n比较大时会很慢。取n=4,查看其递归调用树。
这里写图片描述
分析CutRod()的运行时间,令T[n]表示第二个参数值为n时,CutRod的调用次数,该值等于递归调用树中根为n的子树中的结点个数,blablabla,CutRod的运行时间是n的指数函数。

所以将CutRod()转换为一个更高效的动态规划算法。动态规划算法方法是付出额外的内存空间来节省计算时间,是典型的时空权衡(time-memory-trade-off)的例子,可以将一个指数时间的解转化为一个多项式时间的解。

1.2 动态规划解法

动态规划有两种等价的实现方法。

(1)带备忘的自顶向下法

此方法仍按自然的递归形式编写过程,但过程会保存每个子问题的解(通常保存在一个数组或散列表中)。当需要一个子问题的解时,过程首先检查是否已经保存过此解。如果是,则直接返回保存的值,从而节省了计算时间;否则,按通常方式计算这个子问题。

(2)自底向上法

这种方法一般需要恰当定义子问题“规模”的概念,使得任何子问题的求解都只依赖于“更小的”子问题的求解。因此,我们可以将子问题按照规模顺序,由小至大顺序进行求解。当求解某个子问题时,它所依赖的那些更小的子问题都已求解完毕,结果已经保存。每个子问题只需求解一次,当我们求解它时,它的所有前提子问题都已求解完成。

说明:两种方法得到的算法具有相同的渐进运行时间,仅有的差异是在某些特殊情况下,自顶向下方法并未真正递归地考察所有可能的子问题。由于没有频繁的递归函数调用的开销,自底向上方法的时间复杂度函数通常具有更小的系数。

带备忘的自顶向下CutRod的解法:

int MemoizedCutRod(vector<int> price, int n) {
    vector<int> res(n + 1, -1);
    return Memoized(price, res, n);
}
int Memoized(vector<int> p, vector<int> &res, int n) {
    if (res[n] >= 0)
        return res[n];
    if (n == 0)
        res[0] = 0;
    else {
        int i, temp;
        temp = -1;
        for (i = 1; i <= n; i ++)
            temp = max(temp, p[i] + Memorized(p, res, n - i));
        res[n] = temp;
    }
    return res[n];
}

自底向上的CutRod的解法:

int BottomUpCutRod(vector<int> price, int n) {
    /* res保存求出的更小的子问题的解 */
    vector<int> res(n + 1);
    res[0] = 0;
    int i, j, temp;
    for (i = 1; i <= n; i ++) {
        temp = -1;
        for (j = 1; j <= i; j ++) 
            temp = max(temp, price[j] + res[i - j]);
        res[i] = temp;
    }
    return res[n];
}

自底向上的解法采用子问题的自然顺序,如果j < i,则规模j的子问题比规模i的子问题更小,子问题j必须已经求出,因此,依次顺序求出规模i = 0、1、2、3……n的子问题。

1.3 求最优切割方案的动态规划解法

如果只是求钢条切割公司收益最大值,则到此就可以了;如果是求最优的切割方案,则还需要执行第4步。不仅需要求出保存最大收益值的res[i],同时需要求出最优解对应的第一段钢条的切割长度s[i]。

vector<int> ExtendBottomUpCutRod(vector<int> p, int n, vector<int> &res) {
    vector<int> s(n + 1);
    res[0];
    int i, j;
    for (i = 1; i <= n; i ++) {
        int temp = -1;
        for (j = 1; j <= i; j ++) {
            if (p[j] + res[i - j] > temp) {
                temp = p[j] + res[i - j];
                s[i] = j;
            }
        res[i] = temp;
    }
    return s;
} 
void Print(vector<int> res, vector<int> s, int n) {
    cout << "max profit:" << res[n] << endl;
    while (n > 0) {
        cout << s[n];
        n = n - s[n];
    }
    cout << endl;
}   

代码经过运行,输出结果正确。

参考:
[1]《算法导论》

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值