动态规划——力扣+洛谷刷题总结

动态规划

  • 基本dp
    • P1095守望者的逃离
    • P3842.线段
  • 打家劫舍
  • 0-1背包
    • 0-1背包原理
    • 力扣0-1背包的应用
      • 416.分割等和子集
      • 1049.最后一块儿石头的重量
      • 494.目标和
      • 474.一和零
  • 完全背包
    • 完全背包原理
    • 完全背包的应用
      • 518.零钱兑换
      • 377.组合总和
      • 322.零钱兑换
      • 279.完全平方数
      • 139.单词拆分

基本dp

P1095守望者的逃离

解题思路:

先dp一下,闪现的距离,dp[i]表示:第i秒闪现能走多远(dp[i]=dp[i-1]+60);
然后再算可以走路的时候 第i秒可以走多远,(dp[i]=dp[i-1]+17);
这里第二次dp是直接使用第一次dp后的结果数组的,相当于使用了闪现的最佳距离,在此基础上尝试走路。

[M, S, T] = list(map(int,input().split()))
dp = [0 for i in range(T+1)]
for i in range(1, T+1):
    if M >= 10:
        dp[i] = dp[i-1] + 60
        M -= 10
    else:
        dp[i] = dp[i-1]
        M += 4

for i in range(1,T+1):
    dp[i] = max(dp[i],dp[i-1]+17)
    if dp[i] >= S:
        print('Yes')
        print(i)
        break
if dp[T] < S and dp[T] != 0:
    print('No')
    print(dp[T])

P3842.线段

不难看出,路程长度由两个部分组成:左右走的距离 ++ 上下走的距离,因为上下走的距离一定是 nn,所以我们只需要求出左右走的最小距离即可。

很明显,当我们走完一行的线段时,我们会处在左端点/右端点,这样我们就需要在线性 dpdp 的基础上加一维来表示走完这一行处在左/右端点。

所以我们定义一个 f[i][0/1]f[i][0/1],表示走完第 ii 行的线段后,我们处在左/右端点。

然后我们就可以用上一行的状态来推出下一行了。

分为 44 种情况:

上一行在左端点,下一行走到左端点; 上一行在左端点,下一行走到右端点; 上一行在右端点,下一行走到左端点; 上一行在右端点,下一行走到右端点;

我们以"上一行在右端点,下一行走到右端点"为例:
动态规划——力扣+洛谷刷题总结_第1张图片
这样我们就可以推出状态转移方程了。

  • f[i][1]=min(f[i-1][1]+abs(l[i]-r[i-1])+r[i]-l[i]+1,f[i-1][0]+abs(l[i]-l[i-1])+r[i]-l[i]+1);//本行走到右端点,上一行走到左/右端点,取最小值
  • f[i][0]=min(f[i-1][1]+abs(r[i]-r[i-1])+r[i]-l[i]+1,f[i-1][0]+abs(r[i]-l[i-1])+r[i]-l[i]+1);//本行走到左端点,上一行走到左/右端点,取最小值
n = int(input())
l = [[0,0] for i in range(n+1)]
dp = [[0,0] for i in range(n+1)]
for i in range(1,n+1):
    l[i] = list(map(int,input().split()))
for i in range(1,n+1):
    if i == 1:
        dp[1][1] = l[1][1] - 1
        dp[1][0] = 2*l[1][1] - l[1][0] - 1
        continue
    else:
        temp = l[i][1] - l[i][0] + 1
        dp[i][0] = min(dp[i-1][0] + abs(l[i-1][0] - l[i][1]) + temp, dp[i-1][1] + abs(l[i-1][1] - l[i][1]) + temp)
        dp[i][1] = min(dp[i-1][0] + abs(l[i-1][0] - l[i][0]) + temp, dp[i-1][1] + abs(l[i-1][1] - l[i][0]) + temp)

print(min(dp[n][0] + n - l[n][0], dp[n][1] + n - l[n][1]))

转自Rubyonly 的博客

打家劫舍

0-1背包

0-1背包原理

问题描述:

有N件物品和⼀个最多能被重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。 每件物品只能⽤⼀次,求解将哪些物品装⼊背包⾥物品价值总和最⼤。

要点提取:

  1. 确定dp数组以及下标的含义:

二维数组:dp[i][j] 表示从下标为[0-i]的物品⾥任意取,放进容量为j的背包,价值总和最⼤是多少。
一维数组:dp[j]表示:容量为j的背包,所背的物品价值可以最⼤为dp[j]。

  1. 确定递推公式:

