目录
1、斐波那契数列
# -*- coding:utf-8 -*-
def fib(n):
"""
递归方式实现斐波那契数列---时间复杂度O(2^n),空间复杂度O(n)
:param n:
:return:
"""
if n < 2:
return n
else:
return fib(n-1) + fib(n-2)
def fib1(n):
"""
dp方式实现斐波那契数列---时间复杂度O(n),空间复杂度O(1)
:param n:
:return:
"""
if n < 2:
return n
dp = [0, 1] # 初始化
for i in range(2, n + 1):
# dp.append(dp[i-1] + dp[i - 2]) # 空间复杂度高 O(n)
dp[0], dp[1] = dp[1], dp[0] + dp[1] # 空间复杂度 O(1)----滚动dp一维数组
return dp[-1]
if __name__ == '__main__':
print(fib(8))
print(fib1(8))
变长的dp动画
滚动的dp动画
2、爬楼梯
题目:
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的⽅法可以爬到楼顶呢?
思路:
递推公式:dp[i] = dp[i-1] +dp[i-2]
dp[i]存储的是上第i阶台阶的方法总数:要不就是i-1阶爬一阶上来,要不就是i-2阶爬2个台阶上来
初始化:和斐波那契数列一波相比只是初始不一样,第一个台阶的方法1种,第二个台阶方法是2种
遍历顺序:遍历台阶,从第一阶向后
def fib1(n):
"""
dp方式实现爬楼梯---时间复杂度O(n),空间复杂度O(1)
:param n:
:return:
"""
if n < 2:
return n
dp = [1, 2] # 初始化这里和斐波那契不一样
for i in range(2, n + 1):
# dp.append(dp[i-1] + dp[i - 2]) # 空间复杂度高 O(n)
dp[0], dp[1] = dp[1], dp[0] + dp[1] # 空间复杂度 O(1)----滚动dp一维数组
return dp[1]
return dp[-1]
3、使⽤最⼩花费爬楼梯
LeetCode地址:力扣
题目:数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。
每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。
请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。
思路:
递推公式:dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]
到达第i个台阶所花费的最少体⼒为dp[i]
初始化:dp[0],dp[1] = cost[0],cost[1]
这里dp[1]并没有取min(cost[0],cost[1])因为这里如果去了cost[0],那就没有从
cost[1]作为起点的情况就给排除了,最后的结果也是取最后两个值的最小值也体现
了这个点
到达第i个台阶所花费的最少体⼒为dp[i]
遍历顺序:遍历台阶,即是遍历cost
def fun1(cost):
dp = cost[0:2]
for i in range(2, len(cost)):
dp[0], dp[1] = dp[1], min(dp[0], dp[1]) + cost[i]
return min(dp)
4、不同路径
LeetCode地址:https://leetcode-cn.com/problems/unique-paths/
题目:
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
思路:
递推公式:[i][j]=dp[i - 1][j] + dp[i][j - 1]
走到[i][j]的网格的路径总数,是来自从上、从左来的路径总数
初始化:dp[m][n] = 都是1
遍历顺序:第一行和第一列只有一种路径,所以遍历行从1,列也从1
def uniquePathsWithObstacles(obstacleGrid):
"""二维dp数组解决不同路径问题"""
m, n = len(obstacleGrid), len(obstacleGrid[0])
dp = [[1 for x in range(n)] for y in range(m)]
for i in range(1, m):
for j in range(1, n):
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
return dp[-1][-1]
def uniquePathsWithObstacles(obstacleGrid):
"""一维dp数组解决不同路径问题"""
# 不同路径1中可以使用一维dp数组def uniquePaths(m, n):
dp = [1 for x in range(n)]
for i in range(1, m):
for j in range(1, n):
dp[j] += [j - 1]
return dp[-1]
5、不同路径 II
题⽬链接:https://leetcode-cn.com/problems/unique-paths-ii/
与不同路径的区别
(1)遇到障碍物,说明到达这个网格的路径总数为0
(2)遍历顺序:第一行如果有障碍物,从障碍物向后就不能走了
第一列如果有障碍物,从障碍物向下就不能走了
def uniquePathsWithObstacles(obstacleGrid):
"""二维dp数组解决不同路径问题"""
m, n = len(obstacleGrid), len(obstacleGrid[0])
dp = [[1 for x in range(n)] for y in range(m)]
for i in range(0, m):
for j in range(0, n):
if obstacleGrid[i][j] == 1:#遇到障碍物走到这个网格的路径就为0
dp[i][j] = 0
elif i == 0 and j == 0:
pass
elif i == 0 and j > 0:
dp[i][j] = dp[i][j - 1]
elif j == 0 and i > 0:
dp[i][j] = dp[i - 1][j]
else:
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
return dp[-1][-1]
def uniquePathsWithObstacles(obstacleGrid):
"""一维dp数组解决不同路径问题"""
m, n = len(obstacleGrid), len(obstacleGrid[0])
dp = [1 for x in range(n)]
for i in range(1, m):
for j in range(1, n):
if obstacleGrid[i][j] == 1:
dp[j] //= 2 #遇到障碍物只是阻挡了一个方向所以除以2
else:
dp[j] += dp[j - 1]
return dp[-1]
6、整数拆分
LeetCode地址343题: 力扣
给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。
思路:
递推公式:dp[i] = max(dp[i], dp[i-j]*j, (i-j)*j)
列举拆分所有的情况,dp[i]依赖dp[i-j]的写法
比如:dp[3]拆分情况----
首先尝试拆出来1----dp[3]=0、1*2=2、1*dp[2]=1----dp[3]=2
再次尝试拆出来2----dp[3]=2、2*1=2、2*dp[1]=0----dp[3]=2
dp[4]拆分情况----
首先尝试拆出来1------dp[4]=0、1*3=3、1*dp[3]=2-----dp[4]=3
再次尝试拆出来2------dp[4]=3、2*2=4、2*dp[2]=2-----dp[4]=4
再次尝试拆出来3------dp[4]=4、3*1=3、3*dp[1]=0-----dp[4]=4
初始化:dp[2] = 1
dp[0],dp[1]没有意义,0、1不能拆分,题目也说了从2开始
遍历顺序:遍历dp从3开始,应为2已经初始化了
拆分的场景就是从1到i的所有情况
#方法1:dp动态规划
def integerBreak(n):
dp = [0] * (n + 1)
dp[2] = 1
for i in range(3, n + 1):
for j in range(1, i):
dp[i] = max(dp[i],dp[i-j]*j, (i-j)*j)
return dp[-1]
#方法2:贪心--3这个特殊的数么有啥理论依据
def integerBreak(n):
#贪心
if n == 2:
return 1
if n == 3:
return 2
if n == 4:
return 4
result = 1
while n > 4:
result *= 3
n -= 3
result *= n
return result
背包问题+背包回溯
7、01背包
学习文章:【动态规划】一次搞定三种背包问题 - 弗兰克的猫 - 博客园
题目:一个背包可以装M重的东西,物品5个,重量为[2, 2, 6, 5, 4],价值为[3, 6, 5, 4, 6], 问:如何装可以得到最大的价值?
def bag(bag_w, num, weights, values):
"""
01背包问题
:param bag_w: 背包容量
:param num: 物品个数
:param weights: 物品重量list
:param values: 物品价值list
:return: 描绘的二维数组
"""
# 初始化一个二维数据,放入各种情况下最佳价值,右下角就是最优解
dp = [[0 for row in range(bag_w + 1)] for column in range(num + 1)]
for row in range(1, num + 1):
for col in range(1, bag_w + 1):
if weights[row - 1] > col: # 此时背包容量col小于row物品的容量,放不下row物品
dp[row][col] = dp[row - 1][col]
else:
# 找到去掉row物品容量col - weights[row - 1]对应的row-1个物品的最大价值
t1 = dp[row - 1][col - weights[row - 1]] + values[row - 1]
t2 = dp[row - 1][col]
dp[row][col] = max(t1, t2)
# 找到装入了那些物品----背包回溯
# 从右下角开始找,判断第五个物品是否装进口袋看:装第五个物品的最优解是否大于装第四个物品的最优解,大说明装入了,不大就是没有装入
good_value = dp[-1][-1]
good_n = []
while num > 0 and bag_w > 0:
if dp[num][bag_w] > dp[num - 1][bag_w]:
good_n.append(num)
bag_w -= weights[num - 1]
num -= 1
print("背包最大价值:%s" % good_value)
print("背包物品编号:%s" % good_n[::-1])
def bag1(bag_w, num, weights, values):
# 01背包不做回溯的话,可以使用一维dp滚动数组
# 初始化一个一维数据,放入不同背包容量时最大价值
# 难点在循环顺序
dp = [0 for row in range(bag_w + 1)]
for row in range(0, num):
for col in range(bag_w, weights[row]-1, -1):
dp[col] = max(dp[col], dp[col - weights[row]] + values[row])
return dp[-1]
if __name__ == '__main__':
num = 5 # 5个物品
bag_w = 10 # 背包总容量
weights = [2, 2, 6, 5, 4] # 物品重量集合
values = [6, 3, 5, 4, 6] # 物品价值集合
bag(bag_w, num, weights, values)
bag1(bag_w, num, weights, values)
二维图解
一维图解
(1)分割等和⼦集
LeetCode地址: 力扣
题目:给你一个 只包含正整数 的 非空 数组
nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。思路:将这些nums的元素当做物品,重量=价值=nums对应元素的值,背包假设为nums的和sum(nums)的一半
转化为:背包容量sum(nums)/2,是否有恰好装满的情况(装满了,剩下的一半也是剩下物品的重量了)
递推公式: dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]) (一维)
dp[i][j] = max(dp[i-1][j], dp[i-1][j - nums[i]] + nums[i]) (二维)
初始化:初始值都为0
遍历顺序:和上面的01背包一致
def canPartition(self, nums: List[int]) -> bool:
"01背包的一维数组解法"
total = sum(nums)
if total <= 1 or total % 2 != 0:
return False
target = total // 2
dp = [0] * (target + 1)
for i in range(len(nums)):
for j in range(target, nums[i] - 1, -1):
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])
return dp[-1] == target
def canPartition1(self, nums: List[int]) -> bool:
"01背包的二维数组解法"
total = sum(nums)
if total <= 1 or total % 2 != 0:
return False
target = total // 2
dp = [[0] * (target + 1)] * (len(nums)+1)
for i in range(len(nums)+1):
for j in range(nums[i], target+1):
dp[i][j] = max(dp[i-1][j], dp[i-1][j - nums[i]] + nums[i])
return dp[-1][-1] == target
(2)最后⼀块⽯头的重量 II
LeetCode地址: 力扣
题目:
有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。思路:可以转化为分割等和⼦集的问题,target=total//2
不同点:(1)total不能等分的特殊情况不用处理
(2)子集问题中dp[-1]==target,
这里(total -dp[-1]) -dp[-1],含义将total分2份,一份是dp[-1],一份是total-dp[-1],他们差就是击碎后的最小值了,不理解我们在分析下,(total -dp[-1]) -dp[-1] === 2(target -dp[-1]),这个式子是不是target和dp[-1]的值越相近那结果是不是越小?dp[-1]如果等于target就是等分子集了,是不是就是完美粉碎了?如果还不理解dp[-1]的意义,那就在讲,转化01背包问题:target是一个背包,装石头或者上面等分子集的nums的元素,石头重量即是价值,这样取如何装找到最大价值
递推公式: dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]) (一维)
dp[i][j] = max(dp[i-1][j], dp[i-1][j - nums[i-1]] + nums[i-1]) (二维)
初始化:初始值都为0
遍历顺序:和上面的01背包一致
def lastStoneWeightII(self, stones: List[int]) -> int:
"""一维数组dp解法"""
total = sum(stones)
target = total // 2
dp = [0] * (target + 1)
for i in range(len(stones)):
for j in range(target, stones[i] - 1, -1):
dp[j] = max(dp[j], dp[j - stones[i]] + stones[i])
return total- dp[-1] - dp[-1]
def lastStoneWeightII(self, stones: List[int]) -> int:
"""二维数组dp解法"""
total = sum(stones)
target = total // 2
dp = [[0 for x in range(target + 1)] for y in range(len(stones)+1)]
for i in range(1, len(stones)+1):
for j in range(1, target+1):
if j < stones[i-1]:
dp[i][j] = dp[i-1][j]
else:
dp[i][j] = max(dp[i-1][j], dp[i-1][j - stones[i-1]] + stones[i-1])
return total- dp[-1][-1] - dp[-1][-1]
(3)目标和
LeetCode地址: 力扣
题目:
给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。思路:每个元素有正负的两种情况,是否也是和分割等和⼦集、最后⼀块⽯头的重量 II问题类似,都是将nums分成两份,一份装,一份不装入,装入的是正号,不装入的是负号,是不是也像粉碎石头了?
不同点:这个背包的容量是多少呢?分隔等和子集的背包容量是total的一半,这里呢?一份正数和、一份负数和
正数和 + 负数和 = total
正数和 - 负数和 = target
正数和 = (total + target)/ 2
所以就转化为了正数和的表达式 的数目
递推公式: dp[j] = dp[j]+dp[j - nums[i]] (一维)
dp[i][j] = dp[i-1][j]+dp[i-1][j - nums[i-1]] (二维)
初始化:dp[0] =1、dp[i][0]=0 这个是二维的情况
遍历顺序:和上面的01背包一致
def findTargetSumWays(self, nums: List[int], target: int) -> int:
# 二维数组dp解法
total = sum(nums)
if target > total or (target + total) % 2 != 0 or target < -total:
# 特殊情况处理:target不能大于total,小于-total,如果功能不能整除2,就得不到正数和的部分
return 0
bag_size = (target + total) // 2
dp = [[0 for x in range(bag_size + 1)] for y in range(len(nums) + 1)]
#初始化
for z in range(len(nums)+1):
dp[z][0] = 1
for i in range(1, len(nums)+1):
for j in range(0, bag_size+1): # 这个变量背包从0开始,这里的0有具体的含义了,之前价值0是无意义的了
if j < nums[i-1]:
dp[i][j] = dp[i-1][j]
else:
dp[i][j] = dp[i-1][j - nums[i-1]] + dp[i-1][j]
return dp[-1][-1]
def findTargetSumWays(self, nums: List[int], target: int) -> int:
# 一维数组dp解法
total = sum(nums)
if target > total or (target + total) % 2 != 0 or target < -total:
# 特殊情况处理:target不能大于total,小于-total,如果功能不能整除2,就得不到正数和的部分
return 0
bag_size = (target + total) // 2
dp = [0] * (bag_size + 1)
dp[0] = 1
for i in range(len(nums)):
for j in range(bag_size, nums[i] - 1, -1):
dp[j] = dp[j] + dp[j - nums[i]]
return dp[-1]
(4)一和零
题目链接:474. 一和零
题目:
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
思路:
dp[i][j]的含义:i个0和j个1组成的最大子集长度
递推公式: dp[i][j] = max(dp[i][j], dp[i - m_sum][j - n_sum] + 1)
初始化:0,组成0个0和0个1的字符串是0个
遍历顺序:和上面的01背包一致
题目特点:这里的物品是每一个字符串,背包是m个0和n个1的背包,这个背包不是之前只考虑物品的重量属性了,需要考虑物品的两个属性才能装入这个背包。动图效果多理解下
def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
dp = [[0 for x in range(n + 1)] for y in range(m + 1)]
for w in strs:
m_sum = w.count("0")
n_sum = w.count("1")
for i in range(m, m_sum - 1, -1):
for j in range(n, n_sum - 1, -1):
dp[i][j] = max(dp[i][j], dp[i - m_sum][j - n_sum] + 1)
return dp[-1][-1]
8、完全背包
(1)518题:零钱兑换II
题目链接:518. 零钱兑换 II
题目:
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
思路:
dp[j]的含义:凑成总⾦额j的货币组合数为dp[j]
递推公式: dp[j] += dp[j - coins[i]]
初始化:dp[0]=1
遍历顺序:外层coins,内层顺序吧变量金额,这一点区分了01背包,这里状态覆盖
这里的解释很清楚:动态规划:关于01背包问题,你该了解这些!(滚动数组)
def change(self, amount: int, coins: List[int]) -> int:
dp = [0 for x in range(amount + 1)]
dp[0] = 1
for w in coins:
for j in range(w, amount + 1):
dp[j] += dp[j - w]
return dp[-1]
(2)377题:组合总和IV
题目链接:377. 组合总和 Ⅳ
题目:
给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
思路:
dp[j]的含义:凑成⽬标正整数为i的排列个数为dp[i]
递推公式: dp[j] += dp[j - nums[i]]
初始化:因为题⽬中也说了:给定⽬标值是正整数! 所以 dp[0] = 1是没有意义的,仅仅是为
了推导递推公式。
遍历顺序:外层for遍历背包,内层for循环遍历物品。
def combinationSum4(self, nums: List[int], target: int) -> int:
# 完全背包
# 求排列数就是外层for遍历背包,内层for循环遍历物品。
dp = [0] * (target + 1)
dp[0] = 1
for i in range(target + 1):
for j in range(len(nums)):
if nums[j] <= i: #能放入物品nums[j]的情况
dp[i] += dp[i - nums[j]]
return dp[-1]
(3)322题:零钱兑换
题目链接:322. 零钱兑换
题目:
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
思路:
dp[j]的含义:凑成⽬标正整数为i的最少硬币个数
递推公式: dp[j] = min(dp[j], dp[j - nums[i]]+1)
初始化:因为题⽬中也说了:给定⽬标值是正整数! 所以 dp[0] = 1是没有意义的,仅仅是为
了推导递推公式。
遍历顺序:外层for遍历物品,内层for循环背包。
def coinChange(self, coins: List[int], amount: int) -> int:
# 完全背包
# dp[i]组成i的硬币最少个数,组成不了用-1表示,这里就初始了-1
dp = [float("inf")] * (amount + 1)
dp[0] = 0
for i in coins:
for j in range(i, amount+1):
if dp[j - i] != float("inf"): #不添加coins[i]的情况,没有办法组成j的金额
#入参:coins = [2, 3, 10], amount = 11
dp[j] = min(dp[j - i] + 1, dp[j])
if dp[-1] == float("inf"):
return -1
return dp[-1]
(4)279题:完全平方数
题目:279. 完全平方数
和零钱兑换一模一样,区别就是物品是完全平方数
def numSquares(self, n: int) -> int:
import math
# 完全背包
dp = [float("inf")] * (n + 1)
dp[0] = 0
temp = math.ceil(math.sqrt(n))
for i in range(1, temp + 1):
for j in range(i * i, n + 1): # 这里不用考虑i组成不了dp[j]的情况,因为1的平方还是1,让任何数都有解
dp[j] = min(dp[j - i * i] + 1, dp[j])
print(dp)
return dp[-1]
(5)139题:单词拆分
题目:139. 单词拆分
给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
"""动态规划"""
# 单词就是物品,字符串s就是背包,单词能否组成字符串s,就是问物品能不能把背包装满。
# 递归公式:dp[i]= dp[j] and s[j:i+1] in wordDdict
# 在i之前找到一个位置j---j之前满足拆分,dp[j] = True
# ----j到i之间满足拆分,s[j:i+1] in wordDdict
n = len(s)
dp = [True] + [False] * n
for i in range(1, n+1):
for j in range(i):
if dp[j] and s[j:i] in wordDict:
dp[i] = True
break #这里找到就结束了,减少循环次数
return dp[-1]
9、多重背包
类似面试题目(360的题):
有彩色粉笔m个,白色粉笔n个。
a个彩色粉笔+b个白色粉笔可以打包卖x元;
c个彩色粉笔打包卖y元;
d个白色粉笔打包卖z元。
问粉笔不一定用完,得到的最多价钱是多少?举例
输入:
m=5, n=5, a=1, b=2, c=3, d=3, x=2, y=1, z=3,
输出:
6
# 第一种:dp动态规划
def chalk1(m, n, a, b, c, d, x, y, z):
# dp[i][j]表示i个彩色粉笔和j个白色粉笔的最大价值
dp = [[0 for _ in range(m + 1)] for _ in range(n + 1)]
# 物品做一个数据结构,方便下面遍历物品
temp = {(a, b): x, (c, 0): y, (0, d): z}
if temp_n and temp_m:
max_n = min(n // temp_n, m // temp_m)
elif temp_n:
max_n = n // temp_n
elif temp_m:
max_n = m // temp_m
for _ in range(max_n):
for temp_m, temp_n in temp:
for i in range(m, temp_m - 1, -1):
for j in range(n, temp_n - 1, -1):
# 递推公式和零和一的题目一模一样,寻找不装temp这个物品的最大价值+temp物品的价值
dp[i][j] = max(dp[i][j], dp[i - temp_m][j - temp_n] + temp[(temp_m, temp_n)])
return dp[-1][-1]
#第二种:递归
def chalk2(m, n, a, b, c, d, x, y, z):
res={} #这里放所有购买的彩色和白色粉笔的总数组合
def temp(m, n): # 此方法就是寻找m,n的不同组合的价值是多少
if (m, n) not in res:
r = 0
# 找到三种购买方式的最大价值
if m >= a and n >= b:
r = max(r, temp(m - a, n - b) + x)
if m >= c:
r = max(r, temp(m - c, n) + y)
if n >= d:
r = max(r,temp(m, n - d) + z)
res[(m, n)] = r
return res[(m, n)]
return temp(m, n)
#第三种:递归,相比较第二种,没有创建递归子函数
def chalk3(m, n, a, b, c, d, x, y, z):
res={}
if (m, n) not in res:
r = 0
if m >= a and n >= b:
r = max(r, chalk3(m - a, n - b, a, b, c, d, x, y, z) + x)
if m >= c:
r = max(r, chalk3(m - c, n, a, b, c, d, x, y, z) + y)
if n >= d:
r = max(r, chalk3(m, n - d, a, b, c, d, x, y, z) + z)
res[(m, n)] = r
return res[(m, n)]
(1)打家劫舍
01背包问题
def rob(self, nums: List[int]) -> int:
if len(nums) == 0:
return 0
if len(nums) == 1:
return nums[0]
dp = [0] * len(nums)
dp[0], dp[1] = nums[0], max(nums[:2])
for i in range(2, len(nums)):
dp[i] = max(dp[i-1], dp[i-2] + nums[i])
return dp[-1]
(2)打家劫舍II
def rob(self, nums: List[int]) -> int:
# 拆分两个部分的打家劫舍I, nums[1:]最后一家偷,那就不偷第一家
# nums[:-1]不偷最后一家,偷第一家
if len(nums) == 0:
return 0
if len(nums) == 1:
return nums[0]
def __dp(temp_nums):
if len(temp_nums) == 1:
return temp_nums[0]
dp = [0] * len(temp_nums)
dp[0] = temp_nums[0]
dp[1] = max(temp_nums[:2])
for i in range(2, len(temp_nums)):
dp[i] = max(dp[i-1], dp[i-2] + temp_nums[i])
return dp[-1]
one = __dp(nums[:-1])
two = __dp(nums[1:])
return max(one, two)
(3)打家劫舍III
(4)买卖股票的最佳时机
def maxProfit(self, prices: List[int]) -> int:
"""贪心方法"""
res = 0
low = float("inf")
for i in prices:
low = min(low, i)
res = max(i-low, res)
return res
def maxProfit(self, prices: List[int]) -> int:
"""动态规划"""
# dp[i]第i天:第一个值表示持股状态金额最大,第二值表示不持股状态最大值
#不持股状态dp[i][1]:(1)i-1天就不持股保持dp[i-1][1],(2)i天买股票:i天价格prices[i] + 第i-1持股的最大价值dp[i-1][0]
#持股状态dp[i][0]:(1)i-1天就持股保持dp[i-1][0],(2)i天卖股票:-prices[i]
dp=[[0,0] for _ in range(len(prices))]
dp[0][0] = -prices[0]
dp[0][1] = 0
for i in range(1, len(prices)):
dp[i][0] = max(-prices[i],dp[i-1][0])
dp[i][1] = max(dp[i-1][1],prices[i]+dp[i-1][0])
return dp[-1][-1] #返回最后一天不持股的最大价值
(5)买卖股票的最佳时机II
def maxProfit(self, prices: List[int]) -> int:
"""贪心算法"""
res = 0
for i in range(1,len(prices)):
res += max(prices[i]-prices[i-1], 0)
return res
def maxProfit(self, prices: List[int]) -> int:
"""动态规划"""
dp = [[0, 0] for _ in range(len(prices))]
dp[0][0] = -prices[0]
dp[0][1] = 0
for i in range(1,len(prices)):
# 第i天持股:(1)i-1天持股-->保持i-1持股状态最大收益:dp[i-1][0]
# (2)i-1天不持股-->i天买入:i-1不持股的最大收益dp[i - 1][1]-i天的价格prices[i]
dp[i][0] = max(dp[i-1][0], dp[i - 1][1] - prices[i])
# 第i天不持股:(1)i-1天持股-->i天卖出:i-1持股的最大收益dp[i - 1][0]+i天的价格prices[i]
# (2)i-1天不持股-->保持i-1不持股状态最大收益:dp[i-1][1]
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i])
return dp[-1][-1]
def maxProfit(self, prices: List[int]) -> int:
"""动态规划----一维滚动dp"""
dp = [0, 0]
dp[0] = -prices[0]
dp[1] = 0
for i in range(1,len(prices)):
dp[0] = max(dp[0], dp[1] - prices[i])
dp[1] = max(dp[1], dp[0] + prices[i])
return dp[-1]
(6)买卖股票的最佳时机III
def maxProfit(self, prices: List[int]) -> int:
"""动态规划"""
dp = [[0, 0, 0, 0, 0] for _ in range(len(prices))]
dp[0][1] = -prices[0]
dp[0][3] = -prices[0]
for i in range(1, len(prices)):
dp[i][0] = dp[i-1][0]
dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i])
dp[i][2] = max(dp[i-1][2], dp[i-1][1]+prices[i])
dp[i][3] = max(dp[i-1][3], dp[i-1][2]-prices[i])
dp[i][4] = max(dp[i-1][4], dp[i-1][3]+prices[i])
print(dp)
return dp[-1][-1]
def maxProfit(self, prices: List[int]) -> int:
"""动态规划----一维滚动dp"""
dp = [0, 0, 0, 0, 0]
dp[1] = -prices[0]
dp[3] = -prices[0]
for i in range(1, len(prices)):
dp[1] = max(dp[1], dp[0]-prices[i])
dp[2] = max(dp[2], dp[1]+prices[i])
dp[3] = max(dp[3], dp[2]-prices[i])
dp[4] = max(dp[4], dp[3]+prices[i])
print(dp)
return dp[-1]
(7)买卖股票的最佳时机IV
这里就是就要参考买卖股票的最佳时机III,买卖股票最多2次,这里是k次,观察规律
dp[i]的含义 初始值 递归公式
dp[0] 表示不操作状态 0dp[1] 第⼀次买⼊状态 -prices[0] max(dp[1], dp[0] - prices[1] )dp[2] 第⼀次卖出状态 0 max(dp[2], dp[1] +prices[2] )dp[3] 第⼆次买⼊状态 -prices[0] max(dp[3], dp[2] - prices[3] )dp[4] 第⼆次卖出状态 0 max(dp[4], dp[3] +prices[4] )dp[5] 第三次买⼊状态 -prices[0] max(dp[5], dp[4] - prices[5] )dp[6] 第三次卖出状态 0..................dp[2*k-1] 第k次买⼊状态 -prices[0] max(dp[2*k-1], dp[2*k-2] - prices[2*k-1] )dp[2*k] 第k次卖出状态 0 max(dp[2*k], dp[2*k-1] - prices[2*k] )
def maxProfit(self, k: int, prices: List[int]) -> int:
"""动态规划----一维滚动dp"""
if len(prices) == 0 or k == 0:
# 前面的股票prices长度限制是最小1,这里有0的风险
return 0
dp = [0 for _ in range(2*k+1)]
# 初始化
for i in range(1,len(dp)):
if i % 2 == 1:
dp[i] = -prices[0]
for i in range(1,len(prices)):
for j in range(1,len(dp)):
if j % 2 == 1:
dp[j] = max(dp[j], dp[j-1]-prices[i])
else:
dp[j] = max(dp[j], dp[j-1]+prices[i])
return dp[-1]
def maxProfit(self, k: int, prices: List[int]) -> int:
"""动态规划"""
if len(prices) == 0 or k == 0:
return 0
dp = [[0 for _ in range(2*k+1)] for _ in range(len(prices))]
# 初始化
for i in range(1,len(dp[0])):
if i % 2 == 1:
dp[0][i] = -prices[0]
for i in range(1,len(prices)):
for j in range(1,len(dp[0])):
if j % 2 == 1:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-1]-prices[i])
else:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-1]+prices[i])
print(dp)
return dp[-1][-1]
(8)最佳买卖股票时机含冷冻期
def maxProfit(self, prices: List[int]) -> int:
"""动态规划"""
if len(prices) == 0:
return 0
dp = [[0, 0, 0, 0] for _ in range(len(prices))]
dp[0][0] = -prices[0]
for i in range(1, len(prices)):
dp[i][0] = max(dp[i-1][0], dp[i-1][3]-prices[i], dp[i-1][1]-prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][3])
dp[i][2] = dp[i-1][0] + prices[i]
dp[i][3] = dp[i-1][2]
print(dp)
return max(dp[-1][1:])
(9)买卖股票的最佳时机含⼿续费
def maxProfit(self, prices: List[int], fee: int) -> int:
"""贪心算法"""
res = 0
min_price = prices[0]
for i in range(1,len(prices)):
if min_price > prices[i]:
min_price = prices[i]
if prices[i] > min_price + fee:
res += prices[i]-min_price-fee
min_price = prices[i]-fee
#计算利润每次都要减去手续费,所以要让重置minPrice = prices[i] - fee;这样在明天收获利润的时候,才不会多减一次手续费!
return res
def maxProfit(self, prices: List[int], fee: int) -> int:
"""动态规划"""
dp = [-prices[0], 0]
for i in range(1, len(prices)):
dp[0] = max(dp[0], dp[1]-prices[i])
dp[1] = max(dp[1], dp[0]+prices[i]-fee) #和股票II中一模一样,卖出计算的时候减去手续费就行了
return dp[1]
(10)最⻓递增⼦序列
def lengthOfLIS(self, nums: List[int]) -> int:
dp = [1] * len(nums)
result = 1
for i in range(1, len(nums)):
for j in range(i):
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j]+1)
if dp[i] > result:
result = dp[i]
print(dp)
return result
(11)最⻓连续递增序列
def findLengthOfLCIS(self, nums: List[int]) -> int:
res = 1
temp = 1
for i in range(1,len(nums)):
if nums[i] > nums[i-1]:
temp += 1
else:
temp = 1
res = max(res,temp)
return res
def findLengthOfLCIS(self, nums: List[int]) -> int:
"""动态规划"""
dp = [1] * len(nums)
res = 1
for i in range(1, len(nums)):
if nums[i] > nums[i-1]:
dp[i] = dp[i-1] + 1
res = max(res, dp[i])
return res
(12)最⻓重复⼦数组
dp[i][j]含义:表示nums1前i个元素和nums2前j个元素的最长重复子数组
初始化:dp = [[0 for _ in range(len(nums1)+1)] for _ in range(len(nums2)+1)],最后一列一行是为了dp[0][0] = dp[-1][-1]准备的,要不然需要遍历从1开始
递归公式:dp[i][j] = dp[i-1][j-1] + 1
遍历:遍历nums1的每个index,内层遍历nums2的每个index
好理解的办法就是,遍历从1开始
def findLength(self, nums1: List[int], nums2: List[int]) -> int:
"""动态规划"""
res = 0
dp = [[0 for _ in range(len(nums2))] for _ in range(len(nums1))]
for i in range(len(nums1)):
for j in range(len(nums2)):
if nums2[j] == nums1[i]:
dp[i][j] = dp[i-1][j-1] + 1
res = max(res, dp[i][j])
return res
(13)最⻓公共⼦序列
dp[i][j]含义:表示text1前i个元素和text2前j个元素的最长公共子序列
初始化:dp = [[0 for _ in range(len(text1)+1)] for _ in range(len(text2)+1)],最后一列一行是为了dp[0][0] = dp[-1][-1]准备的,要不然需要遍历从1开始
递归公式:情况1-----text1[i] == text2[j]----------dp[i][j]=dp[i][j]+1
情况2-----text1[i] != text2[j]-----------dp[i][j]=max(dp[i-1][j],dp[i][j-1])---text1的[0....i-1]和text2的[0....j] 、text1的[0....i]和text2的[0....j-1]这两个范围的最长公共子序列取大值
遍历:遍历nums1的每个index,内层遍历nums2的每个index
从1开始遍历,似乎更好理解
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
"""动态规划"""
dp = [[0 for _ in range(len(text1)+1)] for _ in range(len(text2)+1)]
for i in range(len(text2)):
for j in range(len(text1)):
if text2[i] == text1[j]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[-2][-2]
(14)不相交的线
和最⻓公共⼦序列是一模一样的,这里需要想明白这点
def maxUncrossedLines(self, nums1: List[int], nums2: List[int]) -> int:
"""动态规划-求两者的最长公共子序列"""
dp = [[0 for _ in range(len(nums2)+1)] for _ in range(len(nums1)+1)]
for i in range(len(nums1)):
for j in range(len(nums2)):
if nums2[j] == nums1[i]:
dp[i][j] = dp[i-1][j-1]+1
else:
dp[i][j] = max(dp[i][j-1], dp[i-1][j])
return dp[-2][-2]
(15)最⼤⼦序和
(16)判断⼦序列
(17)不同的⼦序列
(18)两个字符串的删除操作
(19)编辑距离
(20)回⽂⼦串
(21)最⻓回⽂⼦序列
(22)毕业旅行问题
题目描述
小明目前在做一份毕业旅行的规划。打算从北京出发,分别去若干个城市,然后再回到北京,每个城市之间均乘坐高铁,且每个城市只去一次。由于经费有限,希望能够通过合理的路线安排尽可能的省一些路上的花销。给定一组城市和每对城市之间的火车票的价钱,找到每个城市只访问一次并返回起点的最小车费花销。输入描述
城市个数n(1<n≤20,包括北京)
城市间的车票价钱 n行n列的矩阵 m[n][n]输出描述
最小车费花销 s示例1
输入4
0 2 6 5
2 0 4 4
6 4 0 2
5 4 2 0输出
13