动态规划--打家劫舍-零钱兑换-算法刷题01
1. 概念
关于动态规划这类问题 强烈建议学完下面的帖子:
https://blog.csdn.net/qq_16664581/article/details/89598243
理解动态规划的使用场景强烈建议读一下这个故事:
https://www.cnblogs.com/sdjl/articles/1274312.html
步骤:
- 确定问题 (可能是求某个问题的最优解 例如:背包问题中 装的物品价值最大,货币兑换问题,使用的币数量最少,或者是后一步的结果依赖前一步 的这类问题)
- 问题分解为子问题(背包问题里面第i个物品 可以选择那与不拿, 货币兑换问题,最大面值的零钱 使用与不使用)
- 写出状态转移方程
f(n) = f(n-1) + f(n-1)
f(n) = best_of[f(n-1) , f(n-1))] - 确定边界
2. 打家劫舍
链接:https://leetcode.cn/problems/house-robber/description/
假设v(i)表示打劫 0–i能够获得的最大赃款
当你来到房间i时,你有两个选择:
- 偷i,然后计算从前面i-2间里偷出的最大值,因为i-1不能再偷了。偷完把v(i)加到总赃款中。
- 不偷i,这样你可以自由地选择前面的i-1间来使得赃款最大化。因为没偷,就不能往总赃款里加值。
实现1:
from typing import List
class Solution:
def rob(self, nums: List[int]) -> int:
def dp(i):
if i == 0: return 0
if i == 1: return nums[0]
val = max(dp(i - 2) + nums[i - 1], dp(i - 1))
return val
return dp(len(nums))
if __name__ == '__main__':
nums = [2, 7, 9, 3, 1]
s = Solution()
res = s.rob(nums)
print(res)
实现2--加入缓存
from typing import List
class Solution:
def rob(self, nums: List[int]) -> int:
cache = {}
def dp(i):
if i == 0: return 0
if i == 1: return nums[0]
if i in cache:
return cache[i]
val = max(dp(i - 2) + nums[i - 1], dp(i - 1))
cache[i] = val
return val
return dp(len(nums))
if __name__ == '__main__':
nums = [2, 7, 9, 3, 1]
s = Solution()
res = s.rob(nums)
print(res)
实现3--递归公式
from typing import List
class Solution:
def rob(self, nums: List[int]) -> int:
a = b = 0
for v in nums:
a, b = b, max(a+v, b) # b赋值给a new_b赋值给b
return b
if __name__ == '__main__':
nums = [2, 7, 9, 3, 1]
s = Solution()
res = s.rob(nums)
print(res)
3 零钱兑换
链接:https://leetcode.cn/problems/coin-change/description/
coins 已经从小到大排好序
定义: f(i, t)
i:面值的子集
t: 目标金额
使用面值子集i 来构成目标金额 t 所使用的硬币最小数量
面值子集i: coins[0], coins[1], … coins[i], i之后不考虑
原问题转化成: f(n-1, amount)
地推公式:
说明:
正常情况下 f(i, t)的计算是分成两支 一支向上走 一只支向下走 取这两者的较小值
假设面值集合i 里面的最大值 val=coins[i]
向上走 代表继续使用最大面值 转化成 f(i, t-val) + 1, 之所以要+1 是因为使用了一次 最大面值,特殊情况:
当t<val的时候 代表最大面值已经超过了t 无法拼出t 这是返回一个无穷大,可以将该分支剪掉, 因为min的运算 取更小值 值为无穷大的 肯定会被抛弃掉
当t=val 代表只能用一个最大面值 拼成 目标值t
其他情况 返回f(i, t-val) + 1
向右走 代表不再使用最大面值 正常情况为f(i-1, t)
当 i==0 已经不能再往右走 返回无穷大 舍弃掉该分支即可
from typing import List
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
# 异常处理
if not amount:
return 0
mini_coin_exist = False
for co in coins:
if co <= amount:
mini_coin_exist = True
if not mini_coin_exist:
return -1
if len(coins) == 1 and amount % coins[0] != 0:
return -1
# 核心逻辑
n = len(coins)
cache = {}
def _dp(i, t):
if (i, t) in cache:
return cache[(i, t)]
val = coins[i]
# 使用val
if t < val:
use_val = float('inf')
elif t == val:
use_val = 1
else:
use_val = _dp(i, t - val) + 1
# 不使用val
no_val = float('inf') if i == 0 else _dp(i - 1, t)
res = min(use_val, no_val)
cache[(i, t)] = res
return res
result = _dp(n - 1, amount)
# 没有方案的情况
if result == float("inf"):
return -1
return result
if __name__ == '__main__':
# coins = [1, 2147483647]
# amount = 2
coins = [384, 324, 196, 481]
amount = 285
s = Solution()
res = s.coinChange(coins, amount)
print(res)