子序列

子序列与动态规划问题

最长递增子序列

https://leetcode.cn/problems/longest-increasing-subsequence/description/

图 0

思路
本题也是代码随想录中子序列问题的第一题,如果没接触过这种题目的话,本题还是很难的,甚至想暴力去搜索也不知道怎么搜。 子序列问题是动态规划解决的经典问题,当前下标i的递增子序列长度,其实和i之前的下表j的子序列长度有关系,那又是什么样的关系呢。

  • 1、dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度
    为什么一定表示 “以nums[i]结尾的最长递增子序” ,因为我们在做递增比较的时候,如果比较 nums[j] 和 nums[i] 的大小,那么两个递增子序列一定分别以nums[j]为结尾 和 nums[i]为结尾, 要不然这个比较就没有意义了,不是尾部元素的比较那么 如何算递增呢。
  • 2、状态转移方程
    • if (nums[i]>nums[j]) dp[i] = max(dp[i],dp[j]+1)这里的含义是,取dp[j]+1的最大值。
  • 3、初始化,子序列的长度最短为1
  • 4、遍历顺序,外层遍历i(1 ~ n),内层遍历j(0 ~ i-1)
  • 还要注意并不是dp[n-1]才是最终的值,最大值应该是dp数组中的最大值
1
2
3
4
5
6
7
8
9
10
11
12
var lengthOfLIS = function(nums) {
let n = nums.length
let dp = Array(n).fill(1)
let result = 1
for(let i=1;i<n;i++){
for(let j=0;j<i;j++){
if (nums[i]>nums[j]) dp[i] = Math.max(dp[i],dp[j]+1)
}
if (dp[i]>result) result = dp[i]
}
return result
};

最长递增子序列

https://leetcode.cn/problems/longest-continuous-increasing-subsequence/description/

图 1

思路
这里的序列要连续,因此当前的状态要由前一个决定

  • 1、dp[i]:以下标i为结尾的连续递增的子序列长度为dp[i]
  • 2、if(nums[i]>nums[i-1]) dp[i] = dp[i-1]+1
  • 3、初始化dp[i]都等于1
  • 4、循环,只需要一层循环遍历i即可
  • 当然同样注意最终答案不是dp[n-1],而是dp数组中的最大值
1
2
3
4
5
6
7
8
9
10
var findLengthOfLCIS = function(nums) {
let n = nums.length
let dp = Array(n).fill(1)
let result =1
for(let i=1;i<n;i++){
if(nums[i]>nums[i-1]) dp[i] = dp[i-1]+1
if (dp[i]>result) result = dp[i]
}
return result
};

最长重复子数组

https://leetcode.cn/problems/maximum-length-of-repeated-subarray/description/

图 2

难点
动态数组怎么表示,又是什么含义

思路
如果要暴力破解的话,把每个数组的子数组遍历出来,然后两两比较,得出最长重复子数组

  • 1、二维dp[i][j],以i-1为结尾的nums[1]和以j-1为结尾的nums[2],的最长重复子数组的长度;本质上也是两两比较了,但是由于我记录了前一个状态的值,在进行下一个状态比较时,子需要对比一个数即可结合前一个状态得出现在状态的值。

    • 为什么i,j表示下标为i-1和j-1呢?因为有dp[i-1],dp[j-1],因此i和j的最小值应该为1。dp数组遍历时候从1开始,但是nums数组要从0号元素开始比较。
  • 2、递推公式:if(nums[i-1]==nums[j-1]) dp[i][j] = dp[i-1][j-1] +1;
    图 4

  • 3、初始化:dp[i][0] = 0,dp[0][j] = 0;因为i和j遍历时都从1开始

  • 4、遍历顺序:

  • 这题也是结果在过程中,而不是遍历完之后。

1
2
3
4
5
6
7
8
9
10
11
12
13
var findLength = function(nums1, nums2) {
let n = nums1.length
let m = nums2.length
let dp = Array.from({length:n+1},()=>Array(m+1).fill(0))
let result = 0
for (let i=1;i<=n;i++){
for(let j=1;j<=m;j++){
if(nums1[i-1] == nums2[j-1]) dp[i][j]=dp[i-1][j-1]+1
if (dp[i][j] > result) result = dp[i][j]
}
}
return result
};

最长公共子序列

https://leetcode.cn/problems/longest-common-subsequence/description/

图 3

思路
这道题就是可以不连续,可以不连续前面一维数组做过,不过现在dp需要时二维数组,因此这题主要突破点再递推公式上

  • 1、dp[i][j]表示以i-1为结尾的第一个字符串和j-1结尾的第二个字符串的最长公共子序列长度

  • 2、递推公式:主要就是两大情况: text1[i - 1] 与 text2[j - 1]相同,text1[i - 1] 与 text2[j - 1]不相同。

    • if (text1[i-1]==text2[j-1]) dp[i][j] = dp[i-1][j-1]+1;
    • else dp[i][j] = max(dp[i-1][j],dp[i][j-1])
  • 3、初始化,均为0即可,

  • 4、遍历顺序,那么为了在递推的过程中,这三个方向都是经过计算的数值,所以要从前向后,从上到下来遍历这个矩阵。
    图 5

