动态规划 - 钢条切割

动态规划

动态规划方法通常用来求解最优化问题,这类问题可以有很多可行解,每个解都有值,我们希望寻找具有最优值(最小值或最大值)的解。我们称这样的解为问题的一个最优解决,而不是最优解,因为可能有多个解都达到最优值

动态规划与分治法

动态规划与分治方法相似,都是通过组合子问题的解来求原问题。

分治法将问题划分为互不相交的子问题,递归地求解子问题,再将它们的解组合起来,求出原问题的解。与之相反,动态规划应用于子问题的重叠的情况,即不同的子问题具有公共的子子问题(子问题的求解是递归进行的,将其划分为更小的子问题)。

在这种情况下,分治算法会做许多不必要的工作,它会反复地求解那些公共子子问题,而动态规划算法对每个子问题值求解一次,将其保存在一个表格中,从而无需每次求解子问题时重新计算,避免了这种不必要的计算工作

动态规划的设计步骤

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

步骤1-3是动态规划求解问题的基础,如果我们仅仅是求解最优解的值,而非解本身,可以忽略步骤4。如果确实要做步骤4,有时需要在执行步骤3的过程中维护一些额外的信息,以便来构造一个最优解

钢条切割

给定一段长度为n英寸的钢条和一个价格表pi(i=1,2,...,n),求解切割钢条方案,使得销售收益rn最大。注意,如果长度为n英寸的钢条价格pn足够大,最优解可能就是完全不需要切割。

分析

假定我们知道出售一段长度为i英寸的钢条价格为pi(i=1,2,...,n),钢条长度为整英寸,如下

162025012825029.png

考虑n=4的情况,我们所有可能的切割方案如下

下载.jpeg

由上图可知,将一段长度为4英寸的钢条切割为两段各长为2英寸的钢条,将产生p2+p2=5+5=10的收益,为最优解。

不难发现规律,对于长度为n英寸的钢条共有2n-1种不同的分割方案,因为在距离钢条左端i(i=1,2,...,n-1)英寸处,我们总可以选择切割或者不切割。

我们用普通的加法符号表示切割方案,因此7=2+2+3表示将长度为7英寸的钢条切割为三段——两段长度为2英寸、一段长度为3英寸。如果一个最优解将钢条切割为k段(对某个1\leqk\leqn),那么最优切割方案:n = i1 + i2 + ... + ik。将钢条切割为长度分别为i1 、i2 、 ... 、 ik小段,得到的最大收益为rn = pi1 + pi2 + ... + pik

对于上述价格表,我们可以先观察所有最优收益值ri(i=1,2,...,10)及对应的最优切割方案:

  • r1=1,切割方案:1 = 1(无切割)
  • r2=5,切割方案:2 = 2(无切割)
  • r3=8,切割方案:3 = 3(无切割)
  • r4=10,切割方案:4 = 2 + 2
  • r5=13,切割方案:5 = 2 + 3
  • r6=17,切割方案:6 = 6(无切割)
  • r7=18,切割方案:7 = 1 + 6 或 7 = 2 + 2 + 3
  • r8=22,切割方案:8 = 2 + 6
  • r9=25,切割方案:9 = 3 + 6
  • r10=30,切割方案:10 = 10(无切割)

更一般地,对于rn(n\geq1),可以用更短地钢条的最优收割收益来描述它:rn = max(pn,r1 + rn-1 ,r2 + rn-2,...,rn-1 + r1)

  • pn对应不切割,直接出售长度为n英寸的钢条方案
  • 其他n-1个参数对应另外n-1种切割方案:对每个i=1,2,...,n-1,首先将钢条切割长度为i和n-i的两段,接着求解这两段的最优收益ri和rn-i(每种方案的最优收益为两段的最优收益之和)

由于无法预知哪种方案会获得最优收益,需要考虑所有可能的i,选出其中收益最大者。

除了上述方案,钢条切割问题还存在一种相似的但更为简单的递归求解方案:

我们将钢条从左边切割长度为i的一段,只对右边剩下的长度为n-i的一段继续进行切割(递归求解),对左边的一段则不再进行切割。即问题分解为:将长度为n的钢条分解为左边开始一段,以及对剩余部分继续分解的结果。

这样,不做任何切割的方案可以描述为:第一段的长度为n,收益为pn,剩余部分长度为0,对应收益为r0=0。所以上面公式可以得到简化版本:rn = max(pi + rn-i)

自顶向下递归

/**
 @param p 收益
 @param n 钢条长度
 @return 最大收益
 */
int CutSteel(const int *p, int n)
{
    if (n == 0)
        return 0;

    int q = -1;
    for (int i = 1; i <= n; i++) {
        q = max(q, p[i] + CutSteel(p, n-i));
    }
    return q;
}

int main(int argc, const char * argv[]) {
    int prices[] = {0,1,5,8,9,10,17,17,20,24,30};
    for (int i = 1; i <= 10; i++) {
        printf("尺寸为%d的最大收益为:%d\n", i, CutSteel(prices, i));
    }
    return 0;
}

