>>动态规划 (用空间去换时间)
状态转移 : 穷举每个状态,把对所有状态的组合穷举出来,然后对每个状态的组合求最值
(能画递归树的前提是 满足最优子结构) :什么是最优子结构? 比如 ,我要统计全校的最高分,我只要知道每个班的最高分再做比较即可 ; 但是如果我要统计全校的最大分差,我只知道每个班的最高分差是无法判断全校的分差
1.斐波那契数列(不是正宗的dp)
递归算法的时间复杂度 = 递归函数调用的次数 x * 递归函数本身的次数
此递归树的时间复杂度 为 2 的 k 次方. f(18 ) , f (17 ) 存在两次冗余计算
以下为动态规划求斐波那契数列
//动态规划求斐波那契数列
function fib(n){
//启用备忘录
let arr = new Array(n + 1).fill(0);
return helper(arr,n);
}
function helper(arr,n){
if(n == 1 || n== 0){
return n
}
if(arr[n] != 0){
return arr[n];
}
else{
arr[n] = helper(arr,n - 1) + helper(arr,n - 2);
return arr[n];
}
}
console.log(fib(5))
//时间复杂度为 O(n)
//这是自顶向下的递归函数
接了下来我们来看自底向上的函数来实现斐波那契
function fib(n){
if(n == 0){
return 0
}
let dp = new Array( n + 1).fill(0);
dp[0] = 0;
dp[1] = 1;
let m;
//这里的for循环相当于上面算法的递归,不过这是自底向上的函数
for( m= 2 ;m <= n ; m++){
//状态转移
dp[m] = dp[m - 1] + dp[m - 2];
}
return dp[n]
}
console.log(fib(5))
//优化!!!!!!!
function fib(n){
if(n == 0){
return 0
}
let dp = new Array( n + 1).fill(0);
dp[0] = 0;
dp[1] = 1;
let m;
//这里的for循环相当于上面算法的递归,不过这是自底向下的函数
// for( m= 2 ;m <= n; m++){
// //其实我们这里发现dp数组有两个长度就够了 !!!!!
// dp[m] = dp[m - 1] + dp[m - 2];
// }
let pre = 0;
let cur = 1;
// 优化如下
for( m= 2 ;m <= n; m++){
//其实我们这里发现dp数组有两个长度就够了
dp[m] = pre + cur;
pre = cur;
cur = dp[m];
}
return dp[n]
}
console.log(fib(5))
2.动态规划特点:
- 重叠子问题
- 状态转移方程
- 最优子结构
题型:求最值
核心:穷举
解题思路:
- 明确状态
- 明确选择
- 明确dp函数/数组的定义
- 明确 base case
好,接下来我们来看下一道例题:
3.数字分组求偶数和
问题描述:
小M面对一组从 1 到 9 的数字,这些数字被分成多个小组,并从每个小组中选择一个数字组成一个新的数。目标是使得这个新数的各位数字之和为偶数。任务是计算出有多少种不同的分组和选择方法可以达到这一目标。
numbers: 一个由多个整数字符串组成的列表,每个字符串可以视为一个数字组。小M需要从每个数字组中选择一个数字。
例如对于[123, 456, 789],14个符合条件的数为:147 149 158 167 169 248 257 259 268 347 349 358 367 369。
测试样例
样例1:
输入:
numbers = [123, 456, 789]输出:14
样例2:
输入:
numbers = [123456789]输出:4
样例3:
输入:
numbers = [14329, 7568]输出:10
OK,让我来解释一下为什么要用动态规划,为什么会想到动态规划
1.问题的性质:
- 组合问题:这是一个组合问题,涉及到从多个选项中选择并计算结果。
- 最优子结构:问题的解可以分为子问题的解。 例如: 选择前i 个数字组中的数字,使得和为偶数,可以分解为选择前 i - 1个数字组中的数字 ,并考虑第i 个数字组中的选择
2.状态定义:
-
状态表示: 定义状态dpi 表示前i 个数组 中选择数字,使得和为偶数的组合数;
dpi表示前i 个数组中选择数字,使得和为奇数的组合数
-
状态转移 :根据当前数字组 的选择,更新状态,
-
如果前i - 1个数字组中的和为偶数,选择当前数字组中的偶数会使得和保持为偶数,选择奇数使得和变为奇数。反之亦然
3.状态转移方程
-
偶数状态转移
dp[i][0] = dp[i-1][0] * new_even + dp[i-1][1] * new_odd- 解释:前
i-1个数字组的和为偶数时,选择当前数字组中的偶数会使和保持为偶数;前i-1个数字组的和为奇数时,选择当前数字组中的奇数会使和变为偶数。
-
奇数状态转移
dp[i][1] = dp[i-1][0] * new_odd + dp[i-1][1] * new_even- 解释:前
i-1个数字组的和为偶数时,选择当前数字组中的奇数会使和变为奇数;前i-1个数字组的和为奇数时,选择当前数字组中的偶数会使和保持为奇数。
4.初始状态
- dp0 = 1,表示没有选择任何数字时,和为0的组合数为1。
5.最终结果
- dpn
,表示前n` 个数字组中选择数字,使得和为偶数的组合数。
function solution(numbers) {
const n = numbers.length;
// 初始化奇数和偶数计数数组
const oddCounts = numbers.map(num => num.split('').filter(digit => digit % 2 !== 0).length);
const evenCounts = numbers.map(num => num.split('').filter(digit => digit % 2 === 0).length);
// 初始化 dp 数组
const dp = new Array(n + 1).fill(0).map(() => [0, 0]);
dp[0][0] = 1; // 初始状态,没有选择任何数字时,和为0的组合数为1
for (let i = 1; i <= n; i++) {
const [prevEven, prevOdd] = dp[i - 1];
const [newEven, newOdd] = [evenCounts[i - 1], oddCounts[i - 1]];
// 选择偶数
dp[i][0] = prevEven * newEven + prevOdd * newOdd;
dp[i][1] = prevEven * newOdd + prevOdd * newEven;
}
return dp[n][0];
}
function main() {
console.log(solution(["123", "456", "789"])); // 输出 14
console.log(solution(["123456789"])); // 输出 4
console.log(solution(["14329", "7568"])); // 输出 10
}
main();
4.DNA序列编辑问题
题目如下:
小R正在研究DNA序列,他需要一个函数来计算将一个受损DNA序列(dna1)转换成一个未受损序列(dna2)所需的最少编辑步骤。编辑步骤包括:增加一个碱基、删除一个碱基或替换一个碱基。
测试样例
样例1:
输入:
dna1 = "AGT",dna2 = "AGCT"输出:1
样例2:
输入:
dna1 = "AACCGGTT",dna2 = "AACCTTGG"输出:4
样例3:
输入:
dna1 = "ACGT",dna2 = "TGC"输出:3
样例4:
输入:
dna1 = "A",dna2 = "T"输出:1
样例5:
输入:
dna1 = "GGGG",dna2 = "TTTT"输出:4
现在我来假定一个测试用例: dna1 = "AGCT",dna2 = "ACGT"
OK,我们来❀一个图:
OK,我们来看代码:
function solution(dna1, dna2) {
const m = dna1.length;
const n = dna2.length;
// 创建一个 (m+1) x (n+1) 的二维数组 dp
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
// 初始化 dp 数组
for (let i = 0; i <= m; i++) {
dp[i][0] = i;
}
for (let j = 0; j <= n; j++) {
dp[0][j] = j;
}
// 填充 dp 数组
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (dna1[i - 1] === dna2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = Math.min(
dp[i - 1][j] + 1, // 删除
dp[i][j - 1] + 1, // 插入
dp[i - 1][j - 1] + 1 // 替换
);
}
}
}
// 返回最终结果
return dp[m][n];
}
function main() {
// You can add more test cases here
// console.log(solution("AGCTTAGC", "AGCTAGCT") === 2);
console.log(solution("AGCCGAGC", "GCTAGCT") === 4);
}
main();
额外学到的
使用 &运算检查 state 的第 i 位置 是否为 1
比如 现在 的 state 为 0b101,我们想要检查 state 的第一位是否为 1,我们可以试图这样做
state & (1 << 0)
规律即为 想要证明state 的第i 位(从右往左) 是否为 1 ,做此运算
state & (1 << (i - 1))
使用 | 运算 使 state 的第 i 位变为 1
依旧是state 0b101 ,我们想要让第二位变成 1 ,做运算 |
state | (1 << 1) //测试state 为111,十进制7
规律即为 想要证明state 的第i 位(从右往左) 是否为 1 ,做此运算
state & (1 << (i - 1))