1
2
3
4
5
6
7
8
9
10
11
12
var longestCommonSubsequence = function(text1, text2) {
let n = text1.length
let m = text2.length
let dp = Array.from({length:n+1},()=> Array(m+1).fill(0))
for(let i=1;i<=n;i++){
for(let j=1;j<=m;j++){
if (text1[i-1]==text2[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[n][m]
};

不相交的线

https://leetcode.cn/problems/uncrossed-lines/description/

图 2

思路
其实也就是说A和B的最长公共子序列是[1,4],长度为2。 这个公共子序列指的是相对顺序不变(即数字4在字符串A中数字1的后面,那么数字4也应该在字符串B数字1的后面)

这么分析完之后,大家可以发现:本题说是求绘制的最大连线数,其实就是求两个字符串的最长公共子序列的长度!

1
2
3
4
5
6
7
8
9
10
11
12
var maxUncrossedLines = function(nums1, nums2) {
let n = nums1.length
let m = nums2.length
let dp = Array.from({length:n+1},()=>Array(m+1).fill(0))
for(let i = 1;i<=n;i++){
for(let j = 1;j<=m;j++){
if (nums1[i-1]==nums2[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[n][m]
};

买卖股票

买卖股票

买卖股票最佳时机(121)

https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/description/

图 0

思路
股票只能买卖一次

  • 1、dp[i][0],dp[i][1] 分别表示持有股票和不持有股票的现金最大值
  • 2、递推公式:
    • dp[i][0] = max(dp[i-1][0],-prices[i]) 维持前一天持有股票的状态或者买入今天的股票,这是因为只能买一次买一次所以是-prices[i]
    • dp[i][1] = max(dp[i-1][1],dp[i-1][0]+prices[i]) 维持前一天不持有股票的状态,或者在今天卖出股票
  • 3、初始化,dp[0][0] = -prices[0]
  • 4、dp[i]的状态依赖dp[i-1]的状态因此从前往后遍历
    可以把二维dp数组压缩为一维如下代码所示
1
2
3
4
5
6
7
8
9
10
11
var maxProfit = function(prices) {
let dp = Array(2).fill(0)
dp[0] = -prices[0]
let n = prices.length
for(let i=1;i<n;i++ ){
let val0 = Math.max(dp[0],-prices[i])
let val1 = Math.max(dp[0]+prices[i],dp[1])
dp = [val0,val1]
}
return dp[1]
};

买卖股票的最佳时机(122)

https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/description/

图 1

思路
股票只能买卖多次

  • 1、dp[i][0],dp[i][1] 分别表示持有股票和不持有股票的现金最大值
  • 2、递推公式:
    • dp[i][0] = max(dp[i-1][0],dp[i-1][1]-prices[i]) 维持前一天持有股票的状态或者买入今天的股票,(今日买入手中的现金=前一天不持有的利益-今日股票价格)
    • dp[i][1] = max(dp[i-1][1],dp[i-1][0]+prices[i]) 维持前一天不持有股票的状态,或者在今天卖出股票
  • 3、初始化,dp[0][0] = -prices[0]
  • 4、dp[i]的状态依赖dp[i-1]的状态因此从前往后遍历
    可以把二维dp数组压缩为一维如下代码所示
1
2
3
4
5
6
7
8
9
10
11
12
13
var maxProfit = function(prices) {
let n = prices.length
let dp = Array(2).fill(0)
dp[0] = -prices[0]
for (let i = 1;i<n;i++){

let val0 = Math.max(dp[0],dp[1]-prices[i])
let val1 = Math.max(dp[0]+prices[i],dp[1])
dp = [val0,val1]
console.log(dp)
}
return dp[1]
};

买卖股票的最佳时机(123)

https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iii/description/

图 2

思路
关键在于至多买卖两次,这意味着可以买卖一次,可以买卖两次,也可以不买卖

  • 1、dp数组状态:dp[i][0],dp[i][1],dp[i][2],dp[i][3],dp[i][4]
    • 0:没有操作
    • 1:第一天持有股票
    • 2:第一天不持有股票
    • 3:第二次持有股票
    • 4:第二次不持有股票
  • 2、递推公式
    • dp[i][0] = dp[i-1][0]
    • dp[i][1] = max(dp[i-1][1],dp[i-1][0]-prices[i]) 保持前一天持有状态,今天刚刚买入。
    • max(dp[i-1][1] + prices[i],dp[i-1][2]) 延续前一天不持有的状态或者,前一天持有今天买了
    • max(dp[i-1][3],dp[i-1][2]-prices[i]),延续前一天买入第二次股票的状态,前一天第一次不持有减去今天股票价格
    • max(dp[i-1][4],dp[i-1][3]+prices[i])
  • 3、初始化:
    • dp[0][0] = 0
    • dp[0][1] = -prices[0]
    • dp[0][2] = 0,同一天买卖,手里现金增加0
    • dp[0][3] = -prices[0],第一天买入,卖出,又买入
    • dp[0][4] = 0
  • 4、遍历:从前往后
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var maxProfit = function(prices) {
let n = prices.length
let dp = Array(5).fill(0)
dp[1] = -prices[0]
dp[3] = -prices[0]
for(let i=1;i<n;i++){
let val0 = dp[0]
let val1 = Math.max(dp[0] - prices[i],dp[1])
let val2 = Math.max(dp[1]+prices[i],dp[2])
let val3 = Math.max(dp[3],dp[2]-prices[i])
let val4 = Math.max(dp[4],dp[3]+prices[i])
dp = [val0,val1,val2,val3,val4]
}
return dp[4]
};

买卖股票最佳时机(188)

https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iv/description/

图 3

思路
至多可以买卖k
从前面一道题可以看出规律,dp[i][k]的状态由dp[i-1][k]dp[i-1][k-1]决定

  • 1、dp[i][j],(0<i<n;0<j<2*k),表示第i天的2k种状态,
  • 2、递推公式,dp[i][j]
    • j是奇数,持有状态:max(dp[i-1][j],dp[i-1][j-1]-prices[i])
    • j是偶数,不持有状态:max(dp[i-1][j],dp[i-1[j]+prices[i]])
  • 3、初始化,索引为奇数的元素全部初始化为-prices[0],其余全为0
  • 4、遍历,从前往后
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var maxProfit = function(k, prices) {
let n = prices.length
let dp = Array(2*k+1).fill(0)
for (let i=1;i<2*k;i+=2){
dp[i] = -prices[0]
}
for(let i=1;i<n;i++){
let arr =[0]
for (let j=1;j<=2*k;j++){
if(j%2 === 1) arr.push(Math.max(dp[j],dp[j-1]-prices[i]))
else arr.push(Math.max(dp[j], dp[j - 1] + prices[i]))
}
dp = arr
}
return dp[2*k]
};

买卖股票的最佳时机含冷冻期(309)

https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-cooldown/description/

图 4

思路

  • 1、dp[j]表示当天股票状态,
    • dp[0] 表示持有股票
    • dp[1] 表示保持卖出股票的状态
    • dp[2] 表示当天卖出股票的操作
    • dp[3] 冷冻的期
  • 2、递推公式
    • dp[0] = max(dp[0],dp[3]-prices[i],dp[1]-prices[i])
    • dp[1] = max(dp[1],dp[3])
    • dp[2] = dp[0]+prices[i]
    • dp[3] = dp[2]
  • 3、初始化
    • dp[0] = -prices[i]
    • dp[1] = 0
    • dp[2] = 0
    • dp[3] = 0
  • 4、遍历顺序
    结果取最后一天状态1,2,3的最大值。
1
2
3
4
5
6
7
8
9
10
11
12
13
var maxProfit = function(prices) {
let n = prices.length
let dp = Array(4).fill(0)
dp[0] = -prices[0]
for(let i=0;i<n;i++){
let val0 = Math.max(dp[0],dp[3]-prices[i],dp[1]-prices[i])
let val1 = Math.max(dp[1],dp[3])
let val2 = dp[0]+prices[i]
let val3 = dp[2]
dp = [val0,val1,val2,val3]
}
return Math.max(dp[1],dp[2],dp[3])
};

买卖股票的最佳时机含手续费(714)

https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/description/

图 5

思路

  • 1、dp[j]表示每天的状态
    • dp[0] 持有
    • dp[1] 不持有
  • 2、递推公式
    • dp[0] = max(dp[0],dp[1]-price[i]-fee) ,保持前一天的持有状态,或者在前一天不持有的状态下买入
    • dp[1] = max(dp[1],dp[0]+prices[i]),保持前一天不持有的状态,或者在昨天持有股票的基础上卖出股票
  • 3、初始化
    • dp[0] = -prices[0]-fee
    • dp[1] = 0
  • 4、遍历
1
2
3
4
5
6
7
8
9
10
11
12
var maxProfit = function(prices, fee) {
let n = prices.length
let dp = Array(2).fill(0)
dp[0] = -prices[0]- fee
dp[1] = 0
for(let i=0;i<n;i++){
let val0 = Math.max(dp[0],dp[1]-prices[i]-fee)
let val1 = Math.max(dp[1],dp[0]+prices[i])
dp = [val0,val1]
}
return dp[1]
};

打家劫舍系列

打家劫舍系列

打家劫舍(198)

https://leetcode.cn/problems/house-robber/description/

图 0

思路
五部曲

  • 1、dp[i] 考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]。

  • 2、递推公式:
    偷:dp[i] = dp[i-2] + num[i],不偷:dp[i] = dp[i-1],因此dp[i] = max(dp[i-2] + num[i],dp[i] = dp[i-1])

  • 3、初始化:
    从递推公式dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);可以看出,递推公式的基础就是dp[0] 和 dp[1]

    从dp[i]的定义上来讲,dp[0] 一定是 nums[0],dp[1]就是nums[0]和nums[1]的最大值即:dp[1] = max(nums[0], nums[1]);

  • 4、遍历:i从2到最后一个房间

1
2
3
4
5
6
7
8
9
10
var rob = function(nums) {
let n = nums.length
let dp = Array(n+1).fill(0)
dp[0] = nums[0]
dp[1] = Math.max(dp[0],nums[1])
for (let i=2;i<n;i++){
dp[i] = Math.max(dp[i-2]+nums[i],dp[i-1])
}
return dp[n-1]
};

打家劫舍(213)

https://leetcode.cn/problems/house-robber-ii/description/

图 1

思路
对于一个数组,成环的话主要有如下三种情况:

  • 1、首尾均不考虑
  • 2、考虑首,不考虑尾
  • 3、考虑尾,不考虑首
    情况2和3包含了情况1,因此只需取情况2和3的最大值即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var rob = function(nums) {
let n = nums.length
if (n===1) result = nums[0]
else{
result1 = robRange(nums,1,n-1)
result2 = robRange(nums,0,n-2)
result = Math.max(result1,result2)
}
function robRange(nums,start,end){
let dp = Array(end-start+2).fill(0)
dp[start] = nums[start]
dp[start+1] = Math.max(nums[start],nums[start+1])
console.log(start,end)
for (let i=start+2;i<=end;i++){
dp[i] = Math.max(dp[i-2]+nums[i],dp[i-1])
}
console.log(dp)
return dp[end]
}
return result
};

打家劫舍(337)

https://leetcode.cn/problems/house-robber-iii/description/

图 2

思路
数据结构变成个二叉树了,投了父节点就不能考虑子节点了,而是要考虑孙子节点

  • 每个节点有一个一维dp数组,数组长度为2,dp[0]表示不偷所获金钱的最大值,dp[1]表示偷所获金钱的最大值。

  • 递归三部曲

    • 确定参数,robtree(root)返回dp数组
    • 终止条件,if(cur == null) return [0,0]
    • 遍历顺序是后序遍历,因为父的值需要依靠孩子的值决定
  • 递归五部曲

    • 1、dp[0]表示不偷所获金钱的最大值,dp[1]表示偷所获金钱的最大值。
    • 2、递推公式dp[0] = max(leftdp[0],leftdp[1]) + max(rightdp[0],rightdp[1]) ,dp[1] = cur.val + leftdp[0] + rightdp[0]
    • 3、初始化均为0
    • 4、遍历
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var rob = function(root) {
//递归后续遍历二叉树
const robtree = (rootnode)=>{
let cur = rootnode
if(cur===null) return [0,0]
let leftdp = robtree(cur.left)
let rightdp = robtree(cur.right)
let val0 = Math.max(leftdp[0],leftdp[1]) + Math.max(rightdp[0],rightdp[1])
let val1 = cur.val + leftdp[0] + rightdp[0]
return [val0,val1]
}
//这里解构的时候容易出错
let [result0,result1] = robtree(root)
// console.log(result0,result1)
return Math.max(result0,result1)
};

完全背包

完全背包

什么是完全背包

  • 01背包,每个物品只能使用一次
  • 完全背包:每个物品可以使用无数次

图 0

01背包引出完全背包

1
2
3
4
5
for (let i=0;i<goodsNum;i++){
for (let j=weight[i];j<=bagWeight;j--){
dp[j] = max(dp[j],dp[j-weight[i]]+value[i])
}
}
  • 01背包使用一维滚动数组时,背包容量要倒序遍历,这样保证每个物品只是用一次。
  • 完全背包,物品可以使用无数次,因此将背包容量遍历的时候改为正序遍历,即可实现

图 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//ACM模式
let readline = require('readline')
const rl = readline.createInterface({
input:process.stdin,
output:process.stdout
});
let inputArr = []
rl.on('line',function(line){
inputArr.push(line.split(' '))
}).on('close',function(){
let n = parseInt(inputArr[0][0])
let v = parseInt(inputArr[0][1])
let weight = []
let value = []
for (let i = 1;i<=n;i++){
weight.push(parseInt(inputArr[i][0]))
value.push(parseInt(inputArr[i][1]))
}
let dp = Array(v+1).fill(0)
for (let i=0;i<=n;i++){
//这边这个循环要注意,总写错j的起始值
for(let j=weight[i];j<=v;j++){
dp[j] = Math.max(dp[j],dp[j-weight[i]]+value[i])
}
}
console.log(dp[v])
})

题目

零钱兑换

https://leetcode.cn/problems/coin-change-ii/description/

图 2

思路
这是一个统一方法数的题目,和之前价值不一样的递推公式。

  • bn找价值最高:max(dp[j],dp[j]+dp[j-weight[i]]+value[i])
  • 而找方法数:dp[j]+=dp[j-weight[i]]
    就比如硬币找零:找总额5元,有1,2,5三种硬币,那么dp[5] = dp[4] + dp[3] + dp[0]

五部曲
1、dp[j] 表示总和为j的找零方式;
2、递推公式 dp[j] += dp[j-coins[i]];
3、初始化,dp[0] = 1,其他索引对应的值均为0,因为要加等于,如果都是零加起来也都是零;
4、遍历,两个都是正序,因为完全背包,每种面额的钱有无数张。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
因为则何种情况下物品的遍历顺序是固定的,有{1,2}就没有被{2,1}

1
2
3
4
5
6
7
8
9
10
11
12
var change = function(amount, coins) {
let n = amount
let m = coins.length
let dp = Array(n+1).fill(0)
dp[0] = 1
for (let i=0;i<m;i++){
for(let j = coins[i];j<=n;j++){
dp[j] += dp[j-coins[i]]
}
}
return dp[n]
};

组合总和IV

https://leetcode.cn/problems/combination-sum-iv/description/

图 3

题目实际上是一个排列问题,元素有顺序的要求了,所以遍历顺序是解题关键

动规五部曲
1、dp[j]表示目标为j时的排列数量
2、递推公式:算方法数dp[j] += dp[j-nums[i]]
3、初始化,dp[0]=1,其他均为零
4、遍历,先遍历背包后遍历物品这样物品就有顺序了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var combinationSum4 = function(nums, target) {
let n = target
let m = nums.length
let dp = Array(n+1).fill(0)
dp[0] = 1
// 背包
for (let i=0;i<=n;i++){
// 物品
for (let j=0;j<m;j++){
if (i-nums[j] >=0) dp[i] += dp[i - nums[j]]
}
}
return dp[n]
};

爬楼梯(进阶版)

https://kamacoder.com/problempage.php?pid=1067

图 4

思路
使用动规五部曲分析

  • 1、dp[i]表示爬到第i级台阶的方法数
  • 2、递推公式:dp[i] = dp[i-1] + dp[i-2] + … +dp[i-m]
  • 3、初始化:
    需要初始化dp[0]到dp[m],但是m是变化的,不能直接赋值需要一步步递推。
    以m为5的情况为例:
    dp[1] = 1
    dp[2] = dp[1] + 1
    dp[3] = dp[2] + dp[1] + 1
    dp[4] = dp[3] + dp[2] + dp[1] +1
    dp[5] = dp[4] + dp[3] +dp[2] + dp[1] +1
    根据上述规律不难想出,让dp[0] = 1
    此外还可以看出这里的规律和递推公式是如出一辙
    dp[j] = dp[j-1] + dp[j-2] + … dp[j-m]
  • 4、遍历顺序:外层遍历爬上的太结束,内层遍历m
    综上得出如下面的代码。

总结
看了代码随想录的文档,惊觉这竟然是一个完全背包问题。其中要爬最终台阶个数为背包容量;而能爬的台阶个数1,2,… m是物品数量。

每次爬的台阶数可以重复也就是物品可以重复放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const readline = require('readline')
const rl = readline.createInterface({
input:process.stdin,
output:process.stdout
});

rl.on('line',function(line){
var tokens = line.split(' ')
let n = parseInt(tokens[0])
let m = parseInt(tokens[1])
let dp = Array(n+1).fill(0)
dp[0] = 1
for (let i=0;i<=n;i++){
for (let j=1;j<=m && i-j>=0;j++){
dp[i] += dp[i-j]
}
}
console.log(dp[n])
})

零钱兑换(322)

https://leetcode.cn/problems/coin-change/description/

图 5

思路
*1、dp[i] 表示兑换总额为i的金钱需要的最少硬币数
*2、递推公式 dp[j] = min(dp[j],dp[j-coins[i]]+1)
*3、初始化 dp[0] = 0,其余均为一个较大的正整数
*4、遍历顺序:先遍历物品后遍历背包,背包容量从小到。

记错模块
dp数组及其下标的含义、递推公式以及初始化都是没有问题的,但是在遍历顺序上我搞反了,导致错误。组合问题还是要先遍历物品,这样物品有顺序。排列问题才需要物品顺序的变化,要先遍历背包。

图 6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var coinChange = function(coins, amount) {
let n = amount
let m = coins.length
let dp = Array(n+1).fill(Number.MAX_SAFE_INTEGER)
dp[0] = 0
for(let i=0;i<m;i++){
console.log(dp)
for(let j=coins[i];j<=n;j++){
dp[j] = Math.min(dp[j],dp[j-coins[i]]+1)

}
}
if (dp[n] == Number.MAX_SAFE_INTEGER) return -1
else return dp[n]
};

完全平方数

https://leetcode.cn/problems/perfect-squares/description/

图 7

思路
感觉这个和前一道题也没啥区别,只不过需要自己简历完全平方数的数组而已。物品是一个个的完全平方数,背包就是和。
*1、dp[i],表示组成和为i的完全平方数的个数
*2、递推公式,dp[j] = min(dp[j],dp[j-nums[i]]+1)
*3、初始化,dp[0] = 0,其他值为最大正整数
*4、遍历顺序,先遍历物品,后遍历背包,背包正序遍历。

1
2
3
4
5
6
7
8
9
10
11
12
var numSquares = function(n) {
let m = Math.floor(Math.sqrt(n))
const num = Array.from({ length: m }, (_, i) => (i + 1) ** 2)
let dp = Array(n+1).fill(Number.MAX_SAFE_INTEGER)
dp[0] = 0
for(let i=0;i<m;i++){
for(let j = num[i];j<=n;j++){
dp[j] = Math.min(dp[j],dp[j-num[i]]+1)
}
}
return dp[n]
};

单词拆分

https://leetcode.cn/problems/word-break/description/

图 8

思考
难点:背包是什么,背包装满怎么表示?
背包是指字符串s,s能否用数组中的单词表示就是能否装满的意思。

*1、dp[i]表示字符串s[0:i]能否由数组组成
*2、递推公式:i表示背包,j表示物品索引
q = i-wordDict(j)
dp[i] = (dp[q] && s[q,i] === wordDictus[j])
*3、初始化,dp[0] = true,其他为false
*4、这本质上是一个求排列的问题,所以要先遍历背包i后后遍历物品j。

记错模块
直接在内循环中使用dp[i] = dp[q] && s.substring(q, i) === wordDict[j]
则会么写的问题在于忽略了三个问题
*首先i-wordDict[j]的大小是否大于0
*每遍历一个单词dp[i]都要被修改,如果已经找到了单词满足条件,继续遍历,答案势必会错
*substring的用法,应该是小括号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var wordBreak = function(s, wordDict) {
let n = s.length
let m = wordDict.length
let dp = Array(n+1).fill(false)
dp[0] = true
console.log(dp)
for(let i=0;i<=n;i++){
for(let j = 0;j<m;j++){
//这里忘记打.length了
let q = i-wordDict[j].length
if (q >= 0 && dp[q] && s.substring(q, i) === wordDict[j]) {
dp[i] = true;
break; // 找到匹配,无需继续检查其他单词
}
console.log(dp)
}
}
return dp[n]
};

01背包

背包问题

背包分类

图 0

01背包

有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

图 1

二维dp数组

五部曲
1、二维dp数组,dp[i][j],i表示物品的编号,j表示背包重量,存储值表示价值
dp[i][j] 表示从0-i的物品中拿总重量<=j的物品的最高价值。
2、递推公式:if j-weight[i] >= 0 dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]) else dp[i][j] = dp[i-1][j]
如果符合条件,那么在拿i与不拿i的较大值中取,如果j小于i的重量,那只有一个选择,选择dp[i-1][j]
3、初始化,dp[i][0] = 0,因为重量为0,能拿0个物品,价值为0
dp[0][i] 的值取决于weight[i]j,如果 weight[i]<=j , dp[0][i] = value[i] 否则,dp[0][j] = 0
4、遍历 for(let i = 0;i<m;i++){ for(let j=1;j<=n;j++){}}
图 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// js node ACM模式
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const inputArr = [];
rl.on('line',function(line){
inputArr.push(line.split(' '))
}).on('close',function(){
const m = parseInt(inputArr[0][0])
const n = parseInt(inputArr[0][1])
const weight = inputArr[1].map(Number)
const bag = inputArr[2].map(Number)
let dp = Array(m).fill().map(()=> Array(n+1).fill(0))
for(let i=0;i<m;i++){
for(let j=1;j<=n;j++){
if (i==0){
if(j>=weight[i]) dp[i][j]=bag[i]
}
else{
// 两种情况
if(j-weight[i]>=0) dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-weight[i]]+bag[i])
else dp[i][j] = dp[i-1][j]
}
}
}
console.log(dp[m-1][n])
})

一维dp数组(滚动数组)

对于背包问题其实状态都是可以压缩的。

在使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);

与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,只用dp[j](一维数组,也可以理解是一个滚动数组)。

这就是滚动数组的由来,需要满足的条件是上一层可以重复利用,直接拷贝到当前层

图 3

倒序遍历的原因:dp[j]的状态受dp[0~j-1]的状态的影响,因此需要倒序遍历。

图 4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const readline = require('readline')

const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})

