一、 动态规划专题
①.记忆化递归“搜索”(自顶向下):用一个数组存放(递归子树的)中间结果,并且在下次递归前判断该结果是否计算过,若计算过直接返回,否则就递归,从而实现剪枝的效果,即空间换时间。
②动态规划法(自底向上):找规律,求子问题的解从而得到最终解,写动态转移方程。(找不到动态转移方程就先尝试记忆化弄清问题结构,再反推自底向上)。
动态规划 “三板斧”
-
分治,找到最优子结构
opt[n]=best_of(opt[n-1], opt[n-2], ...)
-
状态定义,i 条件时的状态
f[i]
-
DP方程,也就是递推公式,例如一维的斐波那契递推公式
dp[i] = dp[i-1] + dp[i-2]
; 二维递推公式例如dp[i][j] = max(dp[i-1][j], dp[i][j-1])
,高级的DP公式可能会达到三维甚至三维以上。
推理方法:
对于一维情况,求 dp[i] 可假设 dp[i-1] 已经求出,需要如何判断才能求出下一项;
对于二维情况,利用递推关系用“填表格”的方式顺序计算,每个 dp 项的值其实等于一个递归子调用的结果,即递归子问题的解。
1.爬楼梯
思路: 同斐波拉契数列
练习:120、64
// 递归树里面有重叠子结构 用记忆数组剪枝
var climbStairs = function(n) {
const memo = Array(n+1).fill(-1)
if(n==1) return 1
if(n==2) return 2
if(memo[n]==-1)
memo[n] = climbStairs(n-1) + climbStairs(n-2)
return memo[n]
};
// 有递归子问题 便可动态规划
// 时间、空间复杂度均为O(n)
var climbStairs = function(n) {
const dp = []
dp[1]=1
dp[2]=2
for(let i=3; i<=n; i++)
dp[i]=dp[i-1]+dp[i-2]
return dp[n]
};
// 结果只与前俩次有关可继续优化空间 滑动数组
// 时间复杂度为O(n),时间复杂度为 O(1)
var climbStairs = function(n) {
if (n <= 1) return n;
const dp =[]
dp[1] = 1
dp[2] = 2
for (let i = 3; i <= n; i++) {
let sum = dp[1] + dp[2]
dp[1] = dp[2]
dp[2] = sum
}
return dp[2]
}
2.整数拆分
思路 :遍历分数,分为俩数还是多个数。
练习:279、91、63
// 递归树里面有重叠子结构 存在重复 用记忆数组
var integerBreak = function(n) {
const memo = Array(n+1).fill(-1)
if(n == 2) return 1
if(memo[n]!=-1)
return memo[n]
let res = 1
// 将数字从1开始分 1*(n-1)...i*(n-i)
for(let i=1;i<=n-1;i++)
//分割为俩个数i*(n-i) 或分割为多个数
res = Math.max(res,i*(n-i),i*integerBreak(n-i))
memo[n]=res
return res
};
//转为动态规划
// 时间复杂度:O(n^2) 空间复杂度:O(n)
var integerBreak = function(n) {
const memo =Array(n+1).fill(-1)
memo[2]=1
//memo[i]:数字i分割(至少俩部分)后的最大成绩
for(let i=3;i<=n;i++)
//分割为 俩个数相乘 j*(i-j)
for(let j=1;j<i;j++)
//或分割为多个数:则将i-j继续分割即memo[i-j](显然memo[i-j]已经算过)
memo[i]=Math.max(memo[i], j*(i-j), j*memo[i-j])
return memo[n]
};
3.打家劫舍
思路:不偷当前第i家,则结果为后面 i+1 家的结果;否则结果为nums[i] + 后 i+2家的价值
练习:213、337、309
// 记忆化搜索
var rob = function (nums) {
if (nums == null || !nums.length) return 0
const memo =Array(nums.length).fill(-1)
// 考虑抢劫nums[i...length)范围的所有房子
const tryRob = (nums, i) => {
if (i >= nums.length) return 0;
if(memo[i]!=-1) return memo[i]
let res = Math.max(
//不偷第i间,结果为从i+1间开始
//偷第i间,结果为第i间价值+从i+2间开始
tryRob(nums, i + 1),
tryRob(nums, i + 2) + nums[i])
memo[i] = res
return res
};
return tryRob(nums, 0)
};
// 优化dp,空间复杂度O(n)
var rob = function(nums) {
const len = nums.length;
if(len == 0) return 0
const dp = new Array(len)
dp[0] = 0
dp[1] = nums[0]
// dp[i]:考虑前i间房子的价值 注意下标nums:[0...len-1] dp:[1...len]
for(let i = 2; i <= len; i++) //+nums[i-1]即偷第i间
dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i-1] ,)
return dp[len]
};
//时间复杂度:O(n)、空间复杂度O(1)
var rob = function(nums) {
if(!nums.length) { return 0; }
let dp0 = 0
let dp1 = nums[0]
for(let i = 2; i <= nums.length; i++) {
const dp2 = Math.max(dp1, dp0 + nums[i-1] )
dp0 = dp1
dp1 = dp2
}
return dp1
};
4. 01背包问题
思路:memo[i][j] 表示容量为 j 时,0-i 号物品能获得最大价值;对于第 i 号物品,当前容量若放不下则结果为之前的价值,若放得下则考虑放与不放的价值谁大(放该物品时的价值=不放时容量的价值+该物品的价值)
练习:完全背包问题
// 时间空间复杂度:n*capacity
const knapsack01 = (weight, value, capacity) => {
const n = weight.length
if (n == 0) return 0
const memo = new Array(n).fill(Array(capacity + 1).fill(-1))
// 初始化第一行(放第一个物品)
for (let k = 0; k <= capacity; k++)
memo[0][k] = (k >= weight[0] ? weight[0] : 0)
for (let i = 1; i < n; i++)
for (let j = 0; j <= capacity; j++) {
if (j < weight[i])
memo[i][j] = memo[i - 1][j]
else
memo[i][j] = Math.max(memo[i-1][j], value[i] + memo[i - 1][j - weight[i]])
}
return memo[n - 1][capacity]
}
console.log(knapsack01([1, 2, 3], [6, 10, 12], 5)) //22
// 实际上下一行的数据 只依靠上一行,优化后 空间复杂度为O(n)
const knapsack01 = (weight, value, capacity) => {
const n = weight.length
if (n == 0) return 0
const memo = new Array(2).fill(Array(capacity + 1).fill(-1))
// 初始化第一行(放第一个物品)
for (let k = 0; k <= capacity; k++)
memo[0][k] = (k >= weight[0] ? weight[0] : 0)
for (let i = 1; i < n; i++)
for (let j = 0; j <= capacity; j++) {
if (j < weight[i])
memo[i % 2][j] = memo[(i - 1) % 2][j]
else
memo[i % 2][j] = Math.max(memo[(i - 1) % 2][j], value[i] + memo[(i - 1) % 2][j - weight[i]])
}
return memo[(n - 1) % 2][capacity]
}
console.log(knapsack01([1, 2, 3], [6, 10, 12], 5)) //22
//继续优化
const knapsack01 = (weight, value, capacity) => {
const n = weight.length
if (n == 0) return 0
const memo = new Array(capacity + 1).fill(-1)
for (let k = 0; k <= capacity; k++)
memo[k] = (k >= weight[0] ? weight[0] : 0)
for (let i = 1; i < n; i++)
for (let j = capacity; j >= weight[i]; j--) {
memo[j] = Math.max(memo[j - 1], value[i] + memo[j - weight[i]])
}
return memo[capacity]
}
5.分割等和子集
思路:类似背包问题,若能分割,则说明数组里的部分和等于sum的一半,即在数组里找到n个数填满容量为sum/2的背包。对于第 i 个数,放或不放只要能填满背包即可(若放,则需要能放得下)
练习:377、474、139、494
var canPartition = function(nums) {
let sum = nums.reduce((pre, cur) => pre + cur);
// 和为奇数时,不可能划分成两个和相等的集合
if (sum % 2 != 0) return false
let n = nums.length,
c = sum / 2
let dp = new Array(n + 1)
.fill(false)
.map(() => new Array(c + 1).fill(false));
// dp[i][j] 表示前i个物品 能不能放满容量为j的背包
// base case 背包空间c=0时候,就相当于装满了
for (let i = 0; i <= n; i++)
dp[i][0] = true;
for (let i = 1; i <= n; i++) {
for (let j = 1; j <= c; j++) {
if (j < nums[i - 1]) {
// 背包容量不足,不能装入第 i 个物品
dp[i][j] = dp[i - 1][j];
} else {
// 不装入或装入背包
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]];
}
}
}
return dp[n][c];
};
// 优化
var canPartition = function(nums) {
let sum = 0
for (let n of nums)
sum += n
if (sum % 2) return false
let n = nums.length,
c = sum / 2
const memo = Array(c + 1).fill(false)
for(let i =0;i<=c;i++) // 看第一个数能不能放入被入背包
memo[i] = (nums[0]==i)
for (let i = 1; i <= n; i++) // 看后面物品中 能不能放满容量为c的背包
for (let j = c; j >= nums[i]; j--)
// 第i个物品不放 或放
memo[j] = memo[j] || memo[j - nums[i]] // memo[j] |= memo[j-nums[i]]
return memo[c]
};
6. 最长递增子序列
思路:dp[i] = max(dp[i], dp[j] + 1) for j in [0, i) ,num[ j ]<num[ i ] ,dp[ i ] 表示[ 0...i ]内选择nums[ i ] 可以获得的最长上升子序列的长度。
练习: 376
var lengthOfLIS = function(nums) {
const dp = new Array(nums.length).fill(1);
for (let i = 0; i < nums.length; i++) {
for (let j = 0; j < i; j++)
if (nums[i] > nums[j])
dp[i] = Math.max(dp[i], dp[j] + 1);
}
return Math.max(...dp);
};
7.最长公共子序列
思路:
a. 分治 LCS[i] = LCS(最后一个字母相同)+ LCS(最后一个字母不相同)
b. 状态定义 f[ i ][ j ] 表示第一个字符串索引 0-i 构成的子串与第二个字符串索引 0-j 子串的最长公共序列
c. DP方程:
if text1[i-1] == text2[i-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
时间、空间复杂度:O(mn)
var longestCommonSubsequence = function(text1, text2) {
let dp = Array.from(Array(text1.length+1),
() => Array(text2.length+1).fill(0));
for(let i = 1; i <= text1.length; i++) {
for(let j = 1; j <= text2.length; 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[text1.length][text2.length];
};
8. 零钱兑换
思路: dp[j] = min(dp[j - coins[i]] + 1, dp[j]) , dp[j]:凑足总额为 j 所需最少硬币个数为dp[j],对于第 i 个硬币拿与不拿,看最后总数谁少。
var coinChange = function(coins, amount) {
let dp = new Array(amount + 1).fill(Infinity);
dp[0] = 0;
for (let i = 1; i <= amount; i++)
for (let coin of coins)
if (i >= coin )
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
return dp[amount] === Infinity ? -1 : dp[amount];
}
9.不同路径
思路:要到达终点[m-1][n-1],有俩种方式从左边来即[i-1][j]或从上边来即[i][j-1]。由此可得动态转移方程:dp[i][j] = dp[i-1][j] + dp[i][j-1] (dp[i][j] 表示从起点到任意位置的路径数,d[i][0]为1理由是从[0, 0]的位置到[i, 0]的路径只有一条,同理d[0][j])
时间、空间复杂度:O(m * n)
a. 分治(子问题) path = path(top) + path(left)
b. 状态定义 f[i, j]
表示第i行第j列的不同路径数
c. DP方程 dp[i][j] = dp[i-1][j] + dp[i][j-1]
// dp[i][j]为每行每列任意点的不同路径数
var uniquePaths = function(m, n) {
const dp=new Array(m).fill(1).map(()=>Array(n).fill(1))
for(i=1;i<m;i++)
for(j=1;j<n;j++)
dp[i][j]=dp[i-1][j]+dp[i][j-1]
return dp[m-1][n-1]
};
//优化空间(O(n)):只需要保存每一行任意点的路径数即可,DP方程可以更新为 dp[j] = dp[j] + dp[j-1]`
var uniquePaths = function(m, n) {
const dp=new Array(n).fill(1)
for(i=1;i<m;i++)
for(j=1;j<n;j++)
dp[j] = dp[j] + dp[j-1]
return dp[n-1]
};
附加、22年各厂面试题整理
tx 1: 移掉 K 位数字
思路:贪心,遍历字符串维护一个栈,如果当前数字小于前一个,则移除前一个元素同时k--,并加入当前元素;若当前元素为0且栈为空则不入栈。
时间、空间复杂度:O(n)
var removeKdigits = function(num, k) {
const stk = []
for (const digit of num) {
while (k && stk.length && stk[stk.length-1] > digit) {
stk.pop()
k --
}
if (digit == '0' && stk.length == 0) //如果当前元素为0,且栈为空,则跳过
continue
stk.push(digit)
}
while (k-- > 0) stk.pop()
return stk.length == 0 ? "0" : stk.join('')
}
2. 复杂数组去重
function removeDuplicate(arr) {
const newArr = []
const obj = {}
arr.forEach(item => {
if (!obj[item]) {
newArr.push(item)
obj[item] = true
}
})
return newArr
}
const arr = ['12', 'webank', '12', [1, 23], { a: 1 }, 23, { a: 1 }]
const result = removeDuplicate(arr)
console.log(result)
// 0: "12"
// 1: "webank"
// 2: (2)[1, 23]
// 3: { a: 1 }
// 4: 23
3. 一维数组转为树状结构
const arrayToTree = (arr) => {
if (!Array.isArray(arr) || arr.length < 1) return null;
const [root] = arr.filter(item => item.id === 0);
const addChildren = (node, dataList) => {
const children = dataList
.filter(item => item.id === node.id)
.map(item => addChildren(item, dataList));
return { ...node, children };
};
return addChildren(root, arr);
};
const list = [{ id: 1, parentId: null, name: 'wuhan' }, { id: 2, parentId: 2, name: 'changsha' }]
console.log(arrayToTree(list))
4. 求俩个日期中间的有效日期
function RealDate(start, end) {
const dayTimes = 24 * 60 * 60 * 1000; // 换算成毫秒级别
const range = end.getTime() - start.getTime();
let total = 0;
res = [];
while (total <= range && range > 0) {
res.push(new Date(start.getTime() + total).toLocaleDateString().replace(/\//g, '-'))
total += dayTimes
}
return res;
}
var start = "2020-09-29"
var end = "2020-10-04"
//console.log(new Date(start).getTime())
var arr1 = RealDate(new Date(start), new Date(end))
console.log(arr1)
// 0: "2020-9-29"
// 1: "2020-9-30"
// 2: "2020-10-1"
// 3: "2020-10-2"
// 4: "2020-10-3"
// 5: "2020-10-4"
// 借用map 哈希映射
const intersect = (nums1, nums2) => {
const map = {};
const res = [];
if (nums1.length < nums2.length) {
[nums1, nums2] = [nums2, nums1]
}
for (const num1 of nums1) {//nums1中各个元素的频次
if (map[num1]) {
map[num1]++;
} else {
map[num1] = 1;
}
}
for (const num2 of nums2) { //遍历nums2
const val = map[num2];
if (val > 0) { //在nums1中
res.push(num2); //加入res数组
map[num2]--; //匹配掉一个,就减一个
}
}
return res;
};