二维数组:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])(逗号前面说明不选第i个物品,逗号后面表明选择第i个物品)
一维数组:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

  1. dp数组如何初始化
    初始化,⼀定要和dp数组的定义吻合dp
    Notes:这里初始化不只要考虑dp[0]是几,还要计算需要开辟多大的数组空间,这里也是考虑j的含义,背包重量的取值。
         根据递归公式发现数组在推导的时候⼀定是取价值最⼤的数,如果题⽬给的价值都是正整数那么⾮0下标都初始化为0就可以了,如果题⽬给的价值有负数,那么⾮0下标就要初始化为负⽆穷。

二维数组: 从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],⽆论是选取哪些物品,背包价值总和⼀定为0。
一维数组:dp[j]表示:容量为j的背包,所背的物品价值可以最⼤为dp[j],那么dp[0]就应该是0,因为背包容量为0所背的物品的最⼤价值就是0。

  1. 确定遍历顺序
    a.有两个遍历的维度:物品和背包重量,在本题中两种遍历维度的先后顺序都可以,但是对于组合问题和排列问题时遍历顺序会有差异
    b.在滚动数组中,外层遍历物品,内层遍历背包重量,0-1背包问题内层要逆序遍历,保证背包中的物品只取一次而二维数组不需要,因为对于⼆维dp, dp[i][j]都是通过上⼀层即dp[i - 1][j]计算⽽来,本层的dp[i][j]并不会被覆盖,但是一维滚动数组会覆盖
# 二维遍历,先遍历物品,在遍历背包
for i in range(1,len(weight)+1):  # 遍历物品
	for j in range(bagWeight+1):  # 二维数组正序遍历背包容量
		if (j < weight[i]): dp[i][j] = dp[i - 1][j]  # 这个是为了展现dp数组⾥元素的变化
		else: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
# 一维滚动数组
for i in range(len(weight)): #遍历物品
	for j in range(bagWeight, weight[i] - 1, -1): # 内层循环需要逆序遍历背包容量
		dp[j] = max(dp[j], dp[j - weight[i]] + value[i])

5.举例推导dp数组
一定要注意j(内层循环)的取值范围!!!开辟的dp数组的大小,含义,然后推导数组验证是否正确。

力扣0-1背包的应用

**如果题目中要求最大或最小,那么递推公式一定是min或者max(dp[j],dp[j-nums[i]]+nums[i]),如果是方法数(或组合数),呢么递推公式一定是dp[j] += dp[j-nums[i]]

416.分割等和子集

题目描述:给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
----------------------------------------示例二------------------------------------------------------
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。

** 要点提取:**

  • 背包的体积为sum / 2
  • 背包要放⼊的商品(集合⾥的元素)重量为 元素的数值,价值也为元素的数值
  • 背包如何正好装满,说明找到了总和为sum / 2 的⼦集。
  • 背包中每⼀个元素是不可重复放⼊。

解题思路分析:

  1. 确定dp数组以及下标的含义

01背包中, dp[j] 表示: 容量为j的背包,所背的物品价值可以最⼤为dp[j]。
套到本题, dp[j]表示: 背包总容量是j,最⼤可以凑成i的⼦集总和为dp[j]。

  1. 确定递推公式

01背包的递推公式为: dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
本题,相当于背包⾥放⼊数值,那么物品i的重量是nums[i],其价值也是nums[i]。
所以递推公式: dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);

  1. dp数组如何初始化

  背包的大小为sum(num)/2,所以需要开辟这么大的空间,并且保证这个值可以取到;
  另一方面从dp[j]的定义来看,⾸先dp[0]⼀定是0,且题⽬给的价值都是正整数那么⾮0下标都初始化为0就可以了。这样才能让dp数组在递归公式的过程中取的最⼤的价值,⽽不是被初始值覆盖了。

每个数组中的元素不会超过 100,数组的⼤⼩不会超过 200
所以总和不会⼤于20000,背包最⼤只需要其中⼀半,所以10001⼤⼩就可以了

  1. 确定遍历顺序
    0-1背包,一维数组,内层循环倒序遍历

  2. 举例推导数组
    j的取值范围,涉及到nums[i]是否选择的问题,所以范围为nums[i]~target(=sum(nums)/2)的闭区间。

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        target = sum(nums)
        if target % 2 == 1:
            return False
        else:
            target = target // 2

        dp = [0 for i in range(10001)]
        flag = False
        for i in range(len(nums)):
            for j in range(target+1,nums[i] - 1,-1):
                dp[j] = max(dp[j],dp[j-nums[i]] + nums[i])


        return dp[target] == target