inputArr = []
rl.on('line',function(line){
inputArr.push(line.split(' '))
}).on('close',function(){
const m = parseInt(inputArr[0][0])
const n = parseInt(inputArr[0][1])
const mArr = inputArr[1].map(Number)
const vArr = inputArr[2].map(Number)
let dp = Array(n+1).fill(0)
for(let i=0;i<m;i++){
for(let j=n;j>=mArr[i];j--){
dp[j] = Math.max(dp[j],dp[j-mArr[i]]+vArr[i])
}
}
console.log(dp[n])
})

题目

分割等和子集416

https://leetcode.cn/problems/partition-equal-subset-sum/description/

图 5

思路
分成两个和相等的数组,只要有一个子集满足 sum(arr)/2 即可

这里没有价值,以物品重量为价值,weight = value 背包装满到数组和的二分之一时,价值达到同样的值就算是成功了。
dp[target] = target

五部曲
1、dp[j] 含义,容量为j的背包所能装的最大价值
2、递推公式,dp[j] = max(dp[j],dp[j-nums[i]])
3、初始化:dp = [0,0,0,…]
4、遍历物品i正序,背包容量j逆序。

图 6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var canPartition = function(nums) {
// 数组求和
let sum= nums.reduce((acc, val) => acc + val, 0)
// sum如果是偶数可以继续,如果是奇数直接返回false
if (sum%2 != 0) return false
let target = sum/2
let dp = Array(target+1).fill(0)
for(let i=0;i<nums.length;i++){
for(let j=target;j>=nums[i];j--){
dp[j] = Math.max(dp[j],dp[j-nums[i]]+nums[i])
}
}
if (dp[target]==target) return true
return false
};

