1.动态规划
动态规划有时会被认为是递归相反的技术。递归是从顶部将问题分解,通过解决分解后的小问题来解决整个问题;动态规划时从底部解决问题,将所有小问题解决,合并为整体解决方案。
1.1斐波那契数列(LeetCode):
//经典的斐波那契数列:0,1,1,2,3,5,8,13
//简单的递归实现
function recurFib(n){
if(n<2){ return n }
return recurFib(n-1)+recurFib(n-2)
}
//动态规划的实现
function dynFib(n){
let arr = [];//记录小问题的解
//记录初始值
arr.push(0);arr.push(1);
for(let i=2;i<=n;i++){
arr.push(arr[i-1]+arr[i-2])
}
return arr[n]
}
//可以不使用数组,直接使用两个变量记录前两个的值
1.2最长公共子串(LeetCode):
在动态规划算法中,状态转移是关键,从上一个状态到下一个状态之间可能存在一些变化,基于这些变化得到最终决策结果。当问题可能很多,但是最终求的是最优解,就可以试着用动态规划。
//最长公共子序列
function lCS(word1,word2){
//建立二维数组,作为状态转移方程
let len1 = word1.length,len2 = word2.length;
let dp = [...new Array(len1+1)].map(() => new Array(len2+1).fill(0));
//let dp = Array.from(new Array(len1 + 1), () => new Array(len2 + 1).fill(0));
//分析状态转移方程
for(let i=1;i<=len1;i++){
for(let j=1;j<=len2;j++){
if(word1[i-1] == word2[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[len1][len2]
}
//如果输出子串,改变下转移方程的值即可
let dp = [...new Array(len1+1)].map(() => new Array(len2+1).fill(''));
...
if(word1[i-1] == word2[j-1]){
dp[i][j] = dp[i-1][j-1] + word1[i-1];
}else{
dp[i][j] = dp[i-1][j].length > dp[i][j-1].length?dp[i-1][j]:dp[i][j-1];
}
1.3 背包问题
给定n个重量为w1,w2,...wn,价值为v1,v2,...vn的物品,以及容量为C的背包,使在满足背包容量的前提下,包内的总价值最大。
递归方法解决
//c:容量,n:数量,value:价值列表,size:大小列表,size为有序数组,从小到大
function knapSack(c,n,value,size){
if(c == 0 || n == 0) return 0
if(size[n-1] > c){//去除放不进去的
return knapSack(c,n-1,value,size)
}else{
//当前放进去和不放进去,总价值取最大
return Math.max(value[n-1]+knapSack(c-size[n-1],n-1,value,size),knapSack(c,n-1,value,size))
}
}
//会涉及到反复取同一个子问题的解
动态规划:
/*找到状态转移时变化的量,一个是空间c,一个是数量n
状态转移方程:F(i,c) = max(F(i-1,c),F(i-1,c-w[i])+v[i])
*/
function knapSack(c,n,value,size){
let dp = [...new Array(n+1)].map(() => new Array(c+1).fill(0));
for(let i=1;i<=n;i++){
for(let j=1;j<=c;j++){
dp[i][j] = dp[i-1][j];
if(size[i-1]<=w){
dp[i][j] = Math.max(value[i-1]+dp[i-1][j-size[i-1]],dp[i-1][j])
}
}
}
return dp[n][c]
}
//简化为一维数组,因为当前行的值只与前一行的值有关,为了防止覆盖前面的值需要从后往前填充数组
let dp = new Array(c+1).fill(0);
for(i=1;i<=n;i++){
for(let j=c;j>=size[i-1];j--){
dp[j] = Math.max(value[i-1]+dp[j-size[i-1]],dp[j-1])
}
}
return dp[c]
2.贪心算法
贪心算法总是会选择当下最优解,通过一系列最优选择带来整体的最优选择。
2.1 找零问题
假设货币面额有1,2,5,10,20,50,100,每种数量都无限多,现在给出金额n(1<=n<=100000),求出最少的货币数量。
//首先尝试最大面额找零,之后尝试次大面额找零,直到完全找零
function makeChange(n){
let coins = [];
if(n%100 < n){//n比100大
coins.push(parseInt(n/100));
n %= 100
}
...
if(n%1 < n){
coins.push(n/1)
}
return coins
}
2.2 背包问题
1.3的背包问题是0-1问题,背包物品是离散的,只能整个放入或者不放入。如果背包物品是连续的,那就可以使用贪心算法,先用价值高的物品填充,接着是次高的...贪心算法可以解决一部分背包问题。
//weights:数组顺序按照价值比率从高到底
function ksack(values,weights,c){
let load = 0;
let i=0,w=0;
while(load < c && i<values.length){
if(weights[i] <= (c-load)){
w += values[i];
load += weights[i]
}else{
let r = (c-load)/weights[i];
w += r*values[i];
load += weights[i]
}
i++
}
return w
}