运行输出

尺寸为1的最大收益为:1
尺寸为2的最大收益为:5
尺寸为3的最大收益为:8
尺寸为4的最大收益为:10
尺寸为5的最大收益为:13
尺寸为6的最大收益为:17
尺寸为7的最大收益为:18
尺寸为8的最大收益为:22
尺寸为9的最大收益为:25
尺寸为10的最大收益为:30
Program ended with exit code: 0

动态规划方法

上面的递归方法效率很低,因它反复方法求解相同的子问题。而动态规划对每个子问题只求解一次,并将结果保存下来,如果随后再次需要此子问题,只需查找保存的结果,而不必重新计算,这是典型的时空权衡,但是时间上的节省是巨大的。

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

  • 带备忘录的自顶向下方法

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

  • 自底向上方法

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

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

带备忘录的自顶向下

int MemorizedCutSteelCore(const int *p, int n, int *r)
{
    if (r[n] > 0) //检查值是否存在
        return r[n];

    int q = -1;
    if (n == 0) //钢条长度为0,收益为0
    {
        q = 0;
    }
    else
    {
        for (int i = 1; i <= n; i++) {
            int temp = p[i] + MemorizedCutSteelCore(p, n - i, r); //自顶向下递归
            q =  max(q,temp);
        }
    }
    r[n] = q; //保存子问题的值
    return q;
}

int MemorizedCutSteel(const int *p , int n)
{
    int *r = new int[n + 1];
    for (int i = 0; i <= n; i++) {
        r[i] = -1;
    }
    return MemorizedCutSteelCore(p, n, r);
}

自底向上

int BottomUpCutSteel(const int *p, int n)
{
    int *r = new int[n+1];
    r[0] = 0;

    for (int i = 1; i <= n; i ++) {
        int q = -1;
        for (int j = 1;j <= i; j++) {
            int temp = p[j] + r[i - j];
            // q = q > temp? q : temp;
            q = max(q, temp);
        }
        r[i] = q;
    }
    return r[n];
}

自顶向下与自底向上算法具有相同的渐进运行时间,都为O(n2)

重构解

上面的动态规划返回最优解的值,但并未返回解本身(一个长度列表,给出切割后每段钢条的长度)。所以可以扩展动态规划算法,使之对每个子问题不仅保存最优收益值,还保存对应的切割方案。

 void ExtendedBottomUpCutSteel(const int *p, int n, int *r, int *s)
{
    r[0] = 0;
    for (int i = 1; i <= n; i++) {
        int q = -1;
        for (int j = 1; j <= i; j++) {
            int temp = p[j-1] + r[i-j];
            if (temp > q) {
                q = temp;
                s[i] = j;
            }
        }
        r[i] = q;
    }
}

// r[]保存长度为i的钢条最大收益,s[]保存最优解对应的第一段钢条的切割长度
void PrintCutSteel(const int *p, int n, int *r, int *s)
{
    ExtendedBottomUpCutSteel(p, n, r, s);
    cout << "长度为" << n << "的钢条最大收益为:" << r[n] << endl;
    cout << "最优方案的钢条长度分别为:";
    while (n>0) {
        cout << s[n] << " ";
        n = n - s[n];
    }
    cout << endl;
}

int main(int argc, const char * argv[]) {
    int n;
    cout << "输入钢条的尺寸:" << endl;
    while (cin >> n) {
        int *p = new int[n];
        for (int i = 0; i < n; i++) {
            cin >> p[i];
        }
        int *r = new int[n + 1];
        int *s = new int[n + 1];

        PrintCutSteel(p, n, r, s);
    }
    return 0;
}

运行程序

输入钢条的尺寸:
7
1 5 8 9 10 17 17 
长度为7的钢条最大收益为:18
最优方案的钢条长度分别为:1 6 

参考

《算法导论》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,470评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,393评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,577评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,176评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,189评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,155评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,041评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,903评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,319评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,539评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,703评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,417评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,013评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,664评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,818评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,711评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,601评论 2 353

推荐阅读更多精彩内容

  • ![cover](http://wxlogo.ncuhome.cn/d_laptop2_1x.png) ##提纲 ...
    蒲编阅读 1,005评论 0 0
  • 《算法导论》这门课的老师是黄刘生和张曙,两位都是老人家了,代课很慢很没有激情,不过这一章非常有意思。更多见:iii...
    mmmwhy阅读 5,270评论 5 31
  • 目录 动态规划与分治法 2.动态规划求解的最优化问题应该具备的两个要素2.1 最优子结构2.2 子问题重叠 动态规...
    王侦阅读 1,378评论 0 1
  • 动态规划应用于子问题重叠的情况。对于公共子问题,分治算法会做很多不必要的工作,它会反复求解公共子问题。而动态规划算...
    LRC_cheng阅读 432评论 0 1
  • 1. 概述 动态规划与分治法相似,都是通过组合子问题来求解原问题。区别在于,分治法将问题划分为互不相交的子问题,递...
    10xjzheng阅读 1,354评论 0 0