最后一块石头的重量 II

https://leetcode.cn/problems/last-stone-weight-ii/description/

图 7

思路
将石头分为两组,并尽可能使得两组石头重量尽可能相等
最后剩余石头的重量 = 数组1总和 - 数组2总和
let target = Math.floor(sum/2), 由于向下取整target<= sum-target, sum - dp[target] >= dp[target]

五部曲
1、dp[j],表示石头放入容量为j的背包能达到的最大重量。
2、递推公式:dp[j] = Math.max(dp[j],dp[j-stones[i]+stones[i]])
3、初始化:dp[0] = 0,背包容量为0时能放的重量为0,其余位置也时0,因为递推公式是求最大值。石头的重量是正数,因此初始化为0,不影响结果。
4、遍历顺序:石头正序,背包重量逆序

1
2
3
4
5
6
7
8
9
10
11
var lastStoneWeightII = function(stones) {
let sum = stones.reduce((acc,val)=> acc+val,0)
let target = Math.floor(sum/2)
let dp = Array(target+1).fill(0)
for(let i=0;i<stones.length;i++){
for(let j=target;j>=stones[i];j--){
dp[j] = Math.max(dp[j],dp[j-stones[i]]+stones[i])
}
}
return sum-dp[target]-dp[target]
};

目标和

