算法导论读书笔记(17)

算法导论读书笔记(17)

目录

  • 动态规划概述

  • 钢条切割
    • 自顶向下的递归实现

    • 使用动态规划解决钢条切割问题

    • 子问题图

    • 重构解

  • 钢条切割问题的简单Java实现

动态规划概述

和分治法一样, 动态规划 (dynamic
programming)是通过组合子问题的解而解决整个问题的。分治法是将问题划分成一些独立的子问题,递归地求解各子问题,然后合并子问题的解而得到原问题的解。与此不同,动态规划适用于子问题并不独立的情况,即各子问题包含公共的子子问题。在这种情况下,分治法会重复地求解公共的子子问题。而动态规划算法对每个子问题只求解一次,将其结果保存在一张表中,从而避免重复。

动态规划通常用于 最优化问题
。此类问题可能有多种可行解。每个解有一个值,而我们希望找出具有最优(最大或最小)值的解。称这样的解为该问题的“一个”最优解(而不是“确定的”最优解),因而可能存在多个最优解。

动态规划算法的设计可以分为如下4个步骤:

  1. 描述最优解的结构

  2. 递归定义最优解的值

  3. 按自底向上的方式计算最优解的值

  4. 由计算出的结构构造一个最优解

钢条切割

钢条切割问题是动态规划问题的一个例子。塞林企业会买进长钢条,将它们切割成短条后卖出(切割是免费的,不计成本)。塞林企业的老总想要知道钢条怎么切割最赚钱。

已知塞林企业对长度为 i 英寸的钢条的售价为 pi 美元,其中 i =
1,2,…。下图给出了一张样本价格表。

钢条切割问题 的描述如下。给定一根长为 n 的钢条以及价格表
pi ,其中 i = 1,2,…, n ,找出钢条切割并卖出后可取得的最大收益
rn

考虑一下 n = 4的情况。下图列出了切割4英寸钢条的所有方式。根据样本价格表,最后可知将4英寸钢条切割成2根2英寸钢条的收益最大。
p2 + p2 = 10。

一根长度为 n 的钢条共有 2n-1
种不同的切割方式。我们这里用普通加法符号表示一个分解,比如7 = 2 + 2 +
3就表示一根长度为7的钢条被切成3份,2根长度为2,一根长度为3。如果最优解将钢条切割成 k 份( 1 <= k
<= n ),那么最优分解即为 n = i1 +
i2 + … + ik ,每段钢条的长度为
i1i2 ,…, ik
,对应的最大收益为 rn = pi1 +
pi2 + … + pik

一般来说,我们可以将最优收益 rn 表示成如下形式:

rn = max (
pn , r1 +
rn-1 , r2 +
rn-2 ,…, rn-1 +
r1 )

第一个参数 pn 代表不切割时钢条的价格。其它的 n - 1个参数首先将钢条分为2份,长度分别为
in - ii = 1,2,…, n - 1),然后分别取得两份的最优收益
rirn-i 之后做和。因为我们不知道
i 取值为多少时会得到最优解,所以我们必须计算所有可能情况并从中选出最优解。

可以看到,为了解决规模为 n
的初始问题,我们首先要解决的是规模小一些的同类型问题。一旦我们做出了一个划分,我们就可以将划分出的两部分视为钢条切割问题的独立的实例。整体最优解就包含在这相关的两部分子问题之中。我们说钢条切割问题满足
最优子结构 的性质:某问题的最优解由相关子问题的最优解组合而成,且这些子问题可以独立求解。

下面以一种简单的方式安排钢条切割的递归结构,我们能看到一个分解是由位于左侧长度为 i 的一份,以及位于右侧的剩余部分 n -
i
。只有右侧的部分可能再次被分解。这样可以得到一个更简洁的公式:

在上面的公式中,最优解只和一个相关的子问题有关(划分后右侧的剩余部分)。

自顶向下的递归实现

下面的过程是一种很直接的,自顶向下,递归风格的实现。

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

过程 CUT-ROD 接受一个价格的数组 p [ 1 .. n ]和一个整数
n 作为参数,返回可能的最优解。如果你用自己最熟悉的语言实现了这个 CUT-ROD
过程并运行它,你会发现即使对于不太大的 n 值,你的程序也会花很长的时间才能得出结果。实际上,每次你将 n
值增加1,你的程序的运行时间大约要翻一番。

过程 CUT-ROD 的效率如此低下的原因就是它不断的重复解决相同的子问题。下图给出了一个很好的说明,其中 n
= 4,可以看到,过程多次重复计算 n = 2和 n = 1。

为了分析 CUT-ROD 的运行时间,设 T ( n )为问题规模为 n
时调用 CUT-ROD 的总次数,该表达式等于根结点标记为 n
的递归树中的总结点数。该总数包含根结点上的初始调用。因此, T ( 0 ) = 1和

其中 j = n - i ,可得 T ( n ) =
2n ,因此 CUT-ROD 的运行时间是 n 的幂。

使用动态规划解决钢条切割问题

