[前端算法]动态规划

基于动态规划的思想来做题,我们首先要想到的思维工具就是“倒着分析问题”。“倒着分析问题”分两步走:

  1. 定位到问题的终点
  2. 站在终点这个视角,思考后退的可能性

爬楼梯

递归+记忆化搜索

自顶向下

var climbStairs = function(n) {
   let map=[]
   function dfs(n)
   {
	  if(n<=2) return n
	  if(map[n] == undefined) map[n]=dfs(n-1)+dfs(n-2)
	   return map[n]
	}
   return dfs(n)
};

记忆化搜索可以理解为优化过后的递归。递归往往可以基于树形思维模型来做,
我们基于树形思维模型来解题时,实际上是站在了一个比较大的未知数量级(也就是最终的那个n),来不断进行拆分,最终拆回较小的已知数量级(f(1)f(2))。这个过程是一个明显的自顶向下的过程。
在这里插入图片描述

动态规划则恰恰相反,是一个自底向上的过程。它要求我们站在已知的角度,通过定位已知未知之间的关系,一步一步向前推导,进而求解出未知的值。
在这道题中,已知 f(1)f(2) 的值,要求解未知的 f(n),我们唯一的抓手就是这个等价关系:

动态规划

自底向上

var climbStairs = function(n) {
    let 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]
};

动态规划常见的题型

最优子结构,它指的是问题的最优解包含着子问题的最优解——不管前面的决策如何,此后的状态必须是基于当前状态(由上次决策产生)的最优决策。就这道题来说,f(n)f(n-1)f(n-2)之间的关系印证了这一点(这玩意儿叫状态转移方程,大家记一下)。

重叠子问题,它指的是在递归的过程中,出现了反复计算的情况。就这道题来说,图上标红的一系列重复计算的结点印证了这一点。
因此,这道题适合用动态规划来做。

对于动态规划,建议大家优先选择这样的分析路径:

  1. 递归思想明确树形思维模型:找到问题终点,思考倒退的姿势,往往可以帮助你更快速地明确状态间的关系
  2. 结合记忆化搜索,明确状态转移方程
  3. 递归代码转化为迭代表达(这一步不一定是必要的,1、2本身为思维路径,而并非代码实现。若你成长为熟手,2中分析出来的状态转移方程可以直接往循环里塞,根本不需要转换)。

最值问题

站在 amount 这个组合结果上的“后退”—— 我们可以假装此时手里已经有了 36 美分,只是不清楚硬币的个数,把“如何凑到36”的问题转化为“如何从36减到0”的问题

f(36) = Math.min(f(36-c1)+1,f(36-c2)+1,f(36-c3)+1......f(36-cn)+1)

function coinChange(coins, amount) {
 //dp[i] 代表总额为i的硬币最小个数
  let dp = [0];

  for (let i = 1; i <= amount; i++) {

    dp[i] = Infinity;

    for (let j = 0; j < coins.length; j++) {

      if (i >= coins[j]) {

        dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);

      }

    }

  }

  if (dp[amount] === Infinity) {

    return -1;

  }

  return dp[amount];

}

01背包问题

“倒推”法明确状态间关系

现在,假设背包已满,容量已经达到了 c。站在c这个容量终点往后退,考虑从中取出一样物品,那么可能被取出的物品就有 i 种可能性。我们现在尝试表达“取出一件”这个动作对应的变化,我用 f(i, c) 来表示前 i 件物品恰好装入容量为 c 的背包中所能获得的最大价值。现在假设我试图取出的物品是 i,那么只有两种可能:

  1. i 件物品在背包里
  2. i 件物品不在背包里
dp[i][v] = Math.max(dp[i-1][v], dp[i-1][v-w[i]] + c[i])

从变量入手:变量是 iv,但本质上来说 v 其实也是随着 i 的变化而变化的,因此我们可以在外层遍历i、在内层遍历 v。明白了这一点,我们就可以编码如下:

for(let i=1;i<=n;i++) {
    for(let v=w[i]; v<=c;v++) {
      dp[i][v] = Math.max(dp[i-1][v], dp[i-1][v-w[i]]+value[i])
    }
}

function knapsack(n, c, w, value) {

  //dp[i][j]表示前i个物品,背包容量为j时的最大价值

  //dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+value[i])

  let dp = [];

  //初始化

  for (let i = 0; i <= n; i++) {

    dp[i] = [];

    for (let j = 0; j <= c; j++) {

      dp[i][j] = 0;

    }

  }

  for (let i = 1; i <= n; i++) {

    for (let j = 1; j <= c; j++) {

      if (j >= w[i]) {

        dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w[i]] + value[i]);

      } else {

        dp[i][j] = dp[i - 1][j];

      }

    }

  }

  return dp[n][c];

}

最长上升子序列问题

300. 最长递增子序列 - 力扣(LeetCode)

子序列,在原有序列的基础上,删除0个或多个数,其他数的顺序保持不变得到的结果
f(i)来表示前i个元素中最长上升子序列的长度。若想基于 f(i) 求解出 f(i+1),我们需要关注到的是第 i+1 个元素和前 i 个元素范围内的最长上升子序列的关系,它们之间的关系有两种可能:

  1. 若第i+1个元素比前i个元素中某一个元素要大,此时我们就可以在这个元素所在的上升子序列的末尾追加第i+1个元素(延长原有的子序列),得到一个新的上升子序列。
  2. 若第i+1个元素并不比前i个元素中所涵盖的最长上升子序列中的某一个元素大,则维持原状,子序列不延长
function lengthOfLIS(nums) {

  //dp[i]表示以nums[i]结尾的最长上升子序列的长度

  //初始化,每个索引位都存在一个以自己为结尾的子序列,长度为1

  let dp = new Array(nums.length).fill(1);

  let max = 1;

  for (let i = 1; 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);

      }

    }

    if (max < dp[i]) {

      max = dp[i];

    }

  }

  return max;

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值