要想写出正确的状态转移方程, 一定要思考以下几点: 1.这个问题的 base case(最简单情况)是什么? 2.这个问题有什么『状态』
但凡遇到需要递归解决的问题,最好都画出递归树。
递归算法的的时间复杂度怎么计算?就是用子问题个数乘以解决一个子问题需要的时间。
斐波那契数列 二叉树节点总数为指数级别的,所以求子问题个数的时间复杂度为 O(2 指数 n)
自顶向下
let fiber = function (num) {
let cache = [];
let fib = function (cache, num) {
if (num <= 1) return 1;
if (cache[num]) {
return cache[num];
} else {
cache[num] = fib(cache, num - 1) + fib(cache, num - 2);
}
return cache[num];
};
return fib(cache, num);
};
console.log(fiber(200));
自底向上 DP table 来消除重叠子问题
let fiber = function (n) {
if (n === 0) return 0;
if (n === 1 || n === 2) return 1;
let dp = [];
dp[1] = dp[2] = 1;
for (let i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
};
console.log(fiber(8));
凑零钱
如何列出正确的状态转移方程
确定 base case
确定『状态』, 也就是原问题和子问题的变量
确定『选择』, 也就是导致『状态』产生变化的行为
明确 dp 函数、数组的定义
dp(n)的定义:输入一个目标金额 n, 返回凑出目标金额 n 的最少硬币数量
let coinChange = function (coins, amount) {
let dp = function (num) {
// 确定base case
if (num == 0) return 0;
if (num < 1) return -1;
let res = num;
for (coin of coins) {
// 确定状态
let subProblem = dp(num - coin);
if (subProblem === -1) continue;
// 确定 选择
res = Math.min(res, 1 + subProblem);
}
return res;
};
return dp(amount);
};
console.log(coinChange([1, 3, 5], 11));
// 带备忘录的
let coinChange = function (coins, amount) {
let memo = [];
let dp = function (num) {
// 查备忘录 避免重复计算
if (memo[n]) return memo[n];
// 确定base case
if (num == 0) return 0;
if (num < 1) return -1;
let res = num;
for (coin of coins) {
// 确定状态
let subProblem = dp(num - coin);
if (subProblem === -1) continue;
// 确定 选择
res = Math.min(res, 1 + subProblem);
}
// 记入备忘录中
memo[num] = res;
return memo[num];
};
return dp(amount);
};
console.log(coinChange([1, 3, 5], 11));
DP table dp 数组的定义: 当目标金额为 i 时,至少需要 dp[i] 枚硬币凑出;
let coinChange = function (coins, amount) {
let dp = function (num) {
// 确定base case
dp[0] == 0;
let dp = Array.from(num);
for (let i = 0; i < dp.length; i++) {
for (coin of coins) {
// 确定状态
if (i - coin < 0) continue;
dp[i] = Math.min(dp(i), dp[num - coin]);
}
}
};
return dp[amount] === amount + 1 ? -1 : dp[amount];
};
console.log(coinChange([1, 3, 5], 11));
回溯算法解题套路框架
解决一个回溯问题, 实际上就是决策树的遍历过程
路径: 也就是已经做出的选择
选择列表: 也就是你当前可以做的选择
结束条件: 也就是到达决策树底层,无法在做选择的条件
1.3 回溯的算法框架 result = []; def backtrack(路径, 选择列表): if 满足结束条件: result.add(路径) return for 选择 in 选择列表: 做选择 backtrack(路径, 选择列表) 撤销选择
一共四种情况: (a,b):区间范围内,不包含 a 和 b [a,b]:区间范围内,包含 a,也包含 b (a,b]:区间范围内,不包含 a,包含 b [a,b):区间范围内,包含 a,不包含 b
2.两数之和
只要数组有序,就应该想到双指针技巧
function twoSum(nums, target) {
let left = 0;
let right = nums.length - 1;
while (left < right) {
let sum = nums[left] + nums[right];
if (sum == target) {
//题目要求索引从1开始的
return [left + 1, right + 1];
} else if (sum < target) {
left++; //让sum大一点
} else {
right--; // 让sum小一点
}
}
return [-1, -1];
}
3.反转数组
function reverseArray(nums) {
let left = 0;
let right = nums.length - 1;
while (left < right) {
let temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
left++;
right--;
}
return nums;
}
4.滑动窗口算法
1.6 二分搜索框架
let binarySearch(){
let left = 0; right = ...;
while(...){
let mid = left + (right - left) / 2;
if(nums[mid] == target){
} else if(nums[mid] < target)
left = ...
else if(nums[mid] > target){
right = ...
}
}
return ...
}