可以看到,递归算法之所以效率低下,是因为它反复求解相同的子问题。,因此,动态规划会仔细安排求解顺序,对每个子问题只求解一次,并将结果保存下来以便之后查找。由此可见,动态规划需要额外的内存空间来节省计算时间,是典型的
时空权衡 (time-memory trade-off)的例子。

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


带备忘的自顶向下法(top-down with memoization)

此方法仍按自然的递归形式编写过程,但过程会保存每个子问题的解。

自底向上法(bottom-up method)

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

两种方法得到的算法具有相同的渐进运行时间,但自底向上方法的时间函数通常具有更小的系数。

下面给出的是自顶向下 CUT-ROD 过程的伪码,加入了备忘机制:

MEMOIZED-CUT-ROD(p, n)
1 let r[0..n] be a new array
2 for i = 0 to n
3 r[i] = -∞
4 return MEMOIZED-CUT-ROD-AUX(p, n, r)
MEMOIZED-CUT-ROD-AUX(p, n, r)
1 if r[n] >= 0
2 reutrn r[n]
3 if n == 0
4 q = 0
5 else
6 q = -∞
7 for i = 1 to n
8 q = max(q, p[i] + MEMOIZED-CUT-ROD-AUX(p, n - i, r))
9 r[n] = q
10 return q

过程 MEMOIZED-CUT-ROD 首先检查值是否已知,如果是,则返回;否则在第6~8行计算值 q
,第9行将 q 存入 r [ n ],最后返回 q

自底向上版本更简单:

BOTTOM-UP-CUT-ROD(p, n)
1 let r[0..n] be a new array
2 r[0] = 0
3 for j = 1 to n
4 q = -∞
5 for i = 1 to j
6 q = max(q, p[i] + r[j - i])
7 r[j] = q
8 return r[n]

过程 BOTTOM-UP-CUT-ROD 采用子问题的自然顺序:若 i < j
,则规模为 i 的子问题比规模为 j 的子问题“更小”。因此,过程依次求解规模为 j = 0,1,…,
n 的子问题。

子问题图

当思考一个动态规划为问题时,我们应该了解问题的子问题之间的依赖关系。

问题的 子问题图 准确地表达了这些信息,子问题图是一个有向图,每个定点唯一地对应一个子问题。如果求子问题
x 的最优解时需要直接用到子问题 y 的最优解,那么在子问题图中就会有一条从子问题 x 到子问题
y 的有向边。下图显示了 n = 4时钢条切割问题的子问题图。

子问题图 G = ( V , E
)的规模可以帮助我们确定动态规划的运行时间。由于每个子问题只求解一次,因此算法运行时间等于每个子问题求解时间之和。通常,一个子问题的求解时间与子问题图中对应顶点的度成正比,而子问题的数目等于子问题的顶点数。因此,通常情况下,动态规划算法的运行时间与顶点和边的数量呈线性关系。

重构解

上面的算法仅返回最优解的收益值,并未返回解本身。这里可以扩展该算法。

EXTENDED-BOTTOM-UP-CUT-ROD(p, n)
1 let r[0..n] and s[0..n] be new arrays
2 r[0] = 0
3 for j = 1 to n
4 q = -∞
5 for i = 1 to j
6 if q < p[i] + r[j - i]
7 q = p[i] + r[j - i]
8 s[j] = i
9 r[j] = q
10 return r and s

钢条切割问题的简单Java实现

/**
* 带备忘的自顶向下方法
*
* @param price 价格表
* @param n 待分割的长度
*/
public static int memoizedCutRod(int[] price, int n) {
int[] revenue = new int[n + 1];
for (int i = 0; i < revenue.length; i++) // 初始化revenue数组
revenue[i] = Integer.MIN_VALUE;
return memoizedCutRodAux(price, n, revenue);
}

private static int memoizedCutRodAux(int[] price, int n, int[] revenue) {
int q;
if (revenue[n] >= 0) // 如果revenue数组中有记录,就返回数组中的结果
return revenue[n];
if (n == 0)
q = 0;
else {
q = Integer.MIN_VALUE;
for (int i = 1; i <= n; i++)
q = Integer.max(q, price[i] + memoizedCutRodAux(price, n - i, revenue));
revenue[n] = q;
}
return q;
}

/**
* 自底向上法
*
* @param price 价格表
* @param n 待分割的长度
*/
public static int bottomUpCutRod(int[] price, int n) {
int[] revenue = new int[n + 1];
int q;
revenue[0] = 0;
for (int j = 1; j <= n; j++) {
q = Integer.MIN_VALUE;
for (int i = 1; i <= j; i++)
q = Integer.max(q, price[i] + revenue[j - i]);
revenue[j] = q;
}
return revenue[n];
}

算法导论读书笔记(17),布布扣,bubuko.com

时间: 2024-12-25 02:42:24

算法导论读书笔记(17)的相关文章

算法导论读书笔记之钢条切割问题

