《算法导论》15章-动态规划 15.1 钢条切割(含有C++代码)

一、引入

动态规划方法通常用来求解最优化问题(optimizationproblem)。这类问题可以有很多可行解,每个解都有一个值,我们希望寻找具有最优值(最小值或最大值)的解。我们称这样的解为问
题的一个最优解(an optimal solution),而不是最优解( the optimal solution),因为可能有多个解都达到最优值。
我们通常按如下4个步骤来设计一个动态规划算法:
1.刻画一个最优解的结构特征。
2.递归地定义最优解的值。
3.计算最优解的值,通常采用自底向上的方法。
4.利用计算出的信息构造一个最优解。

15.1 钢条切割

一、问题相关

《算法导论》15章-动态规划 15.1 钢条切割(含有C++代码)_第1张图片钢条切割问题是这样的:给定一段长度为n英寸的钢条和一个价格表p;(i=1, 2,… n),求切割钢条方案,使得销售收益rn最大。注意,如果长度为n英寸的钢条的价格pn足够大,最优解可能就是完全不需要切割。
《算法导论》15章-动态规划 15.1 钢条切割(含有C++代码)_第2张图片

二、初步想法

1、刻画最优解结构特征

(1)长度为n英寸的钢条共有2n-1种不同的切割方案,因为在距离钢条左端i(i=1,2,… n-1)英寸处,我们总是可以选择切割或不切割。我们用普通的加法符号表示切割方案,因7=2+2+3表示将长度为7英寸的钢条切割为三段一两段长度为2英寸、一段长度为3英寸。
(2)如果一个最优解将钢条切割为k段(对某个1≤k≤n),那么最优切割方案n= i + i2 +…+ ik将钢条切割为长度分别为i1,i2, …,ik的小段,得到最大收益rn= pi1 + pi2 +…+ pik对于上述价格表样例,我们可以观察所有最优收益值r(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(无切割)

(3)更一般地,对于rn(n≥1),我们可以用更短的钢条的最优切割收益来描述它:
rn= max(pn,r1+rn-1,r2+rn-2,…rn-1+r1)
(4)第一个参数pn对应不切割,直接出售长度为n英寸的钢条的方案。其他n-1个参数对应另外n-1种方案:对每个i=1, 2,…,n-1,首先将钢条切割为长度为i和n-i的两段,接着求解这两段的最优切割收益ri和rn-i;(每种方案的最优收益为两段的最优收益之和)。由于无法预知哪种方案会获得最优收益,我们必须考察所有可能的i,选取其中收益最大者。如果直接出售原钢条会获得最大收益,我们当然可以选择不做任何切割。
(5)注意到,为了求解规模为n的原问题,我们先求解形式完全一样,但规模更小的子问题。即当完成首次切割后,我们将两段钢条看成两个独立的钢条切割问题实例。我们通过组合两个相关子问题的最优解,并在所有可能的两段切割方案中选取组合收益最大者,构成原问题的最优解。我们称钢条切割问题满足最优子结构(optimal substructure)性质:问题的最优解由相关子问题的最优解组合而成,而这些子问题可以独立求解。
(6)一种更简单的办法:我们将钢条从左边切割下长度为i的一段,只对右边剩下的长度为n- i的- 一段继续进行切割(递归求解),对左边的一段则不再进行切割。即问题分解的方式为:将长度为n的钢条分解为左边开始一段,以及剩余部分继续分解的结果。
在这里插入图片描述

CUT-ROD(p,q)
if n==0
	return 0
q = -∞
for i = 1 to n
	q = max(q,p[i]+CUT-ROD(p,n-i))
return q

仔细一看这个方法并不是特别好,因为钢条一长,不断递归,问题规模巨大…
《算法导论》15章-动态规划 15.1 钢条切割(含有C++代码)_第3张图片

二、使用动态规划方法求解最优钢条切割问题

动态规划方法是典型的用空间争取时间,典型的时空权衡(time-memory-trade-off)。
有以下几种方法:
第一种方法称为带备忘的自顶向下法(top- down with memoization)曰。此方法仍按自然的递归形式编写过程,但过程会保存每个子问题的解(通常保存在一个数组或散列表中)。当需要一个子问题的解时,过程首先检查是否已经保存过此解。如果是,则直接返回保存的值,从而节省了计算时间;否则,按通常方式计算这个子问题。我们称这个递归过程是带备忘的(memoized)。因为它“记住”了之前已经计算出的结果。
第二种方法称为自底向上法(bottom up method)。这种方法一般需要恰当定义子问题“规模”的概念,使得任何子问题的求解都只依赖于“更小的"子问题的求解。因而我们可以将子问题按规模排序,按由小至大的顺序进行求解。当求解某个子问题时,它所依赖的那些更小的子问题都已求解完毕,结果已经保存。每个子问题只需求解一次, 当我们求解它(也是第一次遇到它)时, 它的所有前提子问题都已求解完成。

1、加入备忘机制的伪代码:

自顶向下

MEMORIZED-CUT-ROD(p,n)
let r[0...n] be a new array
for i = 0 to n	//新建一个存储的数组
	r[i] = -∞
return MEMORIZED-CUT-ROD-AUX(p,n,r)
MEMORIZED-CUT-ROD-AUX(p,n,r)
if r[n] >= 0	//检查所需的值是否已知
	return r[n]
//计算所需值q	
if n==0	
	q = 0
else q = -∞
	for i = 1 to n
		q = max(q,p[i]+MEMORIZED-CUT-ROD-AUX(p,n-i,r))
r[n] = q		//将q存入r[n]
return q

自底向上

BOTTOM-UP-CUT-ROD(p,n)
let r[0..n] be a new array	//创建一个新数组保存子问题的解
r[0] = 0
for j = 1 to n
	q = -∞
	for i = 1 to j
		q = max(q,p[i] + r[j-i])	//直接访问数组元素来获得子问题的解,r[j-i]一定是最优的
	r[j] = q
return r[n]

子问题图

《算法导论》15章-动态规划 15.1 钢条切割(含有C++代码)_第4张图片

c++代码

自顶而下

#include 
using namespace std;
int memorizedCutRodAux(int p[], int n, int r[])
{
    int q;//最大收益值
    if (r[n] >= 0)
        return r[n];//检查所需值是否已知
    if (n == 0)
        q = 0;//n=0时不会有收益
    else
    {
        q = -1;
        for (int i = 0; i < n; ++i)
            q = max(q, p[i] + memorizedCutRodAux(p, n - i - 1, r));
    }
    r[n] = q;
    return q;
}

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

int main()
{
    int p[10] = { 1,5,8,9,10,17,17,20,24,30 };
    int q = memorizedCutRod(p, 5);
    cout << "带备忘的自顶向下方法的最优收益值为:" << q;
    return 0;
}

自底而上

#include 
using namespace std;
int BottomUpCutRod(int p[], int n)
{
    int* r = new int(n + 1);
    r[0] = 0;
    if (n == 0) {
        return 0;
    }
    for (int j = 1; j <= n; j++) {
        int q = INT_MIN;
        for (int i = 1; i <= j; i++) {
            q = max(q, p[i-1] + r[j - i]);
        }
        r[j] = q;
    }
    return r[n];
}
int main()
{
    int p[10] = { 1,5,8,9,10,17,17,20,24,30 };
    int q = BottomUpCutRod(p, 4);
    cout << "带备忘的自底而上方法的最优收益值为:" << q;
    return 0;
}

2、进阶:不仅返回收益值,还返回第一段长度

EXTENDED-BOTTOM-UP-CUT-ROD(p,n)
let r[0...n] and s[0...n] be new arrays
r[0] = 0
for j = 1 to n
	q = -∞
	for i = 1 to j
		if q < p[i] + r[j-i]
			q = p[i] + r[j-i]
			s[j] = i		//将最优长度i保存于s[j]
	r[j] = q
return r and s

下面的过程接受两个参数:价格表p和钢条长度n,然后调用EXTENDED-BOTTOM-UP-CUT-ROD来计算切割下来的每段钢条的长度s[1…n],最后输出长度为n的钢条的完整的最优切割方案:
PRINT-CUT-ROD-SOLUTION(p,n)

PRINT-CUT-ROD-SOLUTION(p,n)
(r,s)= EXTENDED-BOTTOM-UP-CUT-ROD(p,n)
while n> 0
	print s[n]
	n=n-s[n]

C++代码

#include 

using namespace std;

void extendedBottomUpCutRod(int p[], int n, int r[], int s[])
{
    //int r[n+1];记录不同规模子问题的解,这里是1~10
    //int s[n+1];记录切割的解,即怎样切的
    int q;//记录收益
    r[0] = 0;
    for (int j = 1; j <= n; ++j)
    {
        q = -1;//初始为负,常见的表示未知数的方法
        for (int i = 1; i <= j; ++i)
        {
            if (q < p[i - 1] + r[j - i])
            {
                q = p[i - 1] + r[j - i];
                s[j] = i;
            }
        }
        r[j] = q;
    }
}

void printCutRodSolution(int p[], int n, int r[], int s[])
{
    extendedBottomUpCutRod(p, n, r, s);
    cout << "长度为" << n << "的钢条按照此种切割方法的最优收益为:" << r[n] << endl;
    cout << "对应的最优切割方案的解为:";
    while (n > 0)
    {
        cout << s[n] << " ";//输出最优切割方案
        n = n - s[n];
    }
}

int main()
{
    int p[10] = { 1,5,8,9,10,17,17,20,24,30 };
    int r[11], s[11];
    printCutRodSolution(p, 10, r, s);
    return 0;
}

你可能感兴趣的:(算法导论阅读,c++,算法,动态规划)