https://leetcode.cn/problems/target-sum/description/

图 8

思路
讲确定两个数组的问题转换化简为确定一个数组,
把整个数组可以被分成两个子数组,两个子数组中数字和分别为left和right,则有
left - right = target
left + right = sum
可推导出:left - sum + left = target; left = (sum + target)/2
问题转换为组合和为left的问题,可以用回溯暴力破解。

这个问题转换为背包问题是求,装满容量为j的背包的方法。

五部曲
1、dp[j]表示装满容量为j的背包的方法数
2、递推公式:dp[j] = dp[j-nums[i]],
数组为[1,2,3,4,5]
eg:dp[5] = dp[4] + dp[3] + dp[2] + dp[1] + dp[0]
3、初始化:dp[0] = 1
4、遍历顺序:外层遍历数组(正序),内层遍历背包容量(倒序)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var findTargetSumWays = function(nums, target) {
let sum = nums.reduce((acc,val)=>acc+val,0)
if (Math.abs(target) > sum) return 0
if ((sum + target) % 2 == 1) return 0
let left = (sum + target)/2
let dp = Array(left+1).fill(0)
dp[0] = 1
for (let i=0; i<nums.length; i++){
for(let j=left;j>=nums[i];j--){
dp[j] += dp[j-nums[i]]
}
}
return dp[left]
};