1049.最后一块儿石头的重量

题意解析:
尽量让石头分成两堆,让相撞之后剩下的石头最小,
本题物品的重量为stone[i],物品的价值也为stone[i]
解题思路分析:

  1. 确定dp数组以及下标的含义

dp[j]表示容量(这⾥说容量更形象,其实就是重量)为j的背包,最多可以背dp[j]这么重的⽯头。

  1. 确定递推公式

dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);

  1. 初始化

a. dp[j]中的j表示容量,那么最⼤容量(重量)就是所有⽯头的重量和30 * 1000,⽽本题将数组分成两部分,所以要求的target其实只是最⼤重量的⼀半,所以dp数组开到15000⼤⼩就可以了。
b. 因为重量都不会是负数,所以dp[j]都初始化为0就可以了

  1. 确定遍历顺序

使⽤⼀维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒叙遍历!

  1. 举例推导dp数组

关于j的取值:因为涉及到第i个物品选不选,所以j的取值为stones[i]~target的闭区间
最后返回两个数组的差((sum - target) - target)

class Solution:
    def lastStoneWeightII(self, stones: List[int]) -> int:
        dp = [0]*15001
        temp = sum(stones) 
        target = temp// 2
        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 (temp - dp[target]) - dp[target]

494.目标和

注意到本题是装满有几种方法,求的是组合数
本题的思想也是将给定的数组分成两个部分,一部分全是由正数,另一部分全是负数,他们之间的关系如下:

假设加法的总和为x,那么减法对应的总和就是sum - x。
所以我们要求的是 x - (sum - x) = S
x = (S + sum) / 2

此时问题就转化为,装满容量为x背包,有⼏种⽅法。
看到(S + sum) / 2 应该担⼼计算的过程中向下取整有没有影响如在本题中sum 是5, S是2的话是⽆解的。

  1. 确定dp数组以及下标的含义

二维数组:dp[i][j]:使⽤ 下标为[0, i]的nums[i]能够凑满j(包括j)这么⼤容量的包,有dp[i][j]种⽅法。
一维数组:dp[j] 表示:填满j(包括j)这么⼤容积的包,有dp[j]种⽅法

  1. 确定递推公式

所有求组合类问题的公式,都是类似这种:

dp[j] += dp[j - nums[i]]

不考虑nums[i]的情况下,填满容量为j - nums[i]的背包,有dp[j - nums[i]]种⽅法。

那么只要搞到nums[i]的话,凑成dp[j]就有dp[j - nums[i]] 种⽅法。

举⼀个例⼦,nums[i] = 2: dp[3],填满背包容量为3的话,有dp[3]种⽅法。

那么只需要搞到⼀个2(nums[i]),有dp[3]⽅法可以凑⻬容量为3的背包,相应的就有多少种⽅法可以
凑⻬容量为5的背包。

那么需要把 这些⽅法累加起来就可以了, dp[] += dp[j - nums[i]]

  1. 初始化
  • 从递归公式可以看出,在初始化的时候dp[0] ⼀定要初始化为1,因为dp[0]是在公式中⼀切递推结果的起源,如果dp[0]是0的话,递归结果将都是0。
  • dp[0] = 1,理论上也很好解释,装满容量为0的背包,有1种⽅法,就是装0件物品。
  • dp[j]其他下标对应的数值应该初始化为0
  1. 确定遍历顺序

对于01背包问题⼀维dp的遍历, nums放在外循环, target在内循环,且内循环倒序。

  1. 举例推导

j的取值范围,涉及到选不选第i个物品,所以取值还是nums[i]~target的闭区间

class Solution:
    def findTargetSumWays(self, nums: List[int], target: int) -> int:
        temp_s = sum(nums)
        if target > temp_s:
            return 0
        if ((target + temp_s) % 2 == 1):
            return 0
        temp_target = (target + temp_s) // 2
        dp = [0] * 1001
        dp[0] = 1
        for i in range(len(nums)):
            for j in range(temp_target, nums[i] - 1,-1):
                dp[j] += dp[j-nums[i]]

        return dp[temp_target]

474.一和零

