代码随想录:动态规划
基础篇
斐波那契数
|
|
动态规划五部曲:
- 确定 dp 数组元素 dp [i] 的含义:每个斐波拉且数列的值。
- 确定递推公式:dp [i]=dp [i-1]+dp [i-2]。
- dp 数组的初始化:dp [0]=1,dp [1]=1。
- 遍历顺序:从前往后。
- 打印 dp 数组。
|
|
对上面的改进:
由于我们只需要知道 dp [i-1] 和 dp [i-2] 的值,就可以求出 dp [i] 的值,而且题目也不需要我们输出全部的斐波拉且数列,只需要第 n 位置的值,所有我们可以定义一个变量 sum 来存 dp [i-1] 和 dp [i-2] 的和,而不需要保存完整的数组。
|
|
爬楼梯
|
|
第 i 位置的方法数量是 i-1 位置的数量 + i-2 位置的数量。因为到达 i 位置,只能从 i-1 位置迈一步或者 i-2 位置迈两步到达。
|
|
使用最小花费爬楼梯
|
|
动态规划五部曲:
-
确定 dp 数组元素 dp [i] 的含义:到达 i 位置所需要的最小花费(注意:最后需要跳到 len (cost) 的位置(下标从 0 开始))
-
确定递推公式:dp [i]=min (dp [i-1]+cost [i-1],dp [i-2]+cost [i-2]),cost [i-1] 表示从 i-1 位置往上跳所需要的花费
-
dp 数组的初始化:dp [0]=0,dp [1]=0,因为我们可以选择从 0 位置或者从 1 位置开始跳,而站在初始位置是不需要任何花费的,所以初始值都是 0。
-
遍历顺序:从前往后。
-
打印 dp 数组。
|
|
不同路径
|
|
由于本题是一个二维的网格,所以我们需要定义一个二维的 dp 数组。
本题的 dp [i][j] 表示从 start 位置(0,0)出发,到(i,j)位置可以走的路径数。
由于机器人每次只能往右走 1 步,或者往下走 1 步,则 dp [i][j] 的递推公式为:dp [i-1][j]+dp [i][j-1]。
我们还需要对 dp 数组进行初始化,也就是当 i=0 和 j=0 时,dp 数组的值。
dp [0][j] 表示一直往右走,路径数为 1;dp [i][0] 表示一直往下走,路径数也为 1。
|
|
不同路径 II
|
|
和上题不同的是,本题的 obstacleGrid 数组中,可能存在障碍,是无法通行的。
所以在计算 dp 数组时,需要在递推公式上加上一个条件判断,如果 obstacleGrid [i]==0(非障碍物),才计算前两个位置的和,否则为默认值 0。
dp 数组的初始化也需要进行修改,在水平或者垂直方向上(i==0 或 j==0),当遇到障碍物之后,后面的位置应该设置为 0。
|
|
整数拆分
|
|
我们需要先确定 dp 数组中 dp [i] 的含义:表示将 i 拆成 m 个数之后,这些数的最大乘积。
要得到 dp [i] 的值,可以从对 i 进行拆解,j 从 1 开始,到 i-1 结束。
我们有两种方式得到 dp [i] 的值。
- j*(i-j)(拆分为两个数的乘积);
- j*dp[i-j](dp [i-j] 表示拆分后 dp 数组的值,复用前面计算好的值),我们需要取二者中较大的值。
dp [i]=max (dp [i],max (j*(i-j),j*dp [i-j])),注意:我们还需要把 dp [i] 放进去作比较,因为可能越拆分得到的乘积越小,为了不让后面的覆盖前面的值,所以我们需要保留之前 dp [i] 的值参与比较。
对 dp 数组进行初始化:i=0 和 i=1 时,dp [i]=0;i=2 时,dp [i]=1。
|
|
不同的二叉搜索树
|
|
本题稍微有点抽象,我们以举一个具体的例子来推导 dp 的递推公式。比如:n=3 时,我们如何通过前面的 dp 数组求出当前的 dp 数组值?
我们先明确 dp 数组中 dp [i] 的含义:表示有 i 个结点的 BST 有多少种构造方法。
当 BST 有 3 个结点时,分为 3 种情况:
- 左边 0 个结点,右边 2 个结点
- 左边 1 个结点,右边 1 个结点
- 2 个节点,右边 0 个结点
使用公式表示为:dp [3] = dp [0]*dp [2] + dp [1]*dp [1] + dp [2]*dp [0]。
注意:左边的数量和右边的数左边量应该相乘,得到总的数量,而不是相加。
观察 dp [3] 的公式,我们容易得到 dp [i] 的递推公式为:
j 从 0 到 i-1,dp [i]=dp [j]*dp [i-j-1]。
dp 数组的初始化:dp [0]=1, dp [1]=1。
|
|
背包问题
01 背包
(本题在 Leetcode 上没有原题)
有 n 件物品和一个最多能背重量为 w 的背包。第 i 件物品的重量是 weight [i],第 i 件物品的价值是 value [i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
示例 1:
重量 | 价值 | |
---|---|---|
物品 0 | 1 | 15 |
物品 1 | 3 | 20 |
物品 2 | 4 | 30 |
输入:n = 3,w = 4,weight = [1, 3, 4],value = [15, 20, 30]
输出:35。背包最多能背的重量为 4,放入物品 0(重量为 1)和物品 1(重量为 3),最大价值为:15+20=35。
|
|
方式 1:使用二维 dp 数组
dp [i][j]:从下标为 [0-i] 的物品里任意取,放进容量为 j 的背包,价值总和最大是多少。(这一点很重要)
确定递推公式:dp [i][j] 可以由两个情况来得到:
1. 不放 i,即:dp [i-1][j]
2. 放 i,需要先将背包的重量减去物品 i 的重量,然后再加上物品 i 的价值,即:dp [i-1][j-weight [i]]+value [i]
但情况 2 需要注意的是,j-weight [i] 必须要大于 0,背包总容量小于 0 (也就是放入物品 i 之后,超出了背包的容量限制)就没有任何意义了。
我们需要取上面两种情况的较大值,即:dp [i][j]=max (dp [i-1][j],dp [i-1][j-weight [i]]+value [i])
dp 数组初始化:由于 dp [i][j] 需要由 dp [i-1][j](上方)和 dp [i-1][j-weight [i]](左上方)的元素求得,
所以我们需要对第 0 行和第 0 列的元素进行初始化。
dp [0][j],即:i 为 0,存放物品 0 的时候,各个容量的背包所能存放的最大价值。
需要分情况讨论:如果 j 小于物品 0 的重量,则最大价值为 0,否则最大价值为物品 0 的价值。
dp [i][0],即:j 为 0,背包容量为 0,则能放的价值也都是 0,dp [i][0] 始终为 0。
|
|
方式 2:使用一维 dp 数组
我们对前面的二维数组进行压缩,使用一维数组。
在一维 dp 数组中,dp [j] 表示:容量为 j 的背包,所背的物品价值可以最大为 dp [j]。
dp [j] 为 容量为 j 的背包所背的最大价值,那么如何推导 dp [!] 呢?
dp [j] 可以通过 dp [j-weight [i]] 推导出来,dp [j-weight [i]] 表示容量为 j-weight [i] 的背包所背的最大价值。
dp [j-weight [i]]+ value [i] 表示 容量为 j - 物品 i 重量 的背包加上物品 i 的价值。(也就是容量为 j 的背包,放入物品 i 了之后的价值即:dp [j])
此时 dp [j] 有两个选择,一个是取自己 dp [j] 相当于 二维 dp 数组中的 dp [i-1][j],即不放物品 i;一个是取 dp [j-weight [i]]+ value [i]],即放物品 i,指定是取最大的,毕竟是求最大价值,所以递归公式为: dp [j]= max (dp [j],dp [j-weight [i]]+ value [i]);
可以看出相对于二维 dp 数组的写法,就是把 dp [i][j] 中 i 的维度去掉了。
对于 0-1 背包问题,使用一维动态规划数组(dp [i] 表示容量为 i 的背包所能达到的最大价值)时,从后往前遍历背包容量(即逆序遍历)的原因是为了避免重复计算。 如果我们从前往后遍历背包容量,并尝试将每个物品放入背包中,就可能会遇到一个问题:当我们尝试将一个物品放入背包时,我们需要知道如果不放入这个物品,之前的最大价值是多少。但是,如果我们从前往后遍历,并且之前已经尝试将其他物品放入了相同容量的背包,那么 dp [i] 的值就已经被更新过了,这会导致我们得到错误的 “如果不放入这个物品” 的最大价值。
为了避免这种情况,我们从后往前遍历背包容量。这样,当我们考虑将一个物品放入背包时,我们使用的 dp [i-weight [j]] 的值还没有被更新(因为我们还没有遍历到 i-weight [j] 这个容量),所以它仍然表示 “如果不放入这个物品” 时的最大价值。这样我们就可以正确地更新 dp [i] 的值了。
dp 数组初始化:容量为 j 的背包,所背的物品价值可以最大为 dp [j],那么 dp [0] 就应该是 0,因为背包容量为 0 所背的物品的最大价值就是 0。
|
|
分割等和子集
|
|
本题可以转换为:01 背包问题,每个元素只能使用一次。
target=sum(nums)/2。
判断容量为 target 的背包能不能被装满,如果能,说明存在子集和为 target,返回 true,否则返回 false。
|
|
最后一块石头的重量 II
|
|
本题也可以转换为 01 背包问题,和 分割等和子集 有异曲同工之妙。
dp [i] 表示容量为 i 的背包最大能装的价值。
我们可以把 stones 数组分成差值尽量小的两个数组,也就是背包的容量为 sum/2,我们尽量让这个背包装满(不用完全装满)
显然,剩下的重量是大于等于背包中的重量的,用剩下的重量减去背包中的重量就是我们的目标结果。
|
|
目标和
|
|
本题较难,建议多加练习和理解。
我们可以将 nums 分成正负两个数组(一个数组 A 中的符号为正,另一个数组 B 的符号为负)。
则有两个等式:sumA+sumB=sum,sumA-sumB=target(假设数组 A 中的和始终更大)。
将两个式子化简,求得 sumA=(target+sum)/2。
我们现在需要求装满容量为 sumA 的背包有多少种方法?
(注意 sumA 不能由 target+sum 向下取整得到,如果 target+sum 为奇数,则无法让表达式的和为 target)
现在需要明确 dp 数组的含义:dp [j] 表示装满容量为 j 的背包有 dp [i] 种方法。
dp [j] 的的递推式:dp [j] 需要根据 dp [j-nums [i]] 得到,如:dp [5] 如何得到呢?
1. 如果遇到的元素值为 1,则有 dp [4] 种方法可以凑成 dp [5](1+4=5);
2. 如果遇到的元素值为 2,则有 dp [3] 种方法可以凑成 dp [5](2+3=5);
3. 如果遇到的元素值为 3,则有 dp [2] 种方法可以凑成 dp [5](3+2=5);
4. 如果遇到的元素值为 4,则有 dp [1] 种方法可以凑成 dp [5](4+1=5);
5. 如果遇到的元素值为 5,则有 dp [0] 种方法可以凑成 dp [5](0+5=5)。
所以我们需要遍历 nums 数组,把每种遍历到的 dp [j-nums [i]] 都加起来,得到 dp [j]。
dp 数组初始化:dp [0]=1(装满容量为 0 的背包有 1 种方法)。
|
|
一和零
本题较难,建议多加练习和理解。
|
|
本题的 strs 子集中最多有 m 个 0 和 n 个 1,要求子集的最大长度。
这可以转换为 01 背包问题,但和前面的题不同的地方在于,物品的重量变成了两个维度(0 的个数、1 的个数);背包的容量也相应地变成了两个维度。
由于物品的重量变成了两个维度,所以我们需要使用二维 dp 数组。
dp [i][j]:表示容量为 i 个 0 和 j 个 1 的背包最大能容纳的的元素个数。
递推表达式:dp [i][j]=max (dp [i][j], dp [i-x][j-y]+1)(x 表示加入元素中 0 的个数,y 表示加入元素中 1 的个数)。
初始化:dp [0][0]=0。
|
|
完全背包问题
完全背包和 01 背包的不同点在于,在完全背包问题中,每个物品可以使用无限次。
在具体代码实现时,遍历背包需要从前往后遍历,而且遍历背包和遍历物品的 for 循环可以交换顺序。
零钱兑换 II
|
|
这就是一个完全背包问题,每个物品可以使用无限次。
本题和 目标和 这道题基本上是一样的,不过本题的物品可以使用无限次。
dp [j] 表示装满容量为 i 的背包,有 dp [j] 种方法。
递推表达式:遍历物品 coins [i],dp [j]+=dp [j-coins [i]]
|
|
组合总和 Ⅳ
|
|
本题和 零钱兑换 II 的区别是:之前那道题是不强调元素顺序的(组合),本题是强调元素顺序的(排列)
1. 完全背包问题中求组合
在完全背包问题中求组合时,我们通常关注的是如何达到某个背包总容量,而不关心物品被放入背包的顺序。换句话说,只要背包内的物品总价值或总重量相同,物品的顺序是可以任意交换的。
为了解决这个问题,我们通常采用先遍历物品(外层循环),后遍历背包容量(内层循环)的方式。这样做的原因是,外层循环遍历每一种物品,内层循环则尝试将当前物品放入背包中,直到达到或超过背包的容量。由于我们关心的是组合,所以不需要考虑物品的顺序,因此先遍历物品是合理的。
2. 完全背包问题中求排列
然而,在完全背包问题中求排列时,我们关注的是物品放入背包的顺序。换句话说,不同的物品顺序被视为不同的解决方案。
为了解决这个问题,我们需要先遍历背包容量(外层循环),后遍历物品(内层循环)。这样做的原因是,外层循环控制背包的当前容量,而内层循环则尝试在当前容量下放入不同的物品。由于我们关心的是排列,所以需要先确定背包的容量(即先确定一个 “位置”),然后再选择放入哪个物品(即确定该位置上的 “元素”)。
|
|
零钱兑换
|
|
dp [j] 表示装满容量为 j 的背包,最少需要的物品数量。
递推表达式:dp [j]=min (dp [j],dp [j-coins [i]]+1)。
dp 数组初始化:dp [0]=0,由于递推式中求的是每次的最小值,为了不让初始值覆盖掉我们真正求的值,使用其他位置应该初始化为 math.MaxInt。
|
|
完全平方数
|
|
本题和 零钱兑换 是差不多的,都是问装满容量为 n 的背包需要的最少元素个数。
dp [j] 表示装满容量为 j 的背包最小需要 dp [j] 个元素。
|
|
对上面代码的优化,不使用额外的候选数组。
|
|
先遍历物品,再遍历背包容量(这样整体的运行时间会少一点)
|
|
其他
单词拆分
本题转换为完全背包问题有点抽象,建议多加理解。
|
|
本题判断 s 能否由 wordDict 组成,我们可以转换为完全背包问题。
dp [j] 表示容量(字符串长度)为 j 的背包能否被 wordDict 组成,如果能值为 true,否则值为 false。
我们使用 dict(map 类型)来保存 wordDict 中出现的字符串
递推表达式:如果 dp [i] && dict [s [i:j]],dp [j] = true
(也就是 i 位置之前可以被 wordDict 中的字符串表示,同时 i 位置到 j 位置的字符串也可以被 wordDict 中的字符串表示)
遍历顺序:由于字典中的字符串可以使用多次,所以是完全背包问题,我们要先遍历背包容量,再遍历物品。
初始化 dp 数组:dp [0]=true,其余位置的初始值为 false(防止初始值覆盖真正的计算值)
|
|
打家劫舍
|
|
本题的要求是不能偷两个相邻房间的金币,其他的可以随便偷。
dp [i]:表示考虑 i 位置及 i 之前位置,所能偷的最大金额为 dp [i]。
递推公式,dp [i] 可以通过两个方式计算得到:
1. 偷 i 位置,那么 i-1 位置就一定不能偷(考虑范围为 i-2 及其前面位置),则能偷的金额为:dp [i-2]+nums [i];
2. 不偷 i 位置,那么我们的考虑范围为 i-1 及其前面位置,能偷的金额为:dp [i-1]。
我们要计算前面两个方式的较大值,即:dp [i]=max (dp [i-1], dp [i-2]+nums [i])
初始化 dp 数组:dp [0] 表示考虑 0 位置所能偷的最大金额,则:dp [0]=nums [0];
dp [1] 表示考虑 1 及其前面的位置,则 dp [1]=max (nums [0], nums [1])。
|
|
打家劫舍 II
|
|
本题和 打家劫舍 不同的地方在于,本题的首尾数组元素设定为相邻的了,也就是不能不同选首尾的元素。
我们可以把本题转换为下面的三种情况:
1. 不考虑首尾元素,则中间元素的选择和 打家劫舍 中的策略是完全一样的。
2. 不考虑尾元素,则前面元素的选择和 打家劫舍 中的策略也是完全一样的。
3. 不考虑首元素,则后面元素的选择和 打家劫舍 中的策略也是完全一样的。
但其实情况 2 已经包含情况 1 了,因为如果选择的元素在情况 1 中,那么情况 2 也一定包含了这些选择的元素,所以我们可以简化为后面两种情况。
取后面两种情况的较大值就可以了。
|
|
打家劫舍 III
|
|
本题中不能选择两个相邻的二叉树结点,也就是如果选择了结点 A,则 A 的左右孩子都不能被选择
但我们也不能直接简单粗暴地取 层序遍历 取最大值,因为可能存在选择的结点是层级错位的情况。
所以我们还是需要使用 动态规划 的思想。
本题的难点在于如何将 动态规划 和 树的遍历 结合起来,这道题就是典型的 树形 DP 的案例。
首先,在遍历二叉树时,我们应该使用后序遍历,因为父结点是否选择,去决定它左右孩子结点中存的值。
每个孩子结点中应该存什么值呢?应该存选择该结点和不选择该结点分别可以获得的最大金额。
也就是递归函数的返回值应该是一个二值数组 NodeMaxVal,表示该二叉树所能获得的最大金额。
在主函数中调用时,应该返回根结点为 root 的二叉树的 NodeMaxVal 数组中,较大的值。
|
|
买卖股票
买卖股票的最佳时机
|
|
方法 1:贪心算法
贪心算法的思想:遍历 prices 数组,找到数组中的最小值,同时,更新最大利润。
|
|
方法 2:动态规划
在每一天,股票都有两种状态:持有 和 不持有。
使用 dp [i][0] 表示第 i 天持有股票能获得的最大利润为 dp [i][0],dp [i][1] 表示第 i 天不持有股票能获得的最大利润为 dp [i][1]。
递推表达式:dp [i] 可以由前一天的 dp [i-1] 计算得到:
-
dp [i][0] 表示第 i 天持有股票能获得的最大利润,那么可以有两种情况:
-
第 i-1 天持有股票:dp [i-1][0];
-
第 i-1 天不持有股票,那么第 i 天就要买入股票,由于本题只能买入一次股票,则第 i 天就要买入股票就一定是第 1 次买入股票,当前的最大利润为 -prices [i]。
dp [i][0] 应该取上面两个情况的最大值,也就是:dp [i][0]=max (dp [i-1][0],-prices [i])。
-
-
dp [i][1] 表示第 i 天不持有股票能获得的最大利润,也可以有两种情况:
- 第 i-1 天就不持有股票:dp [i-1][1];
- 第 i-1 天持有股票,在第 i 天卖出了股票:dp [i-1][0]+prices [i];
同样的,dp [i][1] 也应该取上面两个情况的最大值,也就是:dp [i][1]=max (dp [i-1][1],dp [i-1][0]+prices [i])。
初始化 dp 数组:dp [0][0]=-prices [0],dp [0][1]=0。
|
|
买卖股票的最佳时机 II
|
|
方法 1:贪心算法
贪心策略:只计算利润为正的情况。
|
|
方法 2:动态规划
本题和 买卖股票的最佳时机 不同的地方在于:股票可以进行多次买卖操作。
和前面一样,在每一天,股票都有两种状态:持有 和 不持有。
使用 dp [i][0] 表示第 i 天持有股票能获得的最大利润为 dp [i][0],dp [i][1] 表示第 i 天不持有股票能获得的最大利润为 dp [i][1]。
-
dp [i][0] 表示第 i 天持有股票能获得的最大利润,那么可以有两种情况:
- 第 i-1 天持有股票:dp [i-1][0];
- 第 i-1 天不持有股票,那么第 i 天就要买入股票,和前面不同,股票可以进行多次买卖,则当前的最大利润为 dp [i-1][1]-prices [i]。
dp [i][0] 应该取上面两个情况的最大值,也就是:dp [i][0]=max (dp [i-1][0],-prices [i])。
-
dp [i][1] 的计算方式和 买卖股票的最佳时机 相同,都是:dp [i][1]=max (dp [i-1][1],dp [i-1][0]+prices [i])。
初始化 dp 数组:dp [0][0]=-prices [0],dp [0][1]=0。
|
|
买卖股票的最佳时机 III
【困难】本题较难,建议多加理解和练习。
|
|
本题和 买卖股票的最佳时机 II 不同的地方在于:最多可以完成两笔交易。
现在股票就有 4 种状态了:
- 第 1 次持有;
- 第 1 次不持有;
- 第 2 次持有;
- 第 2 次不持有。
dp [i][0]、dp [i][1]、dp [i][2]、dp [i][3] 分别表示上面的 4 种状态。
递推表达式:dp [i][0] 表示第 i 天第 1 次持有股票所能获得的最大利润,它可以由两方面得到:
-
第 i-1 天就已经是第 1 次持有股票的状态了,即:dp [i-1][0];
-
第 i-1 天未持有股票,第 i 天第 1 次买入了,即:-prices [i]。
则:dp [i][0]=max (dp [i-1][0],-prices [i])。
同理,可以得到其他 3 个状态的递推表达式为:
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 数组:dp [0][0]=-prices [0],dp [0][1]=-prices [0],0,-prices [0],0。
|
|
买卖股票的最佳时机 IV
|
|
本题和 买卖股票的最佳时机 III 的区别在于:最多可以完成 k 笔交易。
所以我们就不能枚举出所有买卖的情况了,需要使用功能 for 循环来遍历第二个变量(也就是股票的交易状态)。
现在股票就有 4 种状态了:
- 第 1 次持有;
- 第 1 次不持有;
- 第 2 次持有;
- 第 2 次不持有。
dp [i][0]、dp [i][1]、dp [i][2]、dp [i][3] 分别表示上面的 4 种状态。
递推表达式:dp [i][0] 表示第 i 天第 1 次持有股票所能获得的最大利润,它可以由两方面得到:
-
第 i-1 天就已经是第 1 次持有股票的状态了,即:dp [i-1][0];
-
第 i-1 天未持有股票,第 i 天第 1 次买入了,即:-prices [i]。
则:dp [i][0]=max (dp [i-1][0],-prices [i])。
同理,可以得到其他 3 个状态的递推表达式为:
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 数组:dp [0][0]=-prices [0],dp [0][1]=-prices [0],0,-prices [0],0。
我们可以根据前面的例子,得到下面的结论:
最多进行 k 次交易,可以有 2*k 种状态,且 dp [i][j] 取决于 dp [i-1][j]。
所以我们可以把交易过程写在 for 循环中,循环中记录第 j 次买和卖的交易。
dp 数组初始化:如果 j 的下标为偶数(持有),dp [0][j] 值为 -prices [0],否则为 0。
|
|
买卖股票的最佳时机含冷冻期
|
|
本题和 买卖股票的最佳时机 II 不同的地方在于:添加了交易冷冻期,在冷冻期期间,不能进行交易。
冷冻期表示:卖出股票的后一天,不能进行交易。
所以本题的股票状态分为:持有股票、保持卖出股票、当天卖出股票、冷冻期。
上面的 4 个状态分别对应:dp [i][0]、dp [i][1]、dp [i][2]、dp [i][3]。
递推表达式:dp [i][0]=max (dp [i-1][0],dp [i-1][1]-prices [i],dp [i][3]-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]。
初始化 dp 数组:dp [0][0],dp [0][1],dp [0][2],dp [0][3]=-prices [0],0,0,0。
|
|
买卖股票的最佳时机含手续费
|
|
本题和 买卖股票的最佳时机 II 不同的地方在于:本题在完成一次股票交易后,需要支付交易费用。
|
|
子序列问题
最长递增子序列
|
|
我们需要计算最长子序列的长度。dp [i] 表示以 nums [i] 为结尾的数组的最长子序列的长度。
递推表达式:使用变量 j 遍历从 0 到 i 的每个位置,如果 nums [j]<nums [i],则统计次数加 1,也就是:dp [i]=max (dp [i],dp [j]+1)。
初始化 dp 数组:dp [0]=1。
最后需要返回的不是 dp [n-1],因为要求的最长子序列不一定是以 nums [n-1] 结尾的,我们需要遍历 dp 数组,找到最大值返回。
|
|
上面的时间复杂度为 ,有一种时间复杂度为 的写法,思路如下:
维护一个单调递增的数组(dp),在遍历元素 num 时,使用二分查找,确定 num 应该插入到 dp 数组中的位置 index:
- 如果 index 大于 len (dp),说明遇到的元素大于 dp 数组的最后一个元素,则直接将 num 加入到 dp 数组中;
- 否则,将 index 位置的值更新为 num(始终维持一个 dp 数组的单调性)。
最后,dp 数组的长度就是要求的最长递增子序列的长度。
|
|
最长连续递增序列
|
|
贪心算法实现:
|
|
动态规划实现:
本题和 最长递增子序列 的区别在于:上一题不要求子序列的连续性,所以 j 要从 0 到 i 的每个位置都去遍历一遍,然后取出最大值。
在本题中,只需要比较 i 和 i-1 位置即可(连续),如果 nums [i]>nums [i-1],则计数加 1。
|
|
最长重复子数组
|
|
本题需要计算两个数组的最大重复子数组的长度。
由于需要比较两个数组的不同位置,所以我们需要使用二维的 dp 数组。
dp [i][j] 表示以 nums1 [i-1] 和 nums2 [j-1] 结尾的两个数组的最大重复子数组的长度。
递推表达式:如果 nums1 [i-1]==nums2 [j-1],则 dp [i][j]=dp [i-1][j-1]+1。
dp 数组初始化:dp [i][0] 和 dp [0][j] 都是没有意义的,应该初始化为 0。
|
|
最长公共子序列
|
|
本题和 最长重复子数组 的区别在于:本题求的是公共子序列(不用连续),而不是子数组。
dp [i][0] 和 dp [0][j] 都是没有意义的,初始值应该设置为 0。
|
|
不相交的线
|
|
本题看上去很复杂,但其实就是求最长公共子序列,因为公共子序列的元素连接起来一定是不相交的。
|
|
最大子数组和
|
|
方法 1:贪心算法
|
|
方法 2:动态规划
dp [i] 表示 i 位置及之前的数组中,最大子数组(连续)的和为 dp [i]。
递推表达式,在第 i 个位置,有两种选择:
- 延续前面的数组(dp [i-1]+nums [i]);
- 重新开始新的子数组(nums [i])。
应该取二者中的较大值,即:dp [i]=max (dp [i-1]+nums [i],nums [i])。
dp 数组初始化:dp [0]=nums [0]。
|
|
判断子序列
|
|
本题和 最长公共子序列 基本上是一样的,我们只需要求出字符串 t 和 s 的最长公共子序列的长度 maxLen,
然后判断 maxLen 和 len (s) 是否相等,如果相等的话,说明 s 是 t 的子序列。
但在遍历时,我们只能从 t 中删除一些元素,而不能从 s 中删除元素(这是和 最长公共子序列 最大的区别)。
|
|
不同的子序列
本题较为复杂,建议多加练习。
|
|
本题计算有多少种删除 s 字符串的操作,可以使 s 变成 t。
dp [i][j] 表示以 s [i-1] 结尾的数组和以 t [j-1] 结尾的数组,不同子序列的数量为 dp [i][j]。
递推表达式:
如果 s [i-1] 和 t [j-1] 相等,有两种情况:
-
同时不考虑 s 和 t 的最后一个元素(dp [i-1][j-1]);
-
删除 s 的最后一个元素,因为 s 中可能存在多个和 t 中元素相等的情况,所以可以从 s 中删除最后一个元素,再进行比较(dp [i-1][j])
即:dp [i][j]=dp [i-1][j-1]+dp [i-1][j]
如果 s [i-1] 和 t [j-1] 不相等,则:dp [i][j]=dp [i-1][j](删除 s 的最后一个元素)
初始化 dp 数组:dp [i][0]=1, dp [0][j]=0, dp [0][0]=1。
|
|
两个字符串的删除操作
|
|
本题计算:使得字符串 word1 和字符串 word2 相同所需的最小步数。
dp [i][j] 表示以 word1 [i-1] 结尾的数组和以 word2 [j-1] 结尾的数组,使得它们二者相同所需要的最小步数。
递推表达式:如果 word1 [i-1]==word2 [j-1],dp [i][j]=dp [i-1][j-1]。
否则,有两种情况:
-
从 word1 中删除最后一个位置的元素,删除步数加 1,;
-
从 word2 中删除最后一个位置的元素,删除步数加 1。
同时从 word1 中和 word2 中删除最后一个位置的元素包含在了前面两种情况中,不再单独列出。
则递推表达式为:dp [i][j]=min (dp [i-1][j]+1,dp [i][j-1]+1)。
初始化 dp 数组:dp [0][j]=j, dp [i][0]=i。
|
|
编辑距离
|
|
本题可以对 word1 和 word2 进行增 1、删 1、改 1 的操作,计算最少需要多少步可以让两个字符串相同。
dp [i][j] 表示以 word1 [i-1] 和 word2 [j-1] 结尾的字符串,最少需要 dp [i][j] 步可以让两个字符串相同。
递推表达式,如果 word1 [i-1]==word2 [j-1],则 dp [i][j]=dp [i-1][j-1]。
否则,分为几种情况:
-
从 word1 中删除最后一个元素(dp [i-1][j]+1);
-
从 word2 中删除最后一个元素(dp [i][j-1]+1)(这里将 word1 增加元素的操作转换成了 word2 删除元素的操作);
-
替换 word1 的最后一个元素为 word2 的最后一个元素(dp [i-1][j-1]+1)。
我们需要取上面三种情况的最小值,即:dp [i][j]=min (dp [i-1][j]+1,dp [i][j-1]+1,dp [i-1][j-1]+1)。
初始化 dp 数组:dp [i][0]=i, dp [0][j]=j。
|
|
回文子串
本题较难,建议多加练习。
|
|
方法 1,暴力搜索:先获得 s 的所有子字符串,然后判断每个子字符串是否是回文的。
|
|
方法 2:动态规划
本题使用动态规划,需要重点明确 dp 数组的含义,以及如何进行递推。
dp [i][j] 表示 i 到 j(包含)范围内的字符串是否是回文字符串,如果是 dp [i][j] 值为 true,否则为 false。
递推表达式:
如果 s [i]==s [j],分为 3 种情况:
-
如果 j==i,也就是只有 1 个元素,dp [i][j]=true;
-
如果 j==i+1,也就是只有 2 个元素,dp [i][j]=true;
-
如果 j>i+1,也就是至少有 3 个元素,则 dp [i][j] 需要根据 dp [i-1][j-1] 的值进行计算。
如果 dp [i+1][j-1] 值为 true,则 dp [i][j]=true。
我们使用 count 变量统计遇到的回文子串的数量,也就是每次 dp [i][j]==1 时,count++。
dp 数组初始化:dp 数组默认值为 false。
遍历顺序:由于 dp [i][j] 需要根据 dp [i+1][j-1] 的值进行计算。
那么我们在遍历 i 时需要从大到小,遍历 j 时需要从小到大。
|
|
最长回文子序列
|
|
本题需要计算最长的回文子序列。
dp [i][j] 和上一题一样,都表示 i 位置到 j 位置的字符串中,最长的回文子序列的长度为 dp [i][j]。
递推表达式:
-
如果 s [i]==s [j],则 dp [i][j]=dp [i+1][j-1]+2(长度加 2);
-
否则,分别删去当前区间中最前面的元素和最后面的元素,然后取较大值,即:dp [i][j]=max (dp [i+1][j], dp [i][j-1])
遍历方向:由于 dp [i][j] 需要根据 dp [i+1][j-1]、dp [i+1][j]、dp [i][j-1] 三个值计算得到,所以在遍历 i 时需要从大到小,遍历 j 时需要从小到大。
初始化 dp 数组:由于递推表达式 i 一直加 1,j 一直减 1,则移动到最中间的时候,应该要初始化,也就是 i 等于 j 时,dp [i][i]=1。
|
|