一和零

https://leetcode.cn/problems/ones-and-zeroes/description/

图 10

背包容量发生一些变化的01背包,背包容量分为两部分,m用于存储0,n用于存储1

五部曲
1、dp[i][j] 表示0容量为i,1容量为j的背包所能存放的字符串的数量。
2、递推公式:max(dp[i][j],dp[i-zeroNum][j-oneNum]+1); 实际上每一行是一个一维滚动数组
3、初始化:都初始化为0
4、遍历顺序:物品正序,ij逆序。因为是滚动数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var findMaxForm = function(strs, m, n) {
let dp = Array.from(Array(m+1), () => Array(n+1).fill(0));
for (let str of strs) {
let zeroNum = 0, oneNum = 0;
for (let s of str) {
if (s === '1') oneNum++;
else zeroNum++;
}
for(let i = m; i >= zeroNum; i--) {
for (let j = n; j >= oneNum; j--) {
dp[i][j] = Math.max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
}
}
}
return dp[m][n];
};

多重背包

https://kamacoder.com/problempage.php?pid=1066

图 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let readline = require('readline')
const rl = readline.createInterface({
input:process.stdin,
output:process.stdout99
})
let inputArr = []
rl.on('line',function(line){
inputArr.push(line.split(' '))
}).on('close',function(){
let n = parseInt(inputArr[0][0])
let m = parseInt(inputArr[0][1])
let w = inputArr[1].map(Number)
let v = inputArr[2].map(Number)
let num = inputArr[3].map(Number)
let dp = Array(n+1).fill(0)
//遍历物品
for (let i=0;i<m;i++){
//背包
for(let j = n;j>=w[i];j--){
for(let k=1;k<=num[i] && k*w[i]<=j;k++){
dp[j] = Math.max(dp[j],dp[j-w[i]*k]+v[i]*k)
}
}
}
console.log(dp[n])
})

动态规划

动态规划

动态规划基础

动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。

动态规划五部曲
  • 1、dp数组及其下标得含义
  • 2、递推公式
  • 3、dp数组如何初始化呢
  • 4、遍历顺序
  • 5、打印dp数组,为了查错。

基础题目

斐波那契数

https://leetcode.cn/problems/fibonacci-number/description/

图 0

思路

  • 1、dp数组表示什么,dp[i]:第i个斐波那契数的数值
  • 2、递推公式:dp[i] = dp[i-2] + dp[i-1]
  • 3、dp数组初始化:dp[0] = 1,dp[1] = 1
  • 4、遍历顺序:i从2遍历到n
1
2
3
4
5
6
7
8
9
10
11
12
13
var fib = function (n) {
let dp = new Array(n)
dp[0] = 0
dp[1] = 1
if (n == 0) return 0
if (n == 1) return 1
if (n > 1) {
for (let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2]
}
return dp[n]
}
}

爬楼梯

https://leetcode.cn/problems/climbing-stairs/description/

图 1

五部曲解释
1、dp数组表示的是,到达第i+1个台阶的方法数
2、递推公式dp[i] = dp[i-2] + dp[i-1]
3、dp数组初始化,dp[0] = 1,dp[1] = 2; 上一个台阶一种方法,两个台阶两种方法
4、遍历顺序从2~n-1

1
2
3
4
5
6
7
8
9
var climbStairs = function(n) {
let dp=[]
dp[0] = 1
dp[1] = 2
for(let i=2;i<n;i++){
dp[i] = dp[i-1]+dp[i-2]
}
return dp[n-1]
};

使用最小花费爬楼梯

https://leetcode.cn/problems/min-cost-climbing-stairs/description/

图 2

五部曲
1、dp数组,代表到达位置i时消耗的花费
2、递推公式 dp[i] = dp[i-1]+cost[i-1]或者dp[i] = dp[i-2]+cost[i-2],
dp[i] = min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2])
3、初始化,dp[0]=0;dp[1]=0,因为题意明确到达台阶不想上跳是没有花费的。而dp数组的含义是到达位置i的花费,所以dp[0],dp[1]初始化都是0
4、遍历:n是cost长度,顶部在n+1的位置

1
2
3
4
5
6
7
8
9
var minCostClimbingStairs = function(cost) {
let dp = []
dp[0]=0,dp[1]=0;
n = cost.length
for(let i=2;i<=n;i++){
dp[i] = Math.min(dp[i-1] + cost[i-1],dp[i-2] + cost[i-2])
}
return dp[n]
};

