关于动态规划

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 = [[-prices[0]], [0]] 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 = [[False] * (target+1) for i in range(len(nums)+1)] 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 = [ [0]*(amount+1) for i in range(len(coins) + 1)] 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 = [1] + [0] * (amount) 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 = [[0]*(n+1) for _ in range(m+1)] 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]


以上是关于关于动态规划的主要内容,如果未能解决你的问题,请参考以下文章

关于动态规划算法,哪位可以讲一下自己心得体会?

关于动态规划

动态规划基本思想

关于动态规划算法的总结

关于对动态规划的思考

关于动态规划的思考