JavaScript中的高级算法-动态规划&贪心算法

1,680 阅读4分钟

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
}