用javascript分类刷leetcode3.动态规划(图文视频讲解)

什么是动态规划

动态规划,英文:Dynamic Programming,简称DP,将问题分解为互相重叠的子问题,通过反复求解子问题来解决原问题就是动态规划,如果某一问题有很多重叠子问题,使用动态规划来解是比较有效的。

求解动态规划的核心问题是穷举,但是这类问题穷举有点特别,因为这类问题存在「重叠子问题」,如果暴力穷举的话效率会极其低下。动态规划问题一定会具备「最优子结构」,才能通过子问题的最值得到原问题的最值。另外,虽然动态规划的核心思想就是穷举求最值,但是问题可以千变万化,穷举所有可行解其实并不是一件容易的事,只有列出**正确的「状态转移方程」**才能正确地穷举。重叠子问题、最优子结构、状态转移方程就是动态规划三要素

动态规划和其他算法的区别
  1. 动态规划和分治的区别:动态规划和分治都有最优子结构 ,但是分治的子问题不重叠
  2. 动态规划和贪心的区别:动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优解,所以它永远是局部最优,但是全局的解不一定是最优的。
  3. 动态规划和递归的区别:递归和回溯可能存在非常多的重复计算,动态规划可以用递归加记忆化的方式减少不必要的重复计算
动态规划的解题方法
  • 递归+记忆化(自顶向下)
  • 动态规划(自底向上)

ds_135

解动态规划题目的步骤
  1. 根据重叠子问题定义状态
  2. 寻找最优子结构推导状态转移方程
  3. 确定dp初始状态
  4. 确定输出值
斐波那契的动态规划的解题思路

ds_3

动画过大,点击查看

暴力递归
//暴力递归复杂度O(2^n)
var fib = function (N) {
   
    if (N == 0) return 0;
    if (N == 1) return 1;
    return fib(N - 1) + fib(N - 2);
};
递归 + 记忆化
var fib = function (n) {
   
    const memo = {
   }; // 对已算出的结果进行缓存

    const helper = (x) => {
   
        if (memo[x]) return memo[x];
        if (x == 0) return 0;
        if (x == 1) return 1;
        memo[x] = helper(x - 1) + helper(x - 2);
        return memo[x];
    };

    return helper(n);
};
动态规划
const fib = (n) => {
   
    if (n <= 1) return n;
    const dp = [0, 1];
    for (let i = 2; i <= n; i++) {
   
        //自底向上计算每个状态
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n];
};
滚动数组优化
const fib = (n) => {
   
    if (n <= 1) return n;
    //滚动数组 dp[i]只和dp[i-1]、dp[i-2]相关,只维护长度为2的滚动数组,不断替换数组元素
    const dp = [0, 1];
    let sum = null;
    for (let i = 2; i <= n; i++) {
   
        sum = dp[0] + dp[1];
        dp[0] = dp[1];
        dp[1] = sum;
    }
    return sum;
};
动态规划 + 降维,(降维能减少空间复杂度,但不利于程序的扩展)
var fib = function (N) {
   
    if (N <= 1) {
   
        return N;
    }
    let prev2 = 0;
    let prev1 = 1;
    let result = 0;
    for (let i = 2; i <= N; i++) {
   
        result = prev1 + prev2; //直接用两个变量就行
        prev2 = prev1;
        prev1 = result;
    }
    return result;
};
322. 零钱兑换 (medium)

视频讲解:传送门

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。

你可以认为每种硬币的数量是无限的。

示例 1:

输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:

输入:coins = [2], amount = 3
输出:-1
示例 3:

输入:coins = [1], amount = 0
输出:0

提示:

1 <= coins.length <= 12
1 <= coins[i] <= 231 - 1
0 <= amount <= 104

ds_75

不能用贪心做,反例,coins=[1, 3, 5, 6, 7]amount=30,用贪心先用最大的面额7,在用2个1,4 * 7 + 2 * 1 = 30,但是我们用5个6,5 * 6 = 30 就能用最少的硬币兑换完成

方法1.动态规划

  • 思路:dp[i]表示兑换面额i所需要的最少硬币,因为硬币无限,所以可以自底向上计算dp[i],对于dp[0~i]的每个状态,循环coins数组,寻找可以兑换的组合,用i面额减去当前硬币价值,dp[i-coin]在加上一个硬币数就是dp[i],最后取最小值就是答案,状态转移方程就是dp[i] = Math.min(dp[i], dp[i - coin] + 1);
  • 复杂度分析:时间复杂度是O(sn),s是兑换金额,n是硬币数组长度,一共需要计算s个状态,每个状态需要遍历n个面额来转移状态。空间复杂度是O(s),也就是dp数组的长度

Js:

var coinChange = function (coins, amount) {
   
    let dp = new Array(amount + 1).fill(Infinity);//初始化dp数组
    dp[0] = 0;//面额0只需要0个硬币兑换

    for (let i = 1; i <= amount; i++) {
   //循环面额
        for (let coin of coins) {
   //循环硬币数组
            if (i - coin >= 0) {
   //当面额大于硬币价值时
                //dp[i - coin]: 当前面额i减当前硬币价值所需要的最少硬币
                //dp[i] 可由 dp[i - coin] + 1 转换而来
                dp[i] = Math.min(dp[i], dp[i - coin] + 1);
            }
        }
    }

    return dp[amount] === Infinity 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值