Speeding Up Dynamic Programming Computation: Tips and
作者:禅与计算机程序设计艺术
1.简介
动态规划(Dynamic programming)是解决最优化问题的核心算法。该方法通过利用子问题的 cached results来节省时间,并具有广泛的适用性。经过几十年的发展后,在计算机科学领域内已发展成为一个重要的研究方向。然而,在实现和分析该方法方面仍面临诸多挑战。本文探讨了该方法的一些实施技巧。
在实现动态规划算法时,需要注意以下几个方面:
状态转移方程:用于描述各子问题之间的关系。在动态规划算法中, 状态转移方程是核心机制, 它用于描述各子问题之间的关系, 是许多优化算法的基础性工具。当前多数动态规划算法都采用固定的模式构建状态转移方程, 即每个子问题是基于前一个阶段的结果展开计算的。
优化方向:动态规划算法主要依赖自顶向下的递归方式来求解问题。然而,在实际应用中存在多种优化方向;例如,在已知前一状态的情况下,后续状态可以直接计算得出。此外,在实现过程中可以结合备忘录法等技术以进一步提高效率。
3、子problem重叠性:在动态规划算法求解过程中会出现多个相同的中间状态被反复求解的现象称为"subproblem overlapped"现象。对于一些特定情况而言可以通过滚动数组等技术手段来减少冗余运算带来的效率损失
4、终止条件:动态规划算法通常都具备终止条件,在遇到无法进一步扩展的情况时被视为结束点。然而,在判定终止条件这一点上仍然面临较大的挑战。
5、路径压缩:在回溯动态规划算法的过程中,我们经常会出现所谓的链式反应现象,在这种情况下某些子问题会被反复计算。一种有效的方法就是实施路径压缩,在这种情况下只需要记录最后一个被访问的节点即可。
本文旨在通过提供一些Python案例来阐述这些实现技巧的应用。
为了让读者更容易理解,作者在这篇文章中会做以下假设:
1、使用二维数组保存动态规划问题的状态转移方程和子问题之间的关系。
2、图论问题使用邻接矩阵表示。
3、网络流问题使用费用矩阵表示。
4、所有输入数据都是整数。
所提及的优化技巧采用了简便有效的措施,并未涉及复杂的数学推导或理论证明
6、读者具备Python编程能力。
如果读者不具备上述条件,则建议先补充相关知识。
2.背景介绍
2.1 什么是动态规划
dynamic programming(动态规划)是一种高效的决策分析工具,在数学建模中被广泛应用于描述由一系列连续决策组成的复杂系统中各要素之间的相互关系所形成的最优解模型。它通过分解为简单子问题的方法来寻求整体最优解,并且其核心理念是利用最优子结构性质将复杂问题分解为若干子问题。经过数百年的理论与实践探索,在多个领域取得了显著成果。其具体应用场景广泛应用于数学、工程学、经济学等多个领域,并且这一方法论体系在实践中展现出显著的优势:当前状态能够决定后续状态的发展轨迹。这使得动态规划具备了一定程度上的自我调节与优化能力
动态规划被广泛应用于多个领域,在机器学习、零售和军事等多个方面发挥着重要作用。例如,在机器学习中用于解决复杂的机器翻译任务,在零售业中实现精准的商品推荐系统,在军事领域辅助制定战略规划等策略性问题。该算法凭借其高效性和接近最优解的特点,在金融、能源等领域也被用来优化资源分配问题中的数值计算问题。
2.2 为什么要用动态规划
动态规划通常用于解决最优化问题,在这种情况下它将复杂的问题分解为若干子问题,并按照顺序依次解决这些子问题。经过优化处理后,在时间复杂度方面较同类算法具有更高的效率,并且能够有效避免重复运算从而显著提高运行速度。动态规划方法的应用范围非常广泛包括计算最短路线的最大流量背包问题等多种情况,并且在股票交易零钱兑换单源最短路径等问题中也得到了广泛应用。
3.基本概念术语说明
3.1 子问题
动态规划算法旨在解决一个复杂的优化目标。该优化目标可分解为多个相互关联的子优化目标,并且每个子优化目标都可被视为一个相对独立但又相互重叠的小规模优化问题。对于给定的一个大规模优化问题而言,在缺乏有效的优化策略时,默认采用暴力法将导致计算量呈指数级增长(即2的n次方次运算),从而使得该方法的时间效率变得极为低下。因此我们不得不深入分析这些小规模优化间的关联性与共性
两个或多个子问题之间存在重叠性时,若它们共享相同的解,则表明该子问题是重叠子problem.对于复杂problem而言,在采用分治策略时首先要解决的是所有这些重叠subproblem.然后通过组合这些subproblem的解来获得整个complex problem的答案.这种通过将复杂任务分解为若干相同或相似的部分来解决问题的方法被称为分治法.其基本思路是先将主要的问题分解为若干个较小且相似的小规模的问题(称为subtasks),再分别递归地解决每个小规模的问题最后将各个小规模的问题综合起来得到最终答案.
每个数学优化问题是都可以被分解为若干个规模较小且相互独立的小规模优化(subproblem)集合。这些小规模优化(subproblem)在求解时彼此之间相互独立,并不影响对方。动态规划算法的核心目标在于确定一种最优策略,在此策略下,原优化(optimization)的所有小规模优化(subproblem)与其自身的求解均保持相互独立的状态。从而可以在各个层次上的小规模优化(subproblem)中应用动态规划方法来逐步解决整个复杂的数学优化(optimization)任务。
3.2 状态空间和状态转移方程
动态规划算法主要依据子问题的概念, 将复杂的问题分解为其各自的小规模解. 在解决问题的过程中, 首先需明确地界定出状态空间, 并在此基础上明确子问题与其对应的各个状态之间的对应关系. 接下来, 可以根据上述的状态定义, 设计出描述两两状态间转换关系的状态转移方程. 这一过程主要包括确定两个相邻状态下各变量之间的相互转化规律以及变量间的转换条件三部分
1、选择状态:选择当前状态,以便从当前状态转变为下一状态。
2、状态值函数:当前状态下,达到目标状态的最佳方案是什么?
状态值函数的递推关系:当前状态的数值等于当前状态下所选取下一状态的数值,并在此基础上加上其他有助于确保最优解成立的状态边界条件的数值。
3.3 基本元素——最优子结构
动态规划算法的基本概念在于局部最优与全局最优之间的权衡关系。从全局视角来看,在满足所有约束条件的情况下寻求一个最优化方案。然而,在实际应用中寻找这样的最优点往往面临计算复杂性问题。因此,在解决这类问题时通过采用贪心策略来获得一个近似于真实解的最佳解决方案这被称为‘子结构特性’的一种近似处理方法。
局部优化策略旨在分阶段寻求较小区块内的最佳解决方案。这与一次性追求全局最优化的目标不同。因此,在实施过程中可以通过逐步积累各阶段的局部优化来接近整体最优化。最终则能够实现整个问题的整体最优化目标。
因此,在动态规划算法中存在两个关键要素:核心要素是最优子结构性质及其相互独立的小问题特征。当一个优化问题嵌套于另一个优化问题之中时,则表明前者拥有更优的情况。举个例子来说,在某些情况下一个优化方案能够被分解为若干个相互独立的小步骤,并且每个步骤都能达到局部最优化效果。
4.具体算法
4.1 最长公共子序列问题
4.1.1 基本思路
对于两个给定的字符串S₁和S₂来说,在计算它们之间的最长公共子序列(LCS)时旨在寻找一个特定序列
最长公共子序列问题与字符串处理密切相关,在这些技术领域具有重要应用
4.1.2 状态空间和状态转移方程
对于任意两个字符串s1[1..m]和s2[1..n],它们的最长公共子序列问题可以定义为:
LCS(i,j)=max{LCS(i-1,j),LCS(i,j-1)}+1; if s1[i]==s2[j]; else LCS(i,j)=0;
其中,LCS(i,j)表示s1[1..i]和s2[1..j]的最长公共子序列的长度。
状态空间:令dp[i][j]表示s1[1...i]和s2[1...j]的最长公共子序列的长度。
转移方程:
1、初始化:令dp[0][j]=0,0<=j<n; dp[i][0]=0,0<=i<m;
2、转移:如果s1[i]==s2[j],那么LCS(i,j)=LCS(i-1,j-1)+1,否则LCS(i,j)=0;
3、边界条件:LCS(i,j)=LCS(i-1,j)+1 if i>0;
LCS(i,j)=LCS(i,j-1)+1 if j>0;
4.1.3 实现方式
在python中,可以使用动态规划法解决最长公共子序列问题。
def lcs(s1, s2):
m = len(s1)
n = len(s2)
# 初始化数组
dp = [[0 for _ in range(n + 1)] for _ in range(m + 1)]
max_len = 0
end_pos = (0, 0)
# 根据状态转移方程更新dp数组
for i in range(1, m + 1):
for j in range(1, n + 1):
if s1[i - 1] == s2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
if dp[i][j] > max_len:
max_len = dp[i][j]
end_pos = (i, j)
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
return ''.join([s1[end_pos[0] - l : end_pos[0]] for l in range(max_len)])
print(lcs("ABCDGH", "AEDFHR")) # ADH
代码解读
4.2 背包问题
4.2.1 基本思路
在运筹学领域中,背包问题是经典的优化模型。该模型旨在探讨如何在有限资源条件下实现最佳配置以获得最大收益或最小成本。具体而言,在一个有限容量的存储系统中(例如一个仓库),目的是从其中选出一组物品并将其装入一个受限容量的背包装载装置中(例如运输工具),以实现两个主要目标:一是确保装载物品总量不超过系统承载能力;二是使所选物品的整体价值最大化或成本最小化)。该模型不仅在物流与供应链管理中有广泛应用,在生产计划与资源分配方面也发挥着重要作用。同时,在组合优化领域中,背包问题被归类为一种特殊的决策模型(例如动态规划算法常用于解决这类NP难的问题)。此外,在有向无环图框架下求解最优化任务的问题中也包含了背包问题作为其特例(例如通过拓扑排序方法来确定最优路径)。
一般来说,背包问题可以表示为如下标准型:
Maximize sum of values given weights W, capacity C, and item prices P
0 <= wi, ci <= 1, pi >= 0
其中,W=(w1, w2,..., wm)是物品的权重列表,C是背包的容量,P=(p1, p2,..., pm)是物品的价格列表。
4.2.2 状态空间和状态转移方程
我们必须充分应用动态规划算法来建立模型。
首先必须将复杂的问题划分为若干子问题,并明确其状态空间。
我们用S(k)来表示从前k个物品中选择所能获得的最大总价值。
状态变量S(k)的定义基于前k个物品是否被选中这一前提条件。
存在两种选择方案:其中一种选择方式是选取第k号物品,另一种选择方式则是不选取第k号物品。这两种选择分别对应着不同的状态变量。状态变量S(k)其定义式如上所示:
如果选取第k件物品,则S(k)=Pi+(S(k-1)-Pi),0<k<=m,其中Pi=P(k)是第k件物品的价值;
如果不选取第k件物品,则S(k)=S(k-1),0<k<=m。
由此可见,在解决这一问题时可以通过构建一个有向图网络来进行建模分析。每一个节点代表一个问题状态而各条边则描述了不同状态间的转换路径其中各条边的选择情况对应于是否选取第k个物品。
状态转移方程:
S(0)=0,表示不选择任何物品时的最大总价值;
S(1)=P(1);
S(k)=max{S(k-1)},if k==1;
max{S(k-1), Pi+(S(k-2)-Pi)}; k>1;
其中,Pi=P(k)是第k件物品的价值。
4.2.3 实现方式
在python中,可以使用动态规划法解决背包问题。
import numpy as np
def knapsack(weights, profits, capacity):
"""
Knapsack problem using dynamic programming algorithm.
Args:
weights: a list of integers represents the weight of each item.
profits: a list of integers represents the profit of each item.
capacity: an integer represents the maximum weight that can be carried by the knapsack.
Returns:
A tuple containing two lists: [selected items index], [selected items value]. The selected items are sorted
according to their corresponding indices in ascending order. If no solution is found, both lists will be empty.
"""
m = len(weights)
n = capacity
# Initialize arrays
dp = np.zeros((m + 1, n + 1))
prev_state = np.zeros((m + 1, n + 1)).astype('int')
# Fill array bottom-up manner
for i in range(1, m + 1):
for j in range(1, n + 1):
# Case 1: exclude current item from consideration
excluded_profit = dp[i - 1][j]
excluded_weight = dp[i][j - weights[i - 1]]
# Case 2: include current item into consideration
included_profit = profits[i - 1] + excluded_profit
included_weight = min(excluded_weight, excluded_profit + weights[i - 1])
# Choose optimal choice based on computed results
if included_profit > excluded_profit or (included_profit == excluded_profit and included_weight < excluded_weight):
dp[i][j] = included_profit
prev_state[i][j] = i
elif included_weight > excluded_weight:
dp[i][j] = excluded_profit
prev_state[i][j] = i - 1
# Reconstruct items selection based on DP table and previous states
selected = []
value = float('-inf')
for j in reversed(range(1, n + 1)):
while True:
i = int(prev_state[i][j])
if i == 0: break
if dp[i][j]!= dp[i - 1][j]:
selected += [i - 1]
value = dp[i][j]
continue
return [sorted(selected)], [value]
# Example usage:
weights = [2, 3, 4, 5, 6]
profits = [7, 9, 11, 13, 15]
capacity = 8
result = knapsack(weights, profits, capacity)
print('Selected items:', result[0])
print('Total value:', result[1])
代码解读
输出:
Selected items: [0, 1, 2]
Total value: 33
代码解读
