动态规划 -- 钢条切割

/*
    动态规划和分治法相似,都是通过组合子问题的解来求解原问题。 但分治法是将
问题划分为互不相交的子问题,递归地求解子问题,再将它们的解组合起来,求出
原问题的解。与之相反,动态规划应用于子问题重叠的情况,即不同的子问题具有公共的
子问题。在这种情况下,分治法会做很多不必要的工作。
    动态规划方法通常用来求解最优化问题,这类问题通常有很多可行解。我们希望寻找
具有最优值的解。
    我们通常按照如下4个步骤来设计一个动态规划算法:
    · 刻画一个最优解的结构特征
    · 递归地定义最优解的值
    · 计算最优解的值,通常采用自底向上的方法
    · 利用计算出的信息构造一个最优解

钢条切割:给出一个钢条长度的价格表,根据价格表,给定一段长度为 n 的钢条,求一个
切割方案,使得收益最大。可能出现的一种特殊情况就是完全不需要切割。
*/

#include <iostream>

using namespace std;

double max(double a, double b) {
    return a >= b ? a : b;
}

/*
方法一:自顶向下递归实现:伪代码
    CUT-ROD(p, n)
        if n == 0
            return 0  // 如果长度为 0,那么收益肯定为 0
        q = -∞
        for i = 1 to n
            q = max(q, p[i] + CUT-ROD(p, n - i))
        自底向上版本
        return q
    这种实现方法中,CUT-ROD 反复用参数值对自身进行递归调用,即它反复求解了
相同的子问题,那么在递归调用的时候,程序的工作量就会爆炸性地增长。复杂度为 2^n
*/

double cutRod(double *table, int length) {
    if (length == 0)
        return 0;
    double q = 0;
    for (int i = 1; i <= length; ++i) {
        q = max(q, table[i] + cutRod(table, length - i));
    }
    return q;
}

/*
使用动态规划方法求解
    上面的算法称为朴素递归算法,它的效率之所以很低,就是因为重复求解相同的子问题,
因此动态规划要求仔细安排求解顺序,使得每个子问题只求解一次。那么着就需要保存结果,
在再次用到这个结果的时候就不需要重新计算而是使用查找。
    当然,这样的话就需要使用额外的空间来保存计算的结果。但是一般来说这样的代价是值得的。
*/

/*
方法二:带备忘的自顶向下法:
    这种方法仍然按照自然的递归形式编写过程,但过程会保存每个每个子问题的解,在需要使用一个子问
题的解的时候,先查找这个子问题的解是否已经存在。
    伪代码:
    MEMOIZED-CUT-ROD(p, n)
        let r[0 .. n] be a new array
        for i = 0 to n
            r[i] = -∞
        return MEMOIZED-CUT-ROD-AUX(p, n, r)

    MEMOIZED-CUT-ROD-AUX(p, n, r)
        if r[n] >= 0
            return r[n]
        if n == 0
            q = 0
        else q = -∞
            for i = 1 to n
                q = max(q, p[i] + MEMOIZED-CUT-ROD-AUX(p, n - i, r))
            r[n] = q
        return r[n]
    MEMOIZED-CUT-ROD 函数主要进行一些初始化,因为我们已经知道结果肯定是非负的。
    在 MEMOIZED-CUT-ROD-AUX 中,n 代表要求的子问题,r 数组用来记忆。这种求解方法还是
从最大问题开始逐渐将问题划分为子问题。所以称为自顶向下。
*/

double memoizedCutRodAux(double *table, int length, double *r) {
    if (r[length] >= 0)
        return r[length];
    double q = 0;

    if (length > 0) {
        q = -1;
        for (int i = 1; i <= length; ++i)
            q = max(q, table[i] + memoizedCutRodAux(table, length - i, r));
    }
    r[length] = q;
    return q;
}

double memoizedCutRod(double *table, int length) {
    double *r = new double[length + 1];
    for (int i = 1; i <= length; ++i)
        r[i] = -1;
    return memoizedCutRodAux(table, length, r);
}

/*
方法三:自底向上
    这种方法一般需要恰当定义子问题“规模”的概念,使得任何子问题的求解都只依赖于“更小的”
子问题的求解。因而我们可以将子问题按规模排序。按照由小到大的顺序进行求解。当求解某个子问题的
时候,它所以来的那些更小的子问题都已经求解完毕,结果已经保存。这样每个子问题都只需要求解一次。
    这种方法和第二中的时间复杂度是一样的,但它通常有更小的系数。
    伪代码:
    MEMOIZED-CUT-ROD(p, n)
        let r[0 .. n] be a new array
        r[0] = 0
        for i = 1 to n
            q = -∞
            for j = 1 to i
                q = max(q, p[j] + r[i - j])
            r[i] = q
        return r[n]

    自底向上版本 MEMOIZED-CUT-ROD 采用子问题的自然顺序,过程一次求解规模为 0, 1 .. n 的子问题。

    子问题图:
   自底向上动态规划是按“逆拓扑排序”或“反序的拓扑排序”来处理子问题图中的顶点。那么也就是说,对于任何一个 子问题,直至它所依赖的所有子问题均已求解完成,才会求解它。
*/ double memoizedCutRodDown(double *table, int length) { double *r = new double[length + 1]; r[0] = 0; double q = -1; for (int i = 1; i <= length; ++i) { q = -1; for (int j = 1; j <= i; ++j) { q = max(q, table[j] + r[i - j]); } r[i] = q; } return r[length]; } int main(int argc, char const *argv[]) { int steelLength; double *table; cout << "Input the length of the steel: "; cin >> steelLength; table = new double[steelLength + 1]; cout << "Input the price price table (only the price with ascending order [1 .. length]):" << endl; for (int i = 1; i <= steelLength; ++i) { cin >> table[i]; } cout << "Max price(first): " << cutRod(table, steelLength) << endl; cout << "Max price(second): " << memoizedCutRod(table, steelLength) << endl; cout << "Max price(thied): " << memoizedCutRodDown(table, steelLength) << endl; return 0; }
原文地址:https://www.cnblogs.com/xiezhw3/p/4070348.html