不同路径

https://leetcode.cn/problems/unique-paths/description/

图 3

五部曲
1、dp[i][j],从0,0,走到i,j的走法。
2、dp[i][j] = dp[i-1][j] + dp[i][j-1]
3、初始化:dp[0][j] = 1;dp[i][0] = 1
4、遍历:
5、

1
2
3
4
5
6
7
8
9
10
11
12
var uniquePaths = function (m, n) {
let dp = Array(m).fill().map(() => Array(n).fill(0))
dp[0][0] = 1
for (let i = 0; i < m; i++) dp[i][0] = 1
for (let i = 0; i < n; i++) dp[0][i] = 1
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
}
}
return dp[m - 1][n - 1]
}

不同路径II

https://leetcode.cn/problems/unique-paths-ii/description/

图 4

有障碍物的地方路径数量设为0即可解决

五部曲
1、dp数组及下标含义:dp[i][j]表示到达i,j的路径数量
2、递推公式:如果i,j处有障碍,dp[i][j]=0,没有障碍dp[i][j] = dp[i-1][j] + dp[i][j-1]
3、初始化,dp数组全部填充为0,dp[0][0] = 1
4、遍历方法,i:0~m j:0~n,这种遍历方式要搭配if(i > 0) dp[i][j] += dp[i-1][j];
if(j > 0) dp[i][j] += dp[i][j-1];

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var uniquePathsWithObstacles = function(obstacleGrid) {
const m = obstacleGrid.length,n=obstacleGrid[0].length
let dp = Array(m).fill().map(() => Array(n).fill(0))
dp[0][0]=1
for(let i=0;i<m;i++){
for(let j=0;j<n;j++){
if (obstacleGrid[i][j] == 1) dp[i][j]=0
else{
if(i > 0) dp[i][j] += dp[i-1][j];
if(j > 0) dp[i][j] += dp[i][j-1];
}
}
}
return dp[m-1][n-1]
};

整数拆分

https://leetcode.cn/problems/integer-break/description/

图 5

五部曲
1、dp数组及其下标的含义:dp[i]表示i拆分的最大值。
2、递推公式:
图 6

3、初始化:dp[2] = 1,其余均为0
4、遍历顺序:按顺序拆,一定拆出1,拆能两个数和多个数;一定拆除2,拆出两个数和多个数…,求每一次拆的最大值。
图 7

1
2
3
4
5
6
7
8
9
10
var integerBreak = function(n) {
let dp = Array(n+1).fill(0) //n+1因为dp数组下标的含义
dp[2]=1
for(let i=0;i<=n;i++){
for(let j=1;j<= Math.floor(i/2);j++){
dp[i] = Math.max(j*(i-j),j*dp[i-j],dp[i])
}
}
return dp[n]
};

不同的二叉搜索树

https://leetcode.cn/problems/unique-binary-search-trees/description/

图 8

二叉搜索树

图 9

五部曲
1、dp[i] 表示i个结点的二叉搜索数的个数
2、递推公式:dp[i] += dp[j-1] * dp[i-j]
当头结点为j时,左边一定有j-1个结点

3、初始化:dp[0] = 1, dp[1] = 1, dp[2]=2
4、遍历顺序,第一层for循环负责遍历dp[i]的下标,第二个for循环遍历头节点的数值
图 10

1
2
3
4
5
6
7
8
9
10
var numTrees = function(n) {
let dp = Array(n+1).fill(0)
dp[0]=1,dp[1]=1,dp[2]=2
for(let i=3;i<=n;i++){
for(let j=1;j<=i;j++){
dp[i] += dp[j-1]*dp[i-j]
}
}
return dp[n]
};

KMP算法

KMP算法

非常完整的思路https://www.programmercarl.com/0028.%E5%AE%9E%E7%8E%B0strStr.html#%E6%80%9D%E8%B7%AF

三位学者发明的:Knuth,Morris和Pratt,所以取了三位学者名字的首字母。所以叫做KMP

图 1

用途

KMP主要应用在字符串匹配上。

KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。

所以如何记录已经匹配的文本内容,是KMP的重点,也是next数组肩负的重任。

为什么要用前缀表

什么是前后缀

前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串;

后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。

最长相等前后缀

首先要了解文本字符串和模式字符串,前者是题目中提到的haystack,后者是needle。eg:’aabaabaaf’,’aabaaf’

前缀表可以告诉我们应该跳转到哪里继续匹配。而不是从模式字符串的头部重新开始匹配。

图 0

为什么到下表为5时b!=f,可以直接将模式字符串下标调整到2呢。因为最长前后缀相等。
图 1

next实现过程

图 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const getNext = (nextArr, s) => {
let j = 0 //i表示后缀末尾,j表示前缀末尾
nextArr[0] = 0
for (let i = 1; i < s.length; i++) {
//什么情况下要向左移动j
while (s[i] != s[j] && j > 0) {
j = nextArr[j - 1]
}
if (s[i] == s[j]) {
j++
}
nextArr[i] = j
}
return nextArr
}

使用next前缀表实现字符串匹配

图 4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var strStr = function(haystack, needle) {
const getNext = (nextArr, s) => {
let j = 0 //i表示后缀末尾,j表示前缀末尾
nextArr[0] = 0
for (let i = 1; i < s.length; i++) {
while (s[i] != s[j] && j > 0) {
j = nextArr[j - 1]
}
if (s[i] == s[j]) {
j++
}
nextArr[i] = j
}
return nextArr
}
let Arr = new Array(needle.length)
let nextArr = getNext(Arr,needle)
let j=0
//使用next数组实现匹配的过程
for(let i=0;i<haystack.length;i++){
//j跳跃的条件
while(j>0 && j<needle.length && haystack[i] != needle[j]){
j=nextArr[j-1]
}
if(haystack[i] == needle[j]){
j++
}
// 找到匹配字符串的条件
if( j == needle.length){
return i-j+1
}
}
return -1
}

重复的子字符串

https://leetcode.cn/problems/repeated-substring-pattern/description/

图 2

思路1

s+s去掉头和尾,如果s还能出现在其中说明符合题目要求。

1
2
3
4
5
6
7
8
9
10
var repeatedSubstringPattern = function (s) {
let ss = s + s
ss = ss.slice(1, ss.length)
ss = ss.slice(0, ss.length - 1)
if (ss.includes(s)) {
return true
} else {
return false
}
}

