关于动态规划
Posted 金带头鱼
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于动态规划相关的知识,希望对你有一定的参考价值。
文章顺序为:
基本动态规划 - 01背包 - 完全背包 - 多重背包
本文仅介绍基本的 dp 和相应的例题
一些经典/特别的动态规划题目 和股票问题 会单开一个章节来写
动态规划类型最主要的还是要找到 状态转移方程 和 数据的初始化
最常规的动态规划 一般都能通过递归的方式解决
个人刷这类题的时候 如果明显能找到递归的解答方式 则从递归式中找出状态转移方程 .
或者在较大的数据中看局部. 一般都是看开头的几项, 在开头几个有限的数据中也能找到状态转移式
不同的题状态转移式没有什么统一的规律. 具体题目具体分析
下面先列出几道最典型/ 简单的动态规划
之后会介绍 背包问题中的 01背包 完全背包 股票系列问题等
虽然比喻不是很准确. 但是斐波那契数列感觉可以当作一个状态转移式
[1, 1, 2, 3, 5, ......] 中 f(n) = f(n-1) + f(n-2) 这个就可以看作一种状态转移式
# leetcode 322 题 零钱兑换 这道题算是最简单的入门动态规划的题吧
# 给定一组硬币, 找出凑成指定金额的最少硬币数量
# coins = [1, 2, 5], amount = 11
凑成11 的最少硬币数量 只和 凑曾
[10, 9, 6]元硬币的数量相关.可以得出转移式 f(n) = min(f(n-1), f(n-2), f(n-5)) + 1
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
dp = {0:0}
coins = set(coins)
for i in range(1, amount+1):
m = float("inf")
for coin in coins:
if dp.get(i-coin, -1) == -1:
continue
m = min(m, dp[i-coin]+1)
dp[i] = m if m != float("inf") else -1
return dp[amount]
01背包
01背包的特点是 对象只有两种状态.
状态转移式为 N = max(f(n-1), g(n-2)+N) 选择一个最佳值即可
01背包问题一般通过维护一个二维数组来完成计算即可
# leetcode 122 题 股票问题
# 给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
# 设计一个算法来计算你所能获取的最大利润。(多次买卖一支股票)
class Solution:
def maxProfit(self, prices: List[int]) -> int:
ans =
for index, val in enumerate(prices[1:]):
# 买
ans[0].append(max( ans[0][index], ans[1][index] - val ))
# 卖
ans[1].append(max( val + ans[0][index], ans[1][index] ))
return ans[1][-1]
在这道题中 也是维护一个 二维数组 dp[i][j]
i 表示 到数组中的第i 个数为止,
j 表示 到 i个数位置 和为 j
不选择第i个数放到集合时 结果为 dp[i-1][j]
选择 第 i 个数 放到集合时 结果为 dp[i-1][j-nums[i]]
可以得到状态转移方程
dp[i][j] = dp[i-1][j] or dp[i-1][j-nums[i]]
# leetcode 416题 这道题也是典型的01背包问题
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
例 : [1, 5, 11, 5]
class Solution:
def canPartition(self, nums: List[int]) -> bool:
S = sum(nums)
if len(nums) < 2:
return False
if S % 2 != 0:
return False
target = S // 2
dp =
for index, val in enumerate(nums):
for j in range(val, target+1):
if val == j:
dp[index+1][j] = True
continue
dp[index+1][j] = dp[index][j] or dp[index][j-val]
return dp[-1][-1]
416题的优化 , 通过把维护一个二维dp转化为 一维数组 可以减少操作时间
完全背包
完全背包就是在 01背包的基础上解除了状态的限制. 如416题中的 的限制是每个数据只能拿一次. 在完全背包中则是取消了这个限制 , 在思路上和01背包差不多
在下面这道题中和 01背包一样 还是维护一个二维数组
dp[i][j] 其中 i 表示 第i个硬币 j 表示 面额
到这一步为止可以发现和01背包是一样的
在面额 j 中 不用第i枚硬币时的状态为 dp[i-1][j]
仅用 1 枚面额 i硬币时的状态为 dp[i-1][j-coins[i]*1]
仅用 2 枚面额 i硬币时的状态为 dp[i-1][j-coins[i]*2]
仅用 2 枚面额 i硬币时的状态为 dp[i-1][j-coins[i]*3]
....
全部用 i 硬币兑换时的状态为 1
可以得出状态转移式
dp[i][j] = dp[i][j] + dp[i-1][j - coins[i]*1] + dp[i-1][j - coins[i]*2] + ....
# leetcode 518 零钱兑换Ⅱ
# 给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
if not amount:
return 1
dp =
for i, v in enumerate(coins):
for j in range(1, amount+1):
if j % v == 0:
dp[i+1][j] += 1
c = 1
while v * c <= j:
dp[i+1][j] += dp[i][j-c*v]
c += 1
dp[i+1][j] += dp[i][j]
return dp[-1][-1]
如同上面的题一样 因为维护的是一个较大的二维数组 所以执行时间会很长
下面给出一个优化的版本
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
dp =
for i in coins:
for j in range(i, amount+1):
dp[j] += dp[j-i]
return dp[-1]
多重背包 是在完全背包的基础上 限制了 每种操作的上限数量
比如 在硬币问题中 每种硬币的数量从无限个 改为了有限个
解决多重背包最简单的方式是 将多重背包问题转化为01背包
如 一个物品最多可以拿N个 . 可以拆成 N个物品. 每种物品 拿一个, 但是这样导致的问题是 可能会让数据量变得庞大.
另外一个通用的方法是 二进制表示法 -- 二进制表示法 等我学会了再更新
dp[i][j] 中 i表示 0 的数量 j表示1 的数量
当选择用第 n个字符串时
dp[i][j] = 1 + dp[i-count_s(0)][j-coust_s(1)]
不选第N个字符时
dp[i][j] = dp[i][j] 取两者的最大值即可
因为计算图画出来感觉不太好理解 所以没贴
# leetcode 474 题 1和0
现在,假设你分别支配着 m 个 0 和 n 个 1。另外,还有一个仅包含 0 和 1 字符串的数组。
你的任务是使用给定的 m 个 0 和 n 个 1 ,找到能拼出存在于数组中的字符串的最大数量。每个 0 和 1 至多被使用一次。
class Solution:
def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
if len(strs) == 0:
return 0
dp =
for strs_item in strs:
item_count0 = strs_item.count('0')
item_count1 = strs_item.count('1')
for i in range(m, item_count0 - 1, -1):
for j in range(n, item_count1 - 1, -1):
dp[i][j] = max(dp[i][j], 1 + dp[i-item_count0][j-item_count1])
return dp[-1][-1]
以上是关于关于动态规划的主要内容,如果未能解决你的问题,请参考以下文章