目录
动态规划
300. 最长递增子序列
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-increasing-subsequence
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路:
- 设计一个dp数组,dp[i]表示长度为i+1的所有递增子序列中尾数最小为dp[i]。这个dp数组肯定是严格递增的。
例如{1,5,2,6,3,7,4,8};我可以看出两个长度为4的严格递增子序列{1,2,3,4}和{5,6,7,8}。那我dp[3]就应该等于4。我们可以发现dp数组肯定是一个递增数组,你要问为啥?因为我dp[3]=4,我敢保证dp[2]肯定小于4,我不用去在nums数组里挨个挨个计算,很简单,我肯定有一个基于子数组{1,2,3,4}的子数组{1,2,3}。所以我的dp[2]<=3。以此类推嘛。 - 定义一个int类型的数字max,代表最长子序列的值。他是动态更新的
- 用一个for循环遍历整个nums数组,按顺序取出数字num。在dp数组中找到比这个数大的所有数中最小的那个数的位置,很简单,因为我们要保证dp数组中存储的是尾数最小的数字。如果我找不到怎么办,我找到的最大的值dp[max]还要比nums小。那简单啊,max++,然后dp[max]=num;
总结:
- 设计一个dp数组记录长度为i+1的所有递增子序列中尾数最小为dp[i]
- 更新dp,比尾数小,我就覆盖他,比尾数大我就放到他后面
public int lengthOfLIS(int[] nums) {
int max = 0;
int[] dp = new int[nums.length];
for(int num : nums) {
// 二分法查找,时间复杂度为logN
int left = 0, right = max;
while(left < right) {//寻找dp数组中最小的值,当lo>=hi时说明整个数组遍历完了,可以退出循环了
int mid = left+(right-left)/2;
if(dp[mid] < num)//找到一个比拿出来的这个num大的数字中最小的那个。
left = mid+1;
else
right = mid;
}
dp[left] = num;//覆盖掉
if(left == max)//原来是最后一个数字啊,那我+1
max++;
}//如此一来总复杂度为NlogN
return max;
}
673. 最长递增子序列的个数
给定一个未排序的整数数组 nums , 返回最长递增子序列的个数 。
注意 这个数列必须是 严格 递增的。
示例 1:
输入: [1,3,5,4,7]
输出: 2
解释: 有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]。
示例 2:
输入: [2,2,2,2,2]
输出: 5
解释: 最长递增子序列的长度是1,并且存在5个子序列的长度为1,因此输出5。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/number-of-longest-increasing-subsequence
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路:
⭐:这道题和300题很像,但是300题是做了个取巧的动作,我只关注尾数。而这道题目是最长的子序列的情况数,也就是说我们不能光盯着最后一个数字,万一中间也有变化怎么办。所以还是老老实实两个for循环吧。
- 创建两个dp数组dp1和dp2
dp1[i]表示以[0, i]且i必选的最长子序列长度
dp2[i]表示以[0, i]且i必选的最长子序列个数 - 如果外层的数字nums[i] > 内层数字nums[j],说明可以构成递增子序列
- 如果可以构成递增子序列,那我要看看加上nums[i]这个数之后的递增子序列的长度是不是跟我记录的这个dp1[i]一样,如果一样说明好家伙,新的组合。如果大于说明,哎我目前记录的不是最长的子序列长度啊,那我更新一下吧。
基于上面的思想
public int findNumberOfLIS(int[] nums) {
if (nums.length == 0) {
return 0;
}
int[] dp = new int[nums.length];
int[] combination = new int[nums.length];
Arrays.fill(dp, 1);
Arrays.fill(combination, 1);
int max = 1, res = 0;
for (int i = 1; i < dp.length; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
if (dp[j] + 1 > dp[i]) { //如果+1长于当前LIS 则组合数不变
dp[i] = dp[j] + 1;
combination[i] = combination[j];
} else if (dp[j] + 1 == dp[i]) { //如果+1等于当前LIS 则说明找到了新组合
combination[i] += combination[j];//前面以nums[j]作为倒数第二个数的组合数+我已经找到的nums[i]作为结尾的组合数
}
}
}
max = Math.max(max, dp[i]);
}
for (int i = 0; i < nums.length; i++)
if (dp[i] == max) res += combination[i];
return res;
}
1143. 最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
- 例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
示例 1:
输入:text1 = “abcde”, text2 = “ace”
输出:3
解释:最长公共子序列是 “ace” ,它的长度为 3 。
示例 2:
输入:text1 = “abc”, text2 = “abc”
输出:3
解释:最长公共子序列是 “abc” ,它的长度为 3 。
示例 3:
输入:text1 = “abc”, text2 = “def”
输出:0
解释:两个字符串没有公共子序列,返回 0 。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-common-subsequence
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路:
- 首先想到的就是dp数组,因为是两个字符串,所以考虑二维dp数组
- dp[i][j]代表的是字符串1以下标为 i-1 结尾的子序列和字符串2以下标为 j-1 结尾的子序列有最长公共子序列长度为dp[i][j]
- dp数组的初始化:dp[0][j] = 0;dp[i][0] = 0;因为长度为0肯定就没有公共子串嘛
- 公式推导:
- 如果字符串1的第i-1个字符 == 字符串2的第j-1个字符,则dp[i][j] = dp[i-1][j-1]+1;
- 如果不相等,那就选dp[i-1][j]和dp[i][j-1]中大的那个
- 返回dp数组右下角那个值
public int longestCommonSubsequence(String text1, String text2) {
int length1 = text1.length();
int length2 = text2.length();
int[][] dp = new int[length1+1][length2+1];//dp[i][j]代表A中以i结尾的A和以j为结尾的B有最长公共子序列长度dp[i][j]
for(int i = 1; i <= length1; i++){
for(int j = 1; j <= length2; j++){
if(text1.charAt(i-1) == text2.charAt(j-1)){
dp[i][j] = dp[i-1][j-1]+1;
}else{
dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]);
}
}
}
return dp[length1][length2];
}
583. 两个字符串的删除操作
给定两个单词 word1 和 word2 ,返回使得 word1 和 word2 相同所需的最小步数。
每步 可以删除任意一个字符串中的一个字符。
示例 1:
输入: word1 = “sea”, word2 = “eat”
输出: 2
解释: 第一步将 “sea” 变为 “ea” ,第二步将 "eat "变为 “ea”
示例 2:
输入:word1 = “leetcode”, word2 = “etco”
输出:4
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/delete-operation-for-two-strings
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路:
⭐:这题就和583是一样的,583题找出最长公共子串,那我把剩下的删掉不就两个变成一样了吗
public int minDistance(String word1, String word2) {
int length1 = word1.length();
int length2 = word2.length();
int[][] dp = new int[length1+1][length2+1];
for(int i = 1; i <= length1; i++){
for(int j=1; j <= length2; j++){
if(word1.charAt(i-1) == word2.charAt(j-1)){
dp[i][j] = dp[i-1][j-1]+1;
}else{
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
}
}
}
return length1-dp[length1][length2]+length2-dp[length1][length2];
}
72. 编辑距离
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
示例 1:
输入:word1 = “horse”, word2 = “ros”
输出:3
解释:
horse -> rorse (将 ‘h’ 替换为 ‘r’)
rorse -> rose (删除 ‘r’)
rose -> ros (删除 ‘e’)
示例 2:
输入:word1 = “intention”, word2 = “execution”
输出:5
解释:
intention -> inention (删除 ‘t’)
inention -> enention (将 ‘i’ 替换为 ‘e’)
enention -> exention (将 ‘n’ 替换为 ‘x’)
exention -> exection (将 ‘n’ 替换为 ‘c’)
exection -> execution (插入 ‘u’)
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/edit-distance
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路:
⭐:第一眼看到这个题目:
- 两个字符串————>二位dp数组;
- 三种操作————>三个对应公式;
鉴于这两个操作,整体的思路为:
- 建一个二维dp数组:word1前i-1个字符编程word2前j-1个字符要进行dp[i][j]个操作
- 初始化:dp[i][0] = i;直接删除 dp[0][j]=j;直接删除
- 遍历方向:从前往后两个嵌套遍历就行
- 公式:如果word1的第i-1个字符和word2的第j-1个字符相同,那dp[i][j] = dp[i-1][j-1];如果不等:那就分别取删除一个字符情况、插入一个字符、替换一个字符情况中最少的操作步数即可。
public int minDistance(String word1, String word2) {
int length1 = word1.length();
int length2 = word2.length();
int[][] dp = new int[length1+1][length2+1];//word1前i-1个字符变成word2前j-1个字符要进行dp[i][j]个操作
for(int i = 0; i <= length1; i++){
dp[i][0] = i;
}
for(int j = 0; j <= length2; j++){
dp[0][j] = j;
}
for(int i = 1; i <= length1; i++){
for(int j = 1; j <= length2; j++){
if(word1.charAt(i-1) != word2.charAt(j-1)){
dp[i][j] = Math.min(dp[i-1][j]+1, Math.min(dp[i][j-1]+1,dp[i-1][j-1]+1));
}else{
dp[i][j] = dp[i-1][j-1];
}
}
}
return dp[length1][length2];
}
322. 零钱兑换
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/coin-change
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路:
动规四部曲
- 建立dp数组:很简单,一维的就行,dp[i]表示凑齐金额为 i 至少需要dp[i]个硬币
- 初始化:dp[i]都为Integer.MAX_VALUE
- 更新公式:dp[i] = dp[i]和dp[i-coins[j]]中小的那个
- 遍历方向:外层是dp的遍历,内层是硬币coins 的遍历
整道题和爬楼梯很像
public int coinChange(int[] coins, int amount) {
int length = coins.length;
if(length == 0){
return -1;
}
if(amount == 0){
return 0;
}
int[] dp = new int[amount+1];
dp[0] = 0;
Arrays.fill(dp,1,amount+1,Integer.MAX_VALUE);
for(int i = 0; i < length; i++){//需要一个双循环,一个遍历dp数组,一个遍历coins数组
for(int j = coins[i]; j < amount+1; j++){
//这里要特别说明,如果dp[j-coins[i]]都没有改变,那说明当前这个值用该硬币他就是凑不齐的,那就没必要修改了呀,依旧保持Integer.MAX_VALUE即可
if(dp[j-coins[i]] != Integer.MAX_VALUE){
dp[j] = dp[j] < dp[j-coins[i]]+1 ? dp[j] : dp[j-coins[i]] + 1;
}
}
}
if(dp[amount] != Integer.MAX_VALUE){
return dp[amount];
}
return -1;
}
343. 整数拆分
给定一个正整数 n ,将其拆分为 k 个正整的和( k >= 2 ),并使这些整数的乘积最大化。
返回 你可以获得的最大乘积 。
示例 1:
输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。
示例 2:
输入: n = 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/integer-break
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路:
动规四部曲:
- 确定dp数组的定义:数字i被拆分后乘积最大值为dp[i]
- dp初始化:dp[0]和dp[1]不用管,因为拆不了,dp[2] = 1,因为拆分成1和1嘛
- 递推公式:dp[i] = j*dp[i-j];循环遍历找最大
- 遍历方向:肯定是嵌套遍历,最外层遍历i,内层遍历j,j从1遍历到i,表示被拆分出一个j的情况下能达到的最大值
public int integerBreak(int n)
int[] dp = new int[n+1]
dp[2] = 1;
for(int i = 3; i < n+1; i++){
for(int j = 1; j < i; j++){
dp[i] = Math.max(dp[i],Math.max(j*(i-j),j*dp[i-j]));
//之所以要把j*(i-j)加进去,是因为dp[i-j]种拆分的肯定大于1,这是拆分成3个及以上个数字的情况
//如果没有j*(i-j)我们的所有结果里就少了一种拆分成两个数的情况
}
}
return dp[n];
}
241. 为运算表达式设计优先级
给你一个由数字和运算符组成的字符串 expression ,按不同优先级组合数字和运算符,计算并返回所有可能组合的结果。你可以 按任意顺序 返回答案。
示例 1:
输入:expression = “2-1-1”
输出:[0,2]
解释:
((2-1)-1) = 0
(2-(1-1)) = 2
示例 2:
输入:expression = “23-45”
输出:[-34,-14,-10,-10,10]
解释:
(2*(3-(4*5))) = -34
((2*3)-(4*5)) = -14
((2*(3-4))*5) = -10
(2*((3-4)*5)) = -10
(((2*3)-4)*5) = 10
提示:
- 1 <= expression.length <= 20
- expression 由数字和算符 ‘+’、‘-’ 和 ‘*’ 组成。
- 输入表达式中的所有整数值在范围 [0, 99]
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/different-ways-to-add-parentheses
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路:
这一道题实际上就要用到分治的思想了
- 我们按照以“+”、“-”或者“*”为界,将整个运算表达式分成两个部分,我们分别求着两个部分各自的运算值的情况,然后将他们排列组合进行运算不就行了?
- 步骤1说着容易,分别求出来然后排列组合,但是我怎么分别求出来呢?————>这不简单,用递归的思想,每一层递归都是一个新生
- 综上:我们可以知道,先用for循环遍历符号,再用递归求各自的运算值的情况。
- 细节:何时递归可以返回?遇到只有数字没有符号的情况;何时for循环终止?从0开始表里到整个字符串最后一个即可。
- 实现思路:以
expression=“2*3-4*5”
为例
代码实现:
public List<Integer> diffWaysToCompute(String input) {
if(map.containsKey(input)) return map.get(input);
List<Integer> list = new ArrayList<>();
int len = input.length();
for(int i = 0; i < len; i++) {
char c = input.charAt(i);
if(c == '+' || c == '-' || c == '*') { // 出现运算符号,递归求解前半段和后半段。
List<Integer> left = diffWaysToCompute(input.substring(0, i));
List<Integer> right = diffWaysToCompute(input.substring(i+1, input.length()));
for(int l : left) {
for(int r : right) {
switch(c) {
case '+':
list.add(l + r);
break;
case '-':
list.add(l - r);
break;
case '*':
list.add(l * r);
break;
}
}
}
}
}
if(list.size() == 0) list.add(Integer.valueOf(input));//递归结束情况:出现单个数字
return list;
}
97. 交错字符串
给定三个字符串 s1
、s2
、s3
,请你帮忙验证 s3
是否是由 s1
和 s2
交错 组成的。
两个字符串 s
和t
交错 的定义与过程如下,其中每个字符串都会被分割成若干 非空 子字符串:
s = s1 + s2 + ... + sn
t = t1 + t2 + ... + tm
|n - m| <= 1
- 交错 是
s1 + t1 + s2 + t2 + s3 + t3 + ...
或者t1 + s1 + t2 + s2 + t3 + s3 + ...
注意:a + b
意味着字符串 a
和 b
连接。
示例 1:
输入:s1 = “aabcc”, s2 = “dbbca”, s3 = “aadbbcbcac”
输出:true
示例 2:
输入:s1 = “aabcc”, s2 = “dbbca”, s3 = “aadbbbaccc”
输出:false
示例 3:
输入:s1 = “”, s2 = “”, s3 = “”
输出:true
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/interleaving-string
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路:
动规四部曲:
- 确定dp数组的定义:
boolean[][] dp = new boolean[str1.length + 1][str2.length + 1];
dp[i][j]表示str1前i个字符和str2前j个字符串能组成str3前i+j个字符串的可能性为dp[i][j] - dp初始化:
dp[0][0] = true;
俩空字符串肯定相同啊,dp[i][0]则要比对str1.charAt(i) == str3.charAt(i)
,如果返回为true,继续往后遍历,如果返回为false,则后面不用比对了,肯定不等了。dp[0][j]同理。 - 递推公式:比对str1中的字符:
str[i-1] == str3[i+j-1] && dp[i-1][j]
;比对str2中的字符:str2[j - 1] == str3[i + j - 1] && dp[i][j - 1]
- 遍历方向:肯定是嵌套遍历,最外层遍历i,内层遍历j。从前往后遍历。
public boolean isInterleave(String s1, String s2, String s3) {
if (s1.length() + s2.length() != s3.length()) {
return false;
}
char[] str1 = s1.toCharArray();
char[] str2 = s2.toCharArray();
char[] str3 = s3.toCharArray();
boolean[][] dp = new boolean[str1.length + 1][str2.length + 1];
dp[0][0] = true;
for (int i = 1; i <= str1.length; i++) {
// 如果有一个不满足,那么后续的串就都不能拼出str3
if (str1[i - 1] != str3[i - 1]) {
break;
}
dp[i][0] = true;
}
// 填第一行,也就是只选用str2中的字符
for (int j = 1; j <= str2.length; j++) {
// 如果有一个不满足,那么后续的串就都不能拼出str3
if (str2[j - 1] != str3[j - 1]) {
break;
}
dp[0][j] = true;
}
// 填中间的表
for (int i = 1; i <= str1.length; i++) {
for (int j = 1; j <= str2.length; j++) {
// 两种情况:
// 1、str3中[i+j-1]位置的字符是来自str1的
// 这时必须要求str1中[i-1]位置的字符和str3中[i+j-1]位置的字符相同
// 同时dp表中的[i-1,j]是满足条件的
// 2、str3中[i+j-1]位置的字符是来自str2的
// 这时必须要求str2中[j-1]位置的字符和str3中[i+j-1]位置的字符相同
// 同时dp表中的[i,j-1]是满足条件的
// 两种情况只要有一种满足,那么str1中前i个字符和str2中前j个字符就可以拼出str3中这前i+j个字符
if ((str1[i - 1] == str3[i + j - 1]) && dp[i - 1][j] || (str2[j - 1] == str3[i + j - 1]) && dp[i][j - 1]) {
dp[i][j] = true;
}
}
}
return dp[str1.length][str2.length];
}