动态规划
终于开始新的一章了,动态规划的威力强大,其实也相对的有一些讨论,比起贪心算法来说更有迹可循,所以应该会较轻松一些。
动态规划五部曲如下,
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
509. 斐波那契数
斐波那契数,通常用 F(n) 表示,形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是: F(0) = 0,F(1) = 1 F(n) = F(n - 1) + F(n - 2),其中 n > 1 给你n ,请计算 F(n) 。
这题当然很简单了,但是同样可以用动态规划五部曲来解决,简单题不可忽视,是用来帮忙熟悉套路进行练习的,只有通过简单题掌握了方法做难题时才有思路。
数组含义dp[i]第i个斐波那契数
初始化dp[0]=0 dp[1]=1
转移函数 dp[i]=dp[i-1]+dp[i-2]
遍历顺序 从前向后,后面的数依赖于前面的值
举例
70. 爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
这题也很明显了,当前的可能性由前两步决定,所以转移函数很容易确定。
746. 使用最小花费爬楼梯
旧题目描述:
数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。
每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。
请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯
这题思路比较简单,主要是要明确dp数组的含义,然后把初始化条件确定好,至于转移函数则很明显。
62.不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
这题就引入了单层dp数组和双层dp数组了,双层dp数组非常直观,其实单层也比较直观,只是把同一段数组反复利用,可以节省使用空间,最后保留的只是最后一行的值。
还有一种办法是使用数论的方法,一共要走m+n-2步其中必有m-1步向下,n-1步向右,不同的只是组合问题,另外为防止溢出,需要每一步都除以分母而不是最后再除。
63. 不同路径 II
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
这题和上题很不一样,主要是有了障碍物,比较重要的是初始化的时候,第一层的情况如果遇到了障碍,后面的则全部无法到达,除此之外,就是后面遍历时,障碍物要设置为0,转移函数和上题一样。
343. 整数拆分
给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。
这里就是对转移函数有些要求了,核心公式就是这里dp[i]是第i个数的最大乘积
这是因为要将i所有可能的两数拆分都遍历出来,但是这里最大乘积其实没有包括第i个数自身所以要考虑进来。
然后就是初始化问题要合理初始化的话,就是2=1+1,所以dp[2]=1*1,1的话没有意义。
96.不同的二叉搜索树
给定一个整数 n,求以 1 ... n 为节点组成的二叉搜索树有多少种?
这题看似简单,其实还是需要好好分析的,主要思路就是当根节点从1遍历到时所有组合的和,因为根节点确定,左右子树的组成元素也就确定了,这是侯左右子树组合之和就是所求的,累加起来即可。
二维dp数组01背包
1.对于背包问题,有一种写法, 是使用二维数组,即dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
2.那么可以有两个方向推出来dp[i][j],
- 不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以背包内的价值依然和前面相同。)
- 放物品i:由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值
所以递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
3.首先从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0
那么很明显当 j < weight[0] 的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。 当j >= weight[0]时,dp[0][j] 应该是value[0],因为背包容量放足够放编号0物品。
4.先遍历 物品还是先遍历背包重量呢?其实都可以!! 但是先遍历物品更好理解
416. 分割等和子集
题目难易:中等
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意: 每个数组中的元素不会超过 100 数组的大小不会超过 200
这题算是一个应用,使用01背包问题,什么是背包容量呢?总和的一半,什么是重量呢?数组的元素,基于此可以使用一维数组遍历,最后判断背包容量的大小与所装物体大小是否相等。
需要注意的是初始化以及遍历要从第二个元素开始。
1049.最后一块石头的重量II
题目难度:中等
有一堆石头,每块石头的重量都是正整数。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且x小于等于y。那么粉碎的可能结果如下
如果 ,那么两块石头都会被完全粉碎; 如果 那么重量为x的石头将会完全粉碎,而重量为 y 的石头新重量为 。 最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回 0。
本题乍一看会觉得没有思路,可能会想用贪心或者回溯算法来解决。但是因为我知道自己在做动态规划的问题,所以就还是向动态规划的方向去靠拢。因为是要彼此碰撞后留下最小的一块,所以还是可以把所有的石头分为两堆,最后剩下的重量是两堆的重量之差,这样一来,该问题就和上一个问题是一样的了。如果两堆的重量接近则剩余量就少,最好的情况就是各等于一半。这样就还是转化为背包问题了。这里需要提到背包的遍历顺序,我一开始是从小向大遍历,结果就出现了重复使用的情况,所以还是要好好思考。
另外还有个问题,我先在刷题的时候是知道每道题是可以用这个方法解决的,可是如果没有这个前提,我还能做好吗?这也是个问题。等这遍刷完后,就要随机做题了。
494.目标和
难度:中等
给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。
返回可以使最终数组合为目标数 S 的所有添加符号的方法数。
这题很重要!!!
老实说,这题我算没做出来。最后还是看的参考,参考是有一个小的数学推导的其实非常容易,
基于此,使用动态规划就可以求出最后的解。
本体的教训是,不要放弃思考,数学关系随时都有可能用到,如果没有思路不妨试试。
474.一和零
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
这题做得不好,没想到最后忘了和当前值比较大小,只考虑了有当前元素的情况而没考虑没有当前元素的情况,感觉大脑反应不够了,今天的状态很差,几道题都没能仔细地思考, 明天继续吧。
518.零钱兑换II
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
示例 1:
- 输入: amount = 5, coins = [1, 2, 5]
- 输出: 4
这个题有趣地地方在于初始化和遍历顺序,算是完全背包的一个经典题型。完全背包和0-1背包地不同之处就在于,完全背包的物品数量是无限的,这就导致了遍历顺序要从前向后遍历,这样后面遍历地时候就可以利用前面遍历地信息。
至于初始化,我一开始是把所有整除第一个值得量全部赋值1,但是参考只给dp[0]赋值为1,也是同样得道理,后面遍历时自动就可以处理好,不需要额外初始化了。
377. 组合总和 Ⅳ
难度:中等
给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。
示例:
- nums = [1, 2, 3]
- target = 4
所有可能的组合为: (1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1)
请注意,顺序不同的序列被视作不同的组合。
因此输出为 7。
这题很重要!!!
这题乍一看和上题一样,实际上完全不一样。这题是求排列而上题求得是组合。对于排列问题,要先遍历背包容量然后再遍历物品,这样每次遍历得时候才有可能使后面得物品排在前面。这是一个关键。然后就是遍历的时候要进行判断,如果当前物品值大于容量或者最后一个值已经超出范围就跳出。
70. 爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1: 输入: 2 输出: 2 解释: 有两种方法可以爬到楼顶。
- 1 阶 + 1 阶
- 2 阶
示例 2: 输入: 3 输出: 3 解释: 有三种方法可以爬到楼顶。
- 1 阶 + 1 阶 + 1 阶
- 1 阶 + 2 阶
- 2 阶 + 1 阶
这题原本用动态规划很容易,但其实也可以是完全背包的问题,背包容量是阶梯总数,物品重量是可以总的阶数,因为每次都可以走相同的阶数所以相当于无限物品。最后要求的是排列而非组合,所以要先遍历背包容量,再遍历物品。
322. 零钱兑换
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
你可以认为每种硬币的数量是无限的。
唉,我也是服了,明明想到这一点了,谁知道最后也没做出。因为是要最小个数,所以递推公式应该是
不过要有一个判断条件,就是dp[j-coins[i]]要有可行解否则是凑不齐的,思路明明很清晰,怎么最后就没做出来?这一个典型的完全背包问题,也不需要求排列,真的是。
279.完全平方数
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
z这题和上题类似,只要把完全平方数当作物品就可以了,其余的就是初始全为INT_MAX,dp[0]=0,然后依据条件进行判断。
139.单词拆分
给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。
这题很重要!!!
老实说,这题卡了一会儿,首先是字符串的一些方法没有学的很到位,起始位置,长度啥的没有理的很清楚,再就是字符串s和动规数组的长度以及下标是不一样的,这个上面也卡了很久,总之做的很不好。最后慢慢理清之后发现这题是排列而非组合,也就是说顺序是很重要的,所以要先遍历背包然后再遍历物品。其实说起来,转移函数反倒是最容易的。就是当前字符串匹配前之前的也匹配。
198.打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
这题比较有趣,当前的状态取决于之前两个的状态,所以要初始化1,2然后动规公式为
其实就是抢不抢当前的住户。
213.打家劫舍II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,能够偷窃到的最高金额。
这题因为考虑了首尾相接,所以要分成两种情况考虑,即选择第一个元素与未选择第一个元素,如果选择了第一个元素,那么最后一个元素就不必考虑,如果没选择第一个元素,那就考虑最后一个元素,两种情况下取较大的值处理即可。
337.打家劫舍 III
在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。
计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
**这题很重要!!!**这题是动态规划在书上面的应用,主要还是分为两种情况而且递归的返回值也有两个,分别是偷与不偷的值,动态转移函数为
其实就是从数组变成树,核心还是利用子问题的解。
121. 买卖股票的最佳时机
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0.
老实说,这题我其实没太想明白,有个大概的感觉,现在梳理一下。
1.dp数组的含义,截止到当前为止持有股票或未持有股票能获取的最大利润。
2.转移函数
未持有有两种情况,一是卖出,而是之前就未持有而且现在也不买。
持有也有两种情况,一种是今天买下,一种是之前就有,持有和未持有都要选择较大的值保留下来。
3.初始化,就是第一天持有或不持有很容易初始化
4.遍历顺序,从前向后
122.买卖股票的最佳时机II
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
这道题比较有趣,答题思路和上一道差不多,但是转移函数还是要略微修改一下的
区别在于持有时所剩的现金,上一题因为只能卖出去一次,所以持有时要么是本次买入直接取负值,要么是之前买入。而这题因为可以多次买入,所以持有时,就算是本次买入,现金也要加上之前的结果,这就是区别所在。
123.买卖股票的最佳时机III
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
这题其实关键就是dp数组的初始化和dp的动态转移函数了。为了表示两次交易,我设置了四个变量分表表示第一次持有,第一次卖出,第二次持有,第二次卖出,动态转移公式如下,
我一开始写的时候忽略了第一次卖出与第二次卖出的值,只考虑本次卖出,没有考虑本次卖出之前的情况,其实是要与前一次就买出版的情况相比较才是要的答案,经过修改后就可以得出了。
另一个问题是初始化时第二次卖出的值要为-prices[0]的,就是假设当天买入卖出又买入才可以。
188.买卖股票的最佳时机IV
给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
这题其实就是上体的升级版,仔细观察上体的转移函数就可以发现递推公式,这里有个点是dp数组要有(2k+1)个,第一个元素要始终为0方便处理第一次买入的情况。
309.最佳买卖股票时机含冷冻期
给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
- 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
- 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
这题初始化要初始前两个,使用两个变量标记分别是持有和未持有,状态转移函数如下,
如果持有时,要么是之前的状态已经持有,要么是从前前个未持有持续到现在购买即包含了一天的冷冻期,而对未持有的状态,则是前一个未持有或是当前卖出。
看了参考后,使用的是向量机,罗列了四种状态,
据此写出转移函数。
这样的做法明显更符合方法论,即使对更复杂的问题也可以处理不像我是凭感觉,值得学习。
714.买卖股票的最佳时机含手续费
给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
这题应该是股票问题的最后一个了,包含手续费的问题,这里假设在卖出的时候需要交手续费,所以只要在卖出的时候减去手续费就可以了,其余的和基本的股票买卖问题是一样的,可以说有了动态规划,这题变得很简单了。动规公式如下,
同样使用两个量,一个表示当天持有,一个表示当天不持有,反复做这种题有一定的直觉,有可能中间的细节没有清楚。要注意。
300.最长递增子序列
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
这题很重要!!!
这题思考无果,准备看参考。
遮体的关键是dp数组的含义以及如何更新,这里dp数组的含义被定义为以i元素结尾的最长子序列。更新的时候要把所有前面的元素都遍历一遍,只要当前元素大于前面的某个元素,就要把前面那个元素的最大值加一进行比较最后取最大的长度,这里dp数组的更新和之前不同的,也就是不像之前那么取巧了。需要知道之前所有的情况才能的出当前元素的最大值。这里不要害怕循环,诚然有些题可以优化的很好,但是大部分的题只要能给出一个好的解就可以了。
674. 最长连续递增序列
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。
这题比上题可是简单的多了,因为要求连续,所以只需和前一个元素比较就可以了,大于则加1否则就是1.
718. 最长重复子数组
给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。
这题能靠我自己做出来也是相当有成就感,关键是怎么定义dp数组。这里我用了一个二维数组,含义为,以第一个数组i元素为末尾,第二个数组j元素为末尾的最长相等子序列。之所以这样选是因为后面的长度要依赖前面的,只有是以前一个元素为末尾的子序列才能直接相加长度。
知道了dp数组后,就可以在遍历的过程中不断找出最大值最后返回,动规公式很容易,
所以要善于将问题分解为子问题并且做好dp数组的定义,这样构造动规公式时才会容易。
1143.最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0
这题很重要!!!这题很重要!!!这题很重要!!! 我只能说这题真是有趣,先写思路。
与上题的区别是,这题不要求连续了。 dp 数组的含义也有变化,就是以i,j结尾的字符串里的最长公共子序列,不一定要有i,j。因此,当值不相等时不能简单的定义为0,而是要取前面的值中的较大值。动态转移函数为,
和上题相比,多了不相等时的处理。
现在要说说有趣之处了,在最后返回结果时,我一开始用的是dp.size()-1和dp[0].size()-1结果报错说超出时间,可我也没有想到能改进的方法,最后把dp.size()改为text.size()和text2.size()结果就通过了。
首先vector.size()返回的是无符号整型,在与有符号整型比较或加减时,有符号整型会自动转换为无符号,所以 在size为0时,不是-1而是一个正数。
不过当我再次尝试之前的运行时发现又能通过了,真是神奇。
1035.不相交的线
我们在两条独立的水平线上按给定的顺序写下 A 和 B 中的整数。
现在,我们可以绘制一些连接两个数字 A[i] 和 B[j] 的直线,只要 A[i] == B[j],且我们绘制的直线不与任何其他连线(非水平线)相交。
以这种方法绘制线条,并返回我们可以绘制的最大连线数。
这题和上题不能说毫不相关,只能说是一模一样,代码完全一致,只是换了一种描述方法,实质上还是找最长公共子序列。
#53. 最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例: 输入: [-2,1,-3,4,-1,2,1,-5,4] 输出: 6 解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
这题其实很简单,我第一眼的时候是有贪心的思路的,后面用动态规划也比较容易,因为subarray是连续的,所以dp的含义为以当前元素为结尾的subarray的最大值,那么
就理所当然了,这个过程中要随时记录最大值。
392.判断子序列
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
示例 1: 输入:s = "abc", t = "ahbgdc" 输出:true
示例 2: 输入:s = "axc", t = "ahbgdc" 输出:false
这题不过是之前找最长子序列的一个简单应用,只要最后的最长子序列是s的长度,那就说明满足条件,递推公式啥的都是一样的。
其实看了参考后发现还是又可以改进的点的,因为只是要匹配s是否为t的子序列,所以dp数组的含义可以变为以j元素结尾的t字符串所能匹配的最长的s的长度,这样匹配时自不必说,不匹配时只要就可以了,不需要再额外比较。
所以还是不可大意,说不定就有改善的方法。
115.不同的子序列
给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。
字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,"ACE" 是 "ABCDE" 的一个子序列,而 "AEC" 不是)
题目数据保证答案符合 32 位带符号整数范围。
这题很重要!!!
老实说这题不是很容易,也想了很久。一个是dp数组的含义,对于这个dp数组来说,dp[i][j]表示以i结尾的s子串中包含多少以j结尾的t子串。
在定义之后就要开始写递推公式了,这里主要是看当前值是否相等,如果相等的话,那么可能的值就是 这里就是前一个元素所有可能值加上前一个元素对应j-1子串的个数,这里要注意的就是所加的值了,如果不相等,那就直接是前一个元素所有可能的值。
还有一个点是这里数组元素的定义是uint64_t否则最后会超值。
给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。
示例:
- 输入: "sea", "eat"
- 输出: 2
- 解释: 第一步将"sea"变为"ea",第二步将"eat"变为"ea"
这题就是求最长公共子序列换了层皮,其实是一样的。只要求出最长公共子序列的长度,用序列的总长减去公共子序列的长度就是需要的操作数了。
72. 编辑距离
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
这题很重要!!!这题很重要!!!这题很重要!!! 老实说这题我没做出来,最后看了参考也是马马虎虎,主要是对dp数组的含义以及动态公式理解的不到位,或者说思考的角度不对,有些狭隘了,先暂时搁置,以后会再来做一遍的。
647. 回文子串
给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
这题也是有些意思,有趣之处在于遍历顺序,dp[i][j]数组的含义是以i开始j结尾的字符串是否为回文串,这里动态转移函数为因为起始位置依赖于后面的字符串,所以遍历顺序应该是从后向前遍历,而终止位置依赖于前面的,所以遍历顺序为从前向后。这应该是本题的关键了。
516.最长回文子序列
给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000 。
示例 1: 输入: "bbbab" 输出: 4 一个可能的最长回文子序列为 "bbbb"。
示例 2: 输入:"cbbd" 输出: 2 一个可能的最长回文子序列为 "bb"。
这题看似复杂,其实还是找最长公共子序列。不过这里要找的是字符串和它自身的反转,只要找到了,自就是最长回文串。所以思路是一样的,只不过遍历的时候可以稍微调整一些不需要真的翻转。不过看了参考后感觉思路好像不一样,参考里使用的就是dp[i][j]意味着以开头j结尾的字符串里的最长回文子序列。递推公式也略有改变,顺便调整一下遍历顺序,和上题类似。不过最后都可以求出来就是了。所以说合理的转化问题往往可以事半功倍。
动态规划总结
动态规划部分刷完也有几天了,今天总结一下吧。
首先动态规划可以解决好几类典型的问题,核心就是将当前问题分解为子问题,且子问题和当前问题有不同的关系,主要解决步骤就是五部曲
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
1.背包问题
对于背包问题,主要就是0-1背包,完全背包区别在于物品是否可以无限复用,这决定了遍历顺序,这类问题往往不会是显式的形式,而需要自己转化,就是有个目标值,有可选择的集合,不一定是数值也可能是字符串或者其他,总之是集合的某个元素。从集合中组合判断与目标值的关系,或者是要求能否相等,或者是要求能否有集合元素组成等等一类。
2.打家劫舍问题
这个问题比较新鲜其实递推的形式比较明显,就是当前结果与之前或之前几个的关系,因为有不同的要求不同的组合关系,所以可能有不同的情况对应不同的转移公式,最后从记录的结果中选择满足条件的。这里要注意顺序的影响,尤其有道二叉树的应用,就是必须要后序遍历,当前问题依赖于子问题,那么前提就是子问题必须要先解决了才行啊喂!
3.股票问题
股票问题也算老朋友了,也是很经典的应用,主要恶心在购买股票各种不同的要求,比如冷却期,比如手续费,因为有这些要求所以就要多一些变量来存储对应的状态,而不同状态之间有不同的转移函数,而且可能依赖其他的状态,这就是要多个转移函数的问题,分情况讨论最后选出合适的。
4.子序列问题
子序列问题算是比较恶心的了,其实也不能怪他主要是应用的灵活性更高了,简单的套用不一定可以,而且要确实把问题想透彻了才行,不过很多问题看似表述不同其实解决代码完全一样,或者说一个简单的转换就可以了,这就是看破本质了,对于不同的子序列要求,当然转移函数就不一样了,而且dp数组的定义也要明确,有的定义比较奇怪就是为了可以复用子问题,处理好初始化之后就ok了。
不过这里要强调一下编辑距离,这题我确实没做出来,主要是一时搞不太清第三种操作该怎么处理,一开始就试图宏观思考,自己解决这个逻辑,后来发现没必要,大力出奇迹嘛,把所有的工作交给递归,大不了多列几个状态多写几个动规函数,所以逻辑一定要清晰,目标一定要明确!!!
单调栈
通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了
739. 每日温度
请根据每日 气温 列表,重新生成一个列表。对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0 来代替。
例如,给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。
提示:气温 列表长度的范围是 [1, 30000]。每个气温的值的均为华氏度,都是在 [30, 100] 范围内的整数。
这题是单调栈的一个简单应用,算是练练手,核心思路就是满足条件的元素就压入并把前面的清空,不满足条件的元素就直接压入,这样就可以记录前面的信息。
496.下一个更大元素 I
给你两个 没有重复元素 的数组 nums1 和 nums2 ,其中nums1 是 nums2 的子集。
请你找出 nums1 中每个元素在 nums2 中的下一个比其大的值。
nums1 中数字 x 的下一个更大元素是指 x 在 nums2 中对应位置的右边的第一个比 x 大的元素。如果不存在,对应位置输出 -1 。
这题算是上一题的升级版,要用到map标记因为元素保证是不同的了。同样的使用单调栈,满足条件的就压入并且一直弹出,不满足条件的就直接压入,这样最后就可以标记nums2的每个元素所对应的下一份元素,然后遍历一遍就可以了。
503.下一个更大元素II
给定一个循环数组(最后一个元素的下一个元素是数组的第一个元素),输出每个元素的下一个更大元素。数字 x 的下一个更大的元素是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1。
这题和上题的区别仅仅在于要多遍历一遍,第二次遍历的结果不会进行压栈,其余的逻辑一样。
42. 接雨水
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
这题算是经过思考后自己想出来的, 当然已经有了事先的方向朝着固定方向(用单调栈)前进当然很快速。但是这里也有一些细节,首先是栈里元素是从大到小还是从小到大,因为我们知道如果从栈底到栈顶是从大到小,那么每次不符合要求时就意味着可以存储雨水了,否则就继续堆进去。这样当不满足要求时就要进行处理了。那么不满足要求时该做什么呢?怎样做处理完之后后续的操作不需要改变?计算现在为止能存储的雨水量,只要求知道池子底部的高度,两侧的高度从而计算,底部的高度就是栈顶第一个元素的高度,两侧的高度就是现在的高度与栈顶第二个元素高度的较小值,这样就可以计算然后加到总雨水量里。计算完后这个池子就相当于被填平了,不会影响后面的计算,全部可以当作池底处理了。这样遍历完后就可以求出所要的值了。
84.柱状图中最大的矩形
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
这题很重要!!! 按理说这题和上题一样,但是我即使看了参考答案也不是很理解他的逻辑,思考了半天。
首先题目要求的是最大矩形,那么栈的顺序应该是什么样。这里从栈底到栈顶应该是从小到大,然后当不满足要求时,判断当前元素和栈顶第二个元素哪个更低哪个就作为边界,至于宽度就是下标相减。然后就是不不断弹栈直到满足条件。这个过程中因为弹出的元素一定高于两侧所以可以直接求面积而不用担心不满足条件。不断取最大的值即可。
模拟
463. 岛屿的周长
这题确实很简单,只要遍历一遍然后分情况讨论就可以了,如果是边界或者是水,那就可以周长加1.
657. 机器人能否返回原点
在二维平面上,有一个机器人从原点 (0, 0) 开始。给出它的移动顺序,判断这个机器人在完成移动后是否在 (0, 0) 处结束。
移动顺序由字符串表示。字符 move[i] 表示其第 i 次移动。机器人的有效动作有 R(右),L(左),U(上)和 D(下)。如果机器人在完成所有动作后返回原点,则返回 true。否则,返回 false。
注意:机器人“面朝”的方向无关紧要。 “R” 将始终使机器人向右移动一次,“L” 将始终向左移动等。此外,假设每次移动机器人的移动幅度相同。
这题就更简单了,标记上下左右的方向统计x,y的位置就可以了,没啥好说的。
31.下一个排列
实现获取 下一个排列 的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。
如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。
必须 原地 修改,只允许使用额外常数空间。
老实说,这题要看排列的规律。我的思路是找到要更改的那个位置,就是如果当前元素大于前一个元素,说明要更改,在更改后选择最小的大于的元素交换然后排序,有点绕,参考是在遍历的过程中就进行交换重排,但是操作似乎要更多一些。
并查集
首先要知道并查集可以解决什么问题呢?
主要就是集合问题,两个节点在不在一个集合,也可以将两个节点添加到一个集合中。
并查集是一种用于管理元素所属集合的数据结构,实现为一个森林,其中每棵树表示一个集合,树中的节点表示对应集合中的元素。
顾名思义,并查集支持两种操作:
- 合并(Union):合并两个元素所属集合(合并对应的树)
- 查询(Find):查询某个元素所属集合(查询对应的树的根节点),这可以用于判断两个元素是否属于同一集合
并查集在经过修改后可以支持单个元素的删除、移动;使用动态开点线段树还可以实现可持久化并查集。
684.冗余连接
树可以看成是一个连通且 无环 的 无向 图。
给定往一棵 n 个节点 (节点值 1~n) 的树中添加一条边后的图。添加的边的两个顶点包含在 1 到 n 中间,且这条附加的边不属于树中已存在的边。图的信息记录于长度为 n 的二维数组 edges ,edges[i] = [ai, bi] 表示图中在 ai 和 bi 之间存在一条边。
请找出一条可以删去的边,删除后可使得剩余部分是一个有着 n 个节点的树。如果有多个答案,则返回数组 edges 中最后出现的边。
这题就是并查集的一个典型应用,当当前的边构不成环时,节点是没有公共祖先的,但是如果有公共祖先,说明这两个节点就已经在树里,再添加边就要构成环了,因为并查集就是看两个节点在不在同一个集合,所以可以用来判断该题。
这题之所以不能用map来标记出现的节点,是因为即使两个节点已经出现过,但因为两个节点在不同的树,也不会构成环,只有两个节点已经出现并且在同一棵树 或子集时才能认不满足要求。那么如何判断两个节点在同一个集合呢?就是要有标志集合的变量,这里就是根节点。只要节点有相同的根节点那么一定在同一集合。但是在遍历到两个节点时,怎么找根节点或者说怎么随时更新呢?这就是并查集做的事情。
685.冗余连接II
在本问题中,有根树指满足以下条件的 有向 图。该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。
输入一个有向图,该图由一个有着 n 个节点(节点值不重复,从 1 到 n)的树及一条附加的有向边构成。附加的边包含在 1 到 n 中的两个不同顶点间,这条附加的边不属于树中已存在的边。
结果图是一个以边组成的二维数组 edges 。 每个元素是一对 [ui, vi],用以表示 有向 图中连接顶点 ui 和顶点 vi 的边,其中 ui 是 vi 的一个父节点。
返回一条能删除的边,使得剩下的图是有 n 个节点的有根树。若有多个答案,返回最后出现在给定二维数组的答案。
这题很重要!!!这题很重要!!!这题很重要!!!
这题思路倒是很直接,但是一步步做下来是比较麻烦的,我基本上对着参考抄了一遍,所以下一一定要再做一遍。
图论
841.钥匙和房间
有 N 个房间,开始时你位于 0 号房间。每个房间有不同的号码:0,1,2,...,N-1,并且房间里可能有一些钥匙能使你进入下一个房间。
在形式上,对于每个房间 i 都有一个钥匙列表 rooms[i],每个钥匙 rooms[i][j] 由 [0,1,...,N-1] 中的一个整数表示,其中 N = rooms.length。 钥匙 rooms[i][j] = v 可以打开编号为 v 的房间。
最初,除 0 号房间外的其余所有房间都被锁住。
你可以自由地在房间之间来回走动。
如果能进入每个房间返回 true,否则返回 false。
这题自然而然地就用到深度搜索,但是我一开始并没有把它想成有向图,看了参看后用递归的形式实现DFS或BFS而我自己写的时候用的是迭代的办法写出来的DFS,思路还是比较直接的。
127. 单词接龙
字典 wordList 中从单词 beginWord 和 endWord 的 转换序列 是一个按下述规格形成的序列:
- 序列中第一个单词是 beginWord 。
- 序列中最后一个单词是 endWord 。
- 每次转换只能改变一个字母。
- 转换过程中的中间单词必须是字典 wordList 中的单词。
- 给你两个单词 beginWord 和 endWord 和一个字典 wordList ,找到从 beginWord 到 endWord 的 最短转换序列 中的 单词数目 。如果不存在这样的转换序列,返回 0。
老实说这题有点一言难尽,一开始思路确实是有的,就是无向图找最短路径,但是我竟然蠢到用深度搜索和回溯的方法找出全部路径然后选最短的,果不其然超出时间限制,对于这样的问题,广度优先多香的,广度优先最先找到时的路径就是最短路径了,但是在我尝试广度优先后仍然有时间超时,最后把unordered_map改为数组,用下标判断该节点是否访问过节省不少时间,最后才通过。
1356. 根据数字二进制下 1 的数目排序
给你一个整数数组 arr 。请你将数组中的元素按照其二进制表示中数字 1 的数目升序排序。
如果存在多个数字二进制中 1 的数目相同,则必须将它们按照数值大小升序排列。
请你返回排序后的数组。
这题是位运算的一个比较有趣的应用,核心就是怎么求1的个数,公式为n& n-1不断循环直到为1即可,很秀,每次去除最后一位1。
总结
到这里不算附加题目,代码随想录二刷也算完成了,从4.18到5.26这次记录之旅确实比第一遍刷的时候要好很多了,而且这也算是我第一次写总结的文章来记录,有总结确实不一样,一些细节自己觉得没什么但是写出来后确实就会成为一种潜在回忆,而且写的过程中也会再次理清思路加深印象,这个好处一直知道但是一直没做,知道自己真的写了才深以为然,当然我写的比较随意,有很大一部分是复制粘贴的题目就是了。
下一阶段的目标是题目照样还是会继续刷,估计要搜索公司的题了。