思路2

kmp算法,s.length - 最长相等前后缀,表示重复字符串的长度。

如果能被s.length整除,说明符合条件。

当然如果最长相等前后缀为0,纳智捷说明不符合要求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var repeatedSubstringPattern = function(s) {
//最长相等前后缀
let nextArr = new Array(s.length)
nextArr[0] = 0
let j = 0
for (let i = 1; i < s.length; i++) {
while (j > 0 && s[i] != s[j]) {
j = nextArr[j - 1]
}
if (s[i] == s[j]) {
j++
}
nextArr[i] = j
}
//先判断nextArr[nextArr.length - 1] != 0是重要的
// 因为没有最长公共前后缀一定不符合题目要求。

if (
nextArr[nextArr.length - 1] != 0 &&
s.length % (s.length - nextArr[nextArr.length - 1]) === 0
) {
return true
} else {
return false
}
};

翻转字符串中的单词、右旋转字符串

翻转字符串中的单词

https://leetcode.cn/problems/reverse-words-in-a-string/description/

图 0

思路

1、首先需要去除多余的空格
'the sky is blue'
1
2
3
4
5
6
7
8
9
// 使用快慢指针,slowIndex,fastIndex
1.1 先去除字符串首和重复出现的空格
if (strArr[fastIndex] === ' ' && (fastIndex===0 || strArr[fastIndex-1] === ' '))

1.2 去除字符串尾的空格
// 长度 = 最后一位索引 + 1;整个循环结束后慢指针的指向是什么。
// 判断慢指针-1指向的字符是不是空格,是说明原字符串尾部有空格,strArr长度应该是满指针当前位置-1。
// 否说明原字符串尾部没有空格,strArr长度就是满指针所指的位置。
strArr.length = strArr[slowIndex-1] === ' '? slowIndex-1 : slowIndex
2、将整个字符串进行翻转
eg:'the sky is blue' --> 'eulb si yks eht'
1
2
3
4
5
6
使用双指针
while(left<right){
[strArr[left],strArr[right]] = [strArr[right],strArr[left]]
left++
right++
}
3、然后找到每一个单词,对该单词进行翻转
eg:'eulb si yks eht' --> 'blue is the sky'
1
2
// 分两步处理
// 假设字符串中有n个单词,先处理前n个单词,再处理最后一个单词

求解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

var reverseWords = function (s) {
let strArr = Array.from(s)
// 去除多余空格的函数
const removeBlank = (strArr) => {
// 移除前面和中间空格
let slowIndex = 0
let fastIndex = 0
while (fastIndex < strArr.length) {
// 去除最前面和中间多余的空格
if (
strArr[fastIndex] == ' ' &&
(fastIndex == 0 || strArr[fastIndex - 1] == ' ')
) {
fastIndex++
} else {
strArr[slowIndex] = strArr[fastIndex]
slowIndex++
fastIndex++
}
}
strArr.length =
strArr[slowIndex - 1] === ' ' ? slowIndex - 1 : slowIndex
}
//翻转字符串的函数,参数1是字符串,参数2是待处理字符串的起点索引,参数3是待处理字符串的终点索引
const reverse_all = (arr, start, end) => {
let left = start
let right = end
while (left < right) {
;[arr[left], arr[right]] = [arr[right], arr[left]]
left++
right--
}
}
removeBlank(strArr)
reverse_all(strArr, 0, strArr.length - 1)
let startIndex = 0
for (let i = 0; i < strArr.length; i++) {
if (strArr[i + 1] == ' ') {
reverse_all(strArr, startIndex, i)
startIndex = i + 2
}
}
reverse_all(strArr, startIndex, strArr.length - 1)
return strArr.join('')
}

右旋转字符串

https://kamacoder.com/problempage.php?pid=1065

图 1

思路

同上一道题思路相似,先全部反转,然后再把两部分各进行一次反转。

图 3

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var right_circle_string = (s,k)=>{
let strArr = Array.from(s)
//反转函数
function reverse_word(sArr,start,end){
let left = start,right = end
while(left<right){
[sArr[left], sArr[right]] = [sArr[right], sArr[left]]
left++
right--
}
return sArr
}
strArr = reverse_word(strArr,0,strArr.length-1)
strArr = reverse_word(strArr,0,k-1)
strArr = reverse_word(strArr,k,strArr.length-1)
return strArr
}

划分字母区间

划分字母区间

https://leetcode.cn/problems/partition-labels/description/

图 0

思路

1、使用哈希表将每个字符的最远位置记录下来
2、从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点

图 1

求解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var partitionLabels = function(s) {
let map = new Map()
for(let i=0;i<s.length;i++){
map.set(s[i],i)
}
let result = []
let left=0,right=0
for(let i=0;i<s.length;i++){
right = Math.max(right,map.get(s[i]))
// 找到分割点的标志。
if (right === i){
result.push(right-left+1)
left = i+1
}
}
return result
};

单调递增的数字

单调递增的数字

思路

这个思路我是想不到,想到了就很容易实现了。

一旦出现strNum[i - 1] > strNum[i]的情况(非单调递增),首先想让strNum[i - 1]–,然后strNum[i]给为9

此时是从前向后遍历还是从后向前遍历呢?

从前向后遍历的话,遇到strNum[i - 1] > strNum[i]的情况,让strNum[i - 1]减一,但此时如果strNum[i - 1]减一了,可能又小于strNum[i - 2]。

这么说有点抽象,举个例子,数字:332,从前向后遍历的话,那么就把变成了329,此时2又小于了第一位的3了,真正的结果应该是299。

那么从后向前遍历,就可以重复利用上次比较得出的结果了,从后向前遍历332的数值变化为:332 -> 329 -> 299

确定了遍历顺序之后,那么此时局部最优就可以推出全局,找不出反例,试试贪心。

求解

1
2
3
4
5
6
7
8
9
10
11
12
var monotoneIncreasingDigits = function(n) {
let str = n.toString().split('')
for(let i=str.length-1;i>0;i--){
if (str[i]<str[i-1]){
str[i-1]--
for(let j=i;j<str.length;j++){
str[j] = 9
}
}
}
return parseInt(str.join(''))
};