动态规划是一种通过将复杂问题分解为更小的子问题来求解的技术,子问题的解会被存储起来,避免重复计算,从而提高效率。动态规划广泛应用于优化问题中,例如最短路径问题、背包问题、字符串匹配等。本文详细介绍了动态规划的基本概念、术语、核心思想、特点及其实现方法,帮助读者全面理解动态规划。
动态规划基础概念介绍动态规划是一种通过将复杂问题分解为更小的子问题来求解问题的算法技术。子问题的解会被存储起来,这样在遇到相同的子问题时,可以直接使用已存储的解而无需重复计算。动态规划广泛应用于优化问题中,例如最短路径问题、背包问题、字符串匹配等。动态规划的核心是通过自底向上的方式解决子问题,从而构建出整个问题的解。
基本术语-
状态:一个问题的某个子问题的解称为该子问题的状态。状态通常用一个或多个变量表示,例如,在背包问题中,状态可以是背包的容量和已放入背包的物品总量。
-
状态转移:状态之间的关系定义了从一个状态如何转移到另一个状态。状态转移通常通过递推公式来描述,例如,背包问题的状态转移公式可以是
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i])
,其中dp[i][j]
表示前i
个物品在背包容量为j
时的最大价值。 -
最优子结构:如果某个问题的最优解包含了其子问题的最优解,则称该问题具有最优子结构。例如,在背包问题中,前
i
个物品在背包容量为j
时的最大价值依赖于前i-1
个物品在相同容量下的最大价值。 -
重叠子问题:如果同一个子问题在问题求解过程中多次出现,则称该问题具有重叠子问题性质。动态规划通过保存已经解决过的子问题的结果来避免重复计算,从而提高效率。
- 递归与迭代:动态规划既可以使用递归实现,也可以使用迭代实现。递归实现适合于较小的输入规模或特定的问题结构,迭代实现通常是在较大的输入规模或需要优化内存使用的情况下更优的选择。
示例代码
下面是一个简单的动态规划示例代码,用于求解斐波那契数列的第n项:
def fibonacci(n):
if n <= 1:
return n
dp = [0] * (n + 1)
dp[0], dp[1] = 0, 1
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
# 测试代码
print(fibonacci(10)) # 输出第10项斐波那契数列
动态规划的核心思想与特点
动态规划的核心思想是将问题分解成一系列子问题,然后通过求解这些子问题来构建出原问题的解。动态规划方法通常采用自底向上的方式,从最小的子问题开始求解,逐步构建到原问题的解。
核心思想
- 划分问题:将原问题划分为多个相互独立的子问题。
- 求解子问题:求解这些子问题,通常使用递推公式。
- 合并子问题的解:在求解子问题的基础上,利用子问题的解来求解原问题。
- 存储子问题的解:避免重复计算子问题,提高算法效率。
特点
- 最优子结构:问题的最优解包含其子问题的最优解。
- 重叠子问题:问题的解可以通过重复计算子问题的解来获得。
- 状态转移方程:定义状态之间的关系,描述如何从一个状态过渡到另一个状态。
- 自底向上:从解决最小的子问题开始,逐步构建到原问题的解。
示例代码
以下是一个动态规划示例,用于解决经典的背包问题。该问题的目标是在给定容量的背包中,选择若干物品使背包内物品总价值最大。
def knapsack(capacity, weights, values, n):
dp = [[0 for _ in range(capacity + 1)] for _ in range(n + 1)]
for i in range(1, n + 1):
for w in range(capacity + 1):
if weights[i - 1] <= w:
dp[i][w] = max(dp[i - 1][w], dp[i - 1][w - weights[i - 1]] + values[i - 1])
else:
dp[i][w] = dp[i - 1][w]
return dp[n][capacity]
# 测试代码
weights = [1, 2, 3]
values = [6, 10, 12]
capacity = 5
print(knapsack(capacity, weights, values, len(values))) # 输出背包最大价值
动态规划的入门案例分析
背包问题是一个经典的动态规划问题,其目标是在给定背包容量的情况下,选择若干物品放入背包中,使得背包内物品的总价值最大。该问题可以进一步分为0/1背包问题和完全背包问题,0/1背包问题表示每个物品只能选择一次,而完全背包问题表示每个物品可以被选择多次。
0/1背包问题
0/1背包问题中的每个物品只能选择一次。给定一个集合S = {w1, w2, ..., wn}
,其中wi
表示第i
个物品的重量,vi
表示第i
个物品的价值;以及一个背包容量W
。目标是选择一些物品放入背包,使得总重量不超过W
,并且总价值最大。
状态定义
dp[i][j]
表示从前i
个物品中选择,且总重量不超过j
的最大价值。
状态转移方程
-
dp[i][j] = max(dp[i-1][j], dp[i-1][j-wi] + vi)
,表示选择第i
个物品或不选择第i
个物品。 - 初始化
dp[0][j] = 0
,表示没有物品时的最大价值为0。 - 初始化
dp[i][0] = 0
,表示背包容量为0时的最大价值为0。
示例代码
以下是一个0/1背包问题的动态规划实现:
def knapsack_01(weights, values, capacity):
n = len(weights)
dp = [[0 for _ in range(capacity + 1)] for _ in range(n + 1)]
for i in range(1, n + 1):
for j in range(capacity + 1):
if weights[i - 1] > j:
dp[i][j] = dp[i - 1][j]
else:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weights[i - 1]] + values[i - 1])
return dp[n][capacity]
# 测试代码
weights = [1, 3, 4, 5]
values = [1, 4, 5, 7]
capacity = 7
print(knapsack_01(weights, values, capacity)) # 输出最大总价值
完全背包问题
完全背包问题中的每个物品可以被选择多次。给定一个集合S = {w1, w2, ..., wn}
,其中wi
表示第i
个物品的重量,vi
表示第i
个物品的价值;以及一个背包容量W
。目标是选择一些物品放入背包,使得总重量不超过W
,并且总价值最大。
状态定义
dp[j]
表示总重量不超过j
的最大价值。
状态转移方程
dp[j] = max(dp[j], dp[j - wi] + vi)
,表示选择第i
个物品或不选择第i
个物品。
示例代码
以下是一个完全背包问题的动态规划实现:
def knapsack_complete(weights, values, capacity):
dp = [0 for _ in range(capacity + 1)]
for i in range(len(weights)):
for j in range(weights[i], capacity + 1):
dp[j] = max(dp[j], dp[j - weights[i]] + values[i])
return dp[capacity]
# 测试代码
weights = [1, 3, 4, 5]
values = [1, 4, 5, 7]
capacity = 7
print(knapsack_complete(weights, values, capacity)) # 输出最大总价值
动态规划常用算法讲解
动态规划有多种常用算法,包括线性动态规划、二维动态规划等。下面将分别介绍这些算法及其应用场景。
线性动态规划
线性动态规划是一种一维的状态转移方式,通常用于解决可以分解成一维子问题的问题。线性动态规划广泛应用于字符串匹配、最长递增子序列等问题。
状态定义
dp[i]
表示前i
个元素的状态。
状态转移方程
dp[i] = max(dp[i], dp[j] + 1)
,其中j < i
,表示通过前j
个元素的状态转移。
示例代码
以下是一个线性动态规划的实现,用于求解最长递增子序列问题:
def longest_increasing_subsequence(arr):
dp = [1] * len(arr)
for i in range(1, len(arr)):
for j in range(i):
if arr[i] > arr[j]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)
# 测试代码
arr = [10, 9, 2, 5, 3, 7, 101, 18]
print(longest_increasing_subsequence(arr)) # 输出最长递增子序列长度
二维动态规划
二维动态规划是一种二维的状态转移方式,通常用于解决可以分解成二维子问题的问题。二维动态规划广泛应用于背包问题、最长公共子序列等问题。
状态定义
dp[i][j]
表示前i
个元素和前j
个元素的状态。
状态转移方程
dp[i][j] = max(dp[i-1][j], dp[i][j-1], dp[i-1][j-1] + 1)
,表示通过前i-1
个元素、前j-1
个元素或同时转移的状态。
示例代码
以下是一个二维动态规划的实现,用于求解最长公共子序列问题:
def longest_common_subsequence(s1, s2):
m, n = len(s1), len(s2)
dp = [[0 for _ in range(n + 1)] for _ in range(m + 1)]
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
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
return dp[m][n]
# 测试代码
s1 = "ABCBDAB"
s2 = "BDCAB"
print(longest_common_subsequence(s1, s2)) # 输出最长公共子序列长度
动态规划问题的解决步骤与技巧
解决动态规划问题通常遵循以下步骤:
-
定义状态
- 确定问题的子结构,通常用一个或多个变量表示子问题的解。
- 选择合适的状态变量,通常是子问题的解。
-
状态转移方程
- 根据问题的性质,写出状态之间的转移公式。
- 通常包括选择某个子问题的解,或合并多个子问题的解。
-
初始化
- 初始化边界条件,通常是初始状态。
- 例如,初始化
dp[0][j]
为0,表示没有物品时的最大价值为0。
-
结果计算
- 根据状态转移方程,计算每个状态的值。
- 最终的结果通常保存在某个状态变量中。
- 返回结果
- 返回最终的结果,通常是
dp
数组中的某个值。
- 返回最终的结果,通常是
应用技巧
- 状态压缩:通过优化状态变量的数量来减少状态空间,从而提高效率。
- 空间优化:通过优化状态变量的存储方式来减少空间复杂度。
- 状态重用:通过保存已计算的状态来避免重复计算子问题。
示例代码
以下是一个示例代码,展示了如何在动态规划中使用状态压缩来优化空间复杂度:
def longest_increasing_subsequence(s):
dp = [1] * len(s)
for i in range(1, len(s)):
for j in range(i):
if s[i] > s[j]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)
# 测试代码
s = "bcabcba"
print(longest_increasing_subsequence(s)) # 输出最长递增子序列长度
通过这些步骤和技巧,可以更有效地解决动态规划问题,提高算法的效率。推荐的编程学习网站是慕课网。
共同学习,写下你的评论
评论加载中...
作者其他优质文章