算法导论读书笔记之钢条切割问题 巧若拙(欢迎转载,但请注明出处:http://blog.csdn.net/qiaoruozhuo) 给定一段长度为n英寸的钢条和一个价格表 pi (i=1,2, -,n),求切割钢条的方案,使得销售收益rn最大.注意,如果长度为n英寸的钢条价格pn足够大,最优解可能就是完全不需要切割. 若钢条的长度为i,则钢条的价格为Pi,如何对给定长度的钢条进行切割能得到最大收益? 长度i   1   2    3   4     5      6     7     8  

算法导论读书笔记(13)

算法导论读书笔记(13) 目录 红黑树 旋转 插入 情况1 : z 的叔父结点 y 是红色的 情况2 : z 的叔父结点 y 是黑色的,而且 z 是右孩子 情况3 : z 的叔父结点 y 是黑色的,而且 z 是左孩子 删除 情况1 : x 的兄弟 w 是红色的 情况2 : x 的兄弟 w 是黑色的,且 w 的两个孩子都是黑色的 情况3 : x 的兄弟 w 是黑色的, w 的左孩子是红色的,右孩子是黑色的 情况4 : x 的兄弟 w 是黑色的,且 w 的右孩子是红色的 红黑树 红黑树 是一种二叉查

算法导论读书笔记(18)

算法导论读书笔记(18) 目录 最长公共子序列 步骤1:描述最长公共子序列的特征 步骤2:一个递归解 步骤3:计算LCS的长度 步骤4:构造LCS LCS问题的简单Java实现 最长公共子序列 某给定序列的子序列,就是将给定序列中零个或多个元素去掉后得到的结果.其形式化定义如下:给定一个序列 X = < x1 , x2 , - , xm >,另一个序列 Z = < z1 , z2 , - , zk >,如果 Z 满足如下条件则称 Z 为 X 的 子序列 (subsequence),

算法导论读书笔记(15) - 红黑树的具体实现

算法导论读书笔记(15) - 红黑树的具体实现 目录 红黑树的简单Java实现 红黑树的简单Java实现 /** * 红黑树 * * 部分代码参考自TreeMap源码 */ public class RedBlackTree<T> { protected TreeNode<T> root = null; private final Comparator<? super T> comparator; private int size = 0; private static

算法导论读书笔记(16)

算法导论读书笔记(16) 目录 动态顺序统计 检索具有给定排序的元素 确定一个元素的秩 区间树 步骤1:基础数据结构 步骤2:附加信息 步骤3:维护信息 步骤4:设计新操作 动态顺序统计 之前介绍过 顺序统计 的概念.在一个无序的集合中,任意的顺序统计量都可以在 O ( n )时间内找到.而这里我们将介绍如何在 O ( lg n )时间内确定任意的顺序统计量. 下图显示的是一种支持快速顺序统计量操作的数据结构.一棵 顺序统计树 T 通过在红黑树的每个结点中存入附加信息而成.在一个结点 x 内,增

算法导论读书笔记(14) - 二叉查找树的具体实现

算法导论读书笔记(14) - 二叉查找树的具体实现 目录 二叉查找树的简单Java实现 二叉查找树的简单Java实现 /** * 二叉查找树 * 部分代码参考自TreeMap的源码 */ public class BinarySearchTree<T> { protected TreeNode<T> root = null; private final Comparator<? super T> comparator; private int size = 0; pub

算法导论读书笔记-第十四章-数据结构的扩张

算法导论第14章 数据结构的扩张 一些工程应用需要的只是标准数据结构, 但也有许多其他的应用需要对现有数据结构进行少许的创新和改造, 但是只在很少情况下需要创造出全新类型的数据结构, 更经常的是通过存储额外信息的方法来扩张一种标准的数据结构, 然后对这种数据结构编写新的操作来支持所需要的应用. 但是对数据结构的扩张并不总是简单直接的, 因为新的信息必须要能被该数据结构上的常规操作更新和维护. 14.1 动态顺序统计 顺序统计树(order-static tree) : 在红黑树的基础上, 在每个

平摊分析 --- 算法导论读书笔记

我们经常会说一个算法快不快,这个可以由实验得出,也可以通过分析复杂度得出.实验需要大量不同的输入才更全面准确,否则片面地看某个输入下的表现,是比较偏颇的.分析复杂度(通常分析最坏,因为平均涉及输入的概率分布,依靠假设或者实验和经验)有时候并不是一个简单的事,简单的情况是遍历 for(int i = 0; i != n; i++) 的这种情况,显然是O(n)的复杂度.但是一些复杂的情况就比较难办了,举例来说: a.   栈操作:  除了PUSH,POP,添加一个操作叫MULTIPOP. MULTI

字符串匹配问题 ---- 算法导论读书笔记

字符串匹配是一个很常见的问题,可以扩展为模式的识别,解决字符串问题的思想被广泛地应用.介绍四种解决该问题的办法,包括:最朴素的遍历法,Rabin-Karp算法,自动机机匹配,Knuth-Morris-Pratt算法即耳熟能详的KMP. 在一开始,先对时间复杂度做出一个总扩(从大到小):[1]朴素法:O( (n-m+1)m ):[2]Rabin-Karp:预处理:O(m),匹配:最坏O( (n-m+1)m ),但是平均和实际中比这个好得多:[3]自动机:预处理O(m|Σ|),匹配O(n):[4]K