1. 动态规划算法
动态规划:通过把原问题分解为相对简单的子问题来求解复杂问题。动态规划常常适用于有重叠子问题和最优子结构性质的问题。
- 算法总体思想
- 动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题
- 与分治法的区别在于:适用于动态规划算法求解的问题,经分解得到的子问题往往不是互相独立的;若用分治法求解,则分解得到的子问题数目太多,导致最终解决原问题需指数时间, 原因在于:虽然子问题的数目常常只有多项式量级,但在用分治法求解时,有些子问题被重复计算了许多次
- 如果可以保存已解决的子问题的答案,就可以避免大量重复计算,从而得到多项式时间的算法
- 动态规划法的基本思路是:构造一张表来记录所有已解决的子问题的答案(无论算法形式如何,其填表格式是相同的)
- 算法的基本步骤
- 找出最优解的性质(分析其结构特征)
- 递归地定义最优值(优化目标函数)
- 以自底向上的方式计算出最优值
- 根据计算最优值时得到的信息,构造最优解
算法的基本要素:
- 最优子结构
- 在分析问题的最优子结构性质时,所用的方法具有普遍性:首先假设由问题的最优解导出的子问题的解不是最优的;然后设法证明在该假设下可构造出比原问题最优解更好的解;通过矛盾法证明由最优解导出的子问题的解也是最优的
- 解题方法:利用问题的最优子结构性质,以自底向上的方式递归地从子问题的最优解逐步构造出整个问题的最优解
- 最优子结构是问题能用动态规划算法求解的前提:同一个问题可以有多种方式刻划它的最优子结构,有些表示方法的求解速度更快(空间占用小,问题的维度低)
- 重叠子问题
- 子问题的重叠性:采用递归算法求解问题时,产生的子问题并不总是独立的,有些子问题被反复计算多次,称为子问题的重叠性质
- 动态规划算法的特点:对每一个子问题只求解一次,并将结果保存在一个表格中;当再次需要求解该子问题时,可以用常数时间查表得出结果;通常独立的子问题个数随问题的规模呈多项式增长;因此采用动态规划算法求解此类问题只需要多项式时间,因而解题效率较高
- 备忘录方法
- 备忘录方法是动态规划算法的一种变形,它也用表格来保存已解决的子问题答案,以避免重复计算
- 与动态规划的区别在于,备忘录方法的递归方式是自顶向下的
- 备忘录方法的控制结构与直接递归方法的控制结构相同,区别在于备忘录方法为每个解过的子问题建立了备忘录,以备需要时查看,从而避免了相同子问题的重复求解
2. 示例: 最长公共子序列
问题描述:
- 子序列
- 给定序列的子序列是在该序列中删去若干元素后得到的序列
- 若:给定序列
X=x1,x2,……,xn
,称:另一序列
Z=z1,z2,…,zk
是
X
的子序列,是指存在一个严格递增的下标序列:
i1,i2,…,ik ,使得:对于所有 j=1,2,…,k 有: zj=xij - 例如: Z=B,C,D,B 是$X={A,B,C,B,D,A,B}的子序列,相应的递增下标序列为{2,3,5,7}
- 公共子序列
- 给定:序列
X
和
Y - 若:另一序列
Z
:既是
X 的子序列,又是 Y 的子序列 - 称:
Z 是序列 X 和Y 的公共子序列
- 给定:序列
X
和
- 最长公共子序列(LCS)问题
- 给定2个序列 X=x1,x2,……,xm 和 Y=y1,y2,……,ym
- 找出
X
和
Y 的一个最长公共子序列
- 问题分析
- 要求找出“一个”而不是“唯一的”最长公共子序列
- 公共子序列在原序列当中不一定是连续的
动态规划求解LCS问题:
- 最长公共子序列问题具有最优子结构性质
- 给定序列 X=x1,x2,……,xm 和 Y=y1,y2,……,yn ,设它们的一个最长公共子序列为 Z=z1,z2,…,zk ,则:
- 若 xm=yn ;则: zk=xm=yn ,且 Zk−1 是 Xm−1 和 Yn−1 的LCS
- 若
xm≠yn
且
zk≠xm
;则:
Z
是
Xm−1 和 Y 的LCS - 若
xm≠yn 且 zk≠yn ;则: Z 是X 和 Yn−1 的LCS - 可见:LCS(X,Y) 包含了这2个序列的前缀子序列的LCS,因此:最长公共子序列问题具有最优子结构性质
- 定义递归解(分析子问题的递归结构)
- 由LCS问题的最优子结构性质可知:为求解
X
和
Y 的一个LCS - 当
xm=yn
时,须找出LCS(
Xm−1
,
Yn−1
),然后将
xm
(或
yn
)添加到这个LCS上得到LCS(
X
,
Y ) - 当
xm≠yn
时,须解决如下两个子问题:找出一个LCS(
Xm−1
,
Y
)和一个LCS(
X , Yn−1 ) - 由此递归结构可以看出LCS问题具有重叠子问题性质:因为LCS(
Xm−1
,
Y
)和LCS(
X , Yn−1 )都包含一个公共子问题,即求解LCS( Xm−1 , Yn−1 )
- 由LCS问题的最优子结构性质可知:为求解
X
和
- 建立递归关系(递归地定义最优值)
- 用 c[i][j] 表示序列 Xi 和 Yj 的最长公共子序列的长度,其中: Xi= { x1,x2,……,xi }; Yj= { y1,y2,……,yj },若其中一个序列长度为0( i=0或j=0 ),则LCS的长度也是0
- 根据最优子结构性质建立递归关系如下:
- 计算LCS的长度(最优值)
- 子问题空间分析:总共 Θ(mn) 个不同的子问题,所以子问题空间不大,因此考虑采用动态规划法自底向上计算最优值
- 设置两个数组作为输出:用 c[i][j] 表示序列 Xi 和 Yj 的最长公共子序列的长度,问题的最优值记为 c[m][n] ,即LCS(X,Y)的长度。用 b[i][j] 记录 c[i][j] 是从哪一个子问题的解得到的,数组b用于构造最长公共子序列(最优解)
- 按照b[i][j]的值表示的方向往回搜索: b[i][j]=1 :表示从左上方 c[i−1][j−1] 得到; b[i][j]=2 :表示从上方 c[i−1][j] 得到; b[i][j]=3 :表示从左方 c[i][j−1] 得到
- 示例如下:
代码:
def lcs(a, b):
lena = len(a)
lenb = len(b)
c = [[0 for i in range(lenb + 1)] for j in range(lena + 1)]
flag = [[0 for i in range(lenb + 1)] for j in range(lena + 1)]
for i in range(lena):
for j in range(lenb):
if a[i] == b[j]:
c[i + 1][j + 1] = c[i][j] + 1
flag[i + 1][j + 1] = 'ok'
elif c[i + 1][j] > c[i][j + 1]:
c[i + 1][j + 1] = c[i + 1][j]
flag[i + 1][j + 1] = 'left'
else:
c[i + 1][j + 1] = c[i][j + 1]
flag[i + 1][j + 1] = 'up'
return c, flag
def printLcs(flag, a, i, j):
if i == 0 or j == 0:
return
if flag[i][j] == 'ok':
printLcs(flag, a, i - 1, j - 1)
print a[i - 1]
elif flag[i][j] == 'left':
printLcs(flag, a, i, j - 1)
else:
printLcs(flag, a, i - 1, j)
a = 'ABCBDAB'
b = 'BDCABA'
c, flag = lcs(a, b)
for i in c:
print i
print ''
for j in flag:
print(j)
print ''
printLcs(flag, a, len(a), len(b))
print ''
3. 示例: 最大子段和
问题描述:
- 给定n个整数(可能为负数)组成的序列a1,a2,…,an
- 求该序列形如下式的子段和的最大值: max∑jk=iak
- 当所有整数均为负整数时定义其最大子段和为0
- 依次定义,所求的最优值为: max{0,max1≤i≤j≤n∑jk=iak}
- 例如: (a1,a2,a3,a4,a5,a6)=(-2,11,-4,13,-5,-2),该序列的最大子段和为20
算法设计:
- 通过对分治算法的分析可知,若记: b[j]=max1≤i≤j{∑jk=ia[k]}(i≤j≤n)
- 则所求的最大子段和为: max1≤i≤j⩽n∑jk=ia[k]=max1≤j≤n(max1≤i≤j∑jk=ia[k])=max1⩽j⩽nb[j]
- 由 b[j] 的定义可知:当 b[j−1]>0 时: b[j]=b[j−1]+a[j] ;否则: b[j]=a[j]
- 由此可得 b[j] 的动态规划递归式: b[j]=max { b[j−1]+a[j],a[j] } (1≤j≤n)
- 时间复杂度: O(n)
代码:
int MaxSum (int n, int *a) {
int sum = 0, b = 0;
for(int i=1; i<=n; i++){
if(b > 0)
b += a[i];
else
b = a[i];
if(b > sum)
sum = b;
}
return sum;
}
4. 示例: 0-1背包问题
问题描述:
- 给定:n种物品和一个背包
- 物品 i 的重量是 wi,其价值为 vi
- 背包的容量为:Capacity
- 约束条件:
- 对于每种物品,旅行者只有两种选择:放入或舍弃
- 每种物品只能放入背包一次
- 问题:如何选择物品,使背包中物品的总价值最大?
递归定义最优值:
- 设所给0-1背包问题的子问题的最优值为:
m(i,c)
- 即: m(i,c) 是如下0-1背包问题的最优值:背包容量为 c,可选择物品为{ i,i+1,…,n }
- 显然: 0-1背包问题具有最优子结构性质
- 根据最优子结构性质可以建立如下递归式:
算法示例:
下表是至底向上,从左到右生成的。其中,第5行表示只有第5个物品时,背包容量不同的情况下所对应的最大总价值;第4行表示有4,5两个可选物品时的背包最大总价值。
用子问题定义状态:即f[i][c]表示前i件物品恰放入一个容量为c的背包可以获得的最大价值。则其状态转移方程便是:
max(f[i-1][c], f[i-1][c-w[i]]+v[i])
这个式子表示,在前i件物品放进容量c的背包时,考虑两种情况
- 第一种是第i件不放进去,这时所得价值为:f[i-1][c]
- 第二种是第i件放进去,这时所得价值为:f[i-1][c-w[i]]+v[i],就是如果第i件放进去,那么在容量c-w[i]里就要放进前i-1件物品,得到在容量c-w[i]的情况下,放进前i-1件物品的最大价值再加上第i个物品的价值,就是放进前i件物品的最大价值。
最后比较第一种与第二种所得价值的大小,哪种相对大,f[i][c]的值就是哪种。
完整代码如下:
capacity = 10
w = [4, 5, 6, 2, 2]
v = [6, 4, 5, 3, 6]
f = [[-1 for i in range(capacity+1)] for j in range(len(w))]
def get_f(i, c):
if i == 0:
return v[i] if w[i] <= c else 0
else:
if c >= w[i]:
return max(f[i-1][c], f[i-1][c-w[i]]+v[i])
else:
return f[i-1][c]
for c in range(capacity+1):
for i in range(len(w)):
f[i][c] = get_f(i, c)
print f