题意解析:
本题本题其实是01背包问题!其中strs 数组⾥不同⻓度的字符串元素就是不同⼤⼩的待装物品,⽽m 和 n相当于是⼀个背包,两个维度的背包。
解题思路分析:

  1. 确定dp数组以及下标的含义

dp[i][j]:最多有i个0和j个1的strs的最⼤⼦集的⼤⼩为dp[i][j]

  1. 确定递推公式

dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);

  • dp[i][j] 可以由前⼀个strs⾥的字符串推导出来, strs⾥的字符串有zeroNum个0, oneNum个1。
  • dp[i][j] 就可以是 dp[i - zeroNum][j - oneNum] + 1。
  • 然后我们在遍历的过程中,取dp[i][j]的最⼤值。
  • 对⽐⼀下01背包的递推公式: dp[j] = max(dp[j], dp[j - weight[i]] + value[i])就会发现,字符串的zeroNum和oneNum相当于物品的重量(weight[i]),字符串本身的个数相当于物品的价值(value[i])
  1. dp数组如何初始化
  • 01背包的dp数组初始化为0
  • 因为物品价值不会是负数,初始为0,保证递推的时候dp[i][j]不会被初始值覆盖。
  1. 确定遍历顺序

外层for循环遍历物品,内层for循环遍历背包容量且从后向前遍历

  1. 举例推导dp数组
    0-1背包,考虑当前第i个字符串选不选,所以j得取值是第i个字符串中0的数量m(或者1的数量n)的闭区间
class Solution:
    def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
        dp = [[0 for i in range(n+1)] for j in range(m+1)]
        for e in range(len(strs)):
            temp = strs[e]
            one_num = temp.count('1')
            zero_num = temp.count('0')
            for i in range(m, zero_num-1,-1):
                for j in range(n, one_num-1,-1):
                    dp[i][j] = max(dp[i][j], dp[i-zero_num][j-one_num] + 1)

        return dp[m][n]

完全背包

完全背包原理

问题描述:
  有N件物品和⼀个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。 每件物品都有⽆限个(也就是可以放⼊背包多次) ,求解将哪些物品装⼊背包⾥物品价值总和最⼤。
与0-1背包的区别:

  • 每个物品可以取无限次,所以内层for循环要正序遍历
"""0-1背包"""
for i in range(len(weight)): # 遍历物品
	for j in range(bagWeight+1,weight[i]-1,-1):   # 反向遍历背包容量且注意j的取值
		dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
"""完全背包"""
for i in range(lenweight)):  # 遍历物品
	for j in range(weight[i]-1,bagWeight):  # 正向遍历背包容量,且j只取到背包容量减1(bagWeight-1)
		dp[j] = max(dp[j], dp[j - weight[i]] + value[i])

装满背包有几种方式:
针对这类问题,两个for循环的先后顺序非常重要,分别对应两类问题,组合数(与数字的顺序无关)和排列数(与数字的顺序有关,比如{1,3}和{3,1}为两种方式的情况)

完全背包的应用

518.零钱兑换

题意分析:
钱币数量不限,这是⼀个完全背包。
但本题和纯完全背包不⼀样, 纯完全背包是能否凑成总⾦额,⽽本题是要求凑成总⾦额的个数!
注意题⽬描述中是凑成总⾦额的硬币组合数,为什么强调是组合数呢?
例如示例⼀:
5 = 2 + 2 + 1
5 = 2 + 1 + 2
这是⼀种组合,都是 2 2 1。
如果问的是排列数,那么上⾯就是两种排列了。
组合不强调元素之间的顺序,排列强调元素之间的顺序。

  1. 确定dp数组以及下标的含义

dp[j]:凑成总⾦额j的货币组合数为dp[j]

  1. 确定递推公式

dp[j] (考虑coins[i]的组合总和) 就是所有的dp[j - coins[i]](不考虑coins[i])相加。

dp[j] += dp[j - coins[i]];

  1. 数组初始化

组合数,dp[0] = 1是 递归公式的基础。
根据j的定义,背包最大为amount,所以开辟amount+1的数组空间

  1. 确定遍历顺序

纯完全背包求得是能否凑成总和,和凑成总和的元素有没有顺序没关系,即:有顺序也⾏,没有顺
序也⾏!⽽本题要求凑成总和的组合数,元素之间要求没有顺序。外层for循环遍历物品(钱币),内层for遍历背包(⾦钱总额)时,这种遍历顺序中dp[j]⾥计算的是组合数!

  1. 举例推导数组

本题还是考虑第i个数字选不选,所以j的取值范围为nums[j]~amount的闭区间

class Solution:
    def change(self, amount: int, coins: List[int]) -> int:
        dp = [0] * (amount+1)
        dp[0] = 1
        for i in range(len(coins)):
            for j in range(coins[i],amount+1):
                dp[j] += dp[j - coins[i]]

        return dp[amount]

377.组合总和

题意分析:
本题其实就是求排列!组合不强调顺序, (1,5)和(5,1)是同⼀个组合。排列强调顺序, (1,5)和(5,1)是两个不同的排列。
解题思路分析:

  1. 确定dp数组以及下标的含义

dp[i]: 凑成⽬标正整数为i的排列个数为dp[i]

  1. 确定递推公式

求装满背包有⼏种⽅法,递推公式⼀般都是dp[i] += dp[i - nums[j]];

  1. 数组初始化
  • dp[0]要初始化为1
  • 初始化长度为target+1的数组空间
  1. 确定遍历顺序

个数可以不限使⽤,说明这是⼀个完全背包。 得到的集合是排列,说明需要考虑元素之间的顺序

  • 如果求组合数就是外层for循环遍历物品,内层for遍历背包。
  • 如果求排列数就是外层for遍历背包,内层for循环遍历物品

target(背包)放在外循环,将nums(物品)放在内循环,内循环从前到后遍历。

  1. 举例推导dp数组
    j的取值范围,排列参考如下代码:
for (int j = 0; j <= amount; j++) { // 遍历背包容量
	for (int i = 0; i < coins.size(); i++) { // 遍历物品
		if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]];
	}
}
class Solution:
    def combinationSum4(self, nums: List[int], target: int) -> int:
        dp = [0] * (target + 1)
        dp[0] = 1
        for i in range(target + 1):
            for j in range(len(nums)):
                if i - nums[j] >= 0: # 要保证第j个元素能够取到,所以这里的判断条件一定是大于等于零
                    dp[i] += dp[i-nums[j]]
        
        return dp[target]

322.零钱兑换

解题思路分析:

  1. 确定dp数组以及下标的含义

dp[j]:凑⾜总额为j所需钱币的最少个数为dp[j]

  1. 确定递推公式

dp[j] = min(dp[j - coins[i]] + 1, dp[j]);

  1. 数组初始化

凑⾜总⾦额为0所需钱币的个数⼀定是0,那么dp[0] = 0
考虑到递推公式的特性, dp[j]必须初始化为⼀个最⼤的数,否则就会在min(dp[j - coins[i]] + 1, dp[j])⽐
较的过程中被初始值覆盖。

4.确定遍历顺序

  • 本题求钱币最⼩个数,并不强调集合是组合还是排列。 那么钱币有顺序和没有顺序都可以,都不影响钱币的最⼩个数。
  • 如果求组合数就是外层for循环遍历物品,内层for遍历背包。
  • 如果求排列数就是外层for遍历背包,内层for循环遍历物品。
  1. 举例推导dp数组

考虑第i个物品选不选的情况,j的取值范围还是nums[j]~amount的闭区间

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        if amount == 0:
            return 0
        else:
            dp = [amount+1] * (amount + 1)
            dp[0] = 0
            for i in range(len(coins)):
                for j in range(coins[i],amount+1):
                    dp[j] = min(dp[j], dp[j-coins[i]]+1)

            return dp[amount] if dp[amount] < amount + 1 else -1

279.完全平方数

  • dp[i]:和为i的完全平⽅数的最少数量为dp[i]
  • dp[j] = min(dp[j - i * i] + 1, dp[j]);
  • dp[0]⼀定是0;⾮0下标的dp[i]⼀定要初始为最⼤值,这样dp[j]在递推的时候才不会被初始值覆盖。
  • 求最⼩数,遍历顺序都可以
  • j的取值为1到根号i的闭区间
class Solution:
    def numSquares(self, n: int) -> int:
        dp = [n+1] * (n+1) # 非0下标要初始化为最大值,dp[j]在递推的时候才不会被覆盖
        dp[0] = 0
        for i in range(n+1):
            for j in range(1, int(pow(n,0.5))+1):  # j的取值为1到根号i的闭区间
                dp[i] = min(dp[i],dp[i-j*j] + 1)

        return dp[n]

139.单词拆分

你可能感兴趣的:(刷题笔记,动态规划,leetcode,算法,python)