【近日力扣】零钱兑换+零钱兑换||+礼物的最大价值+三角形最短路径和+打家劫舍+打家劫舍||

动态规划专题(一)

动态规划题目比较千变万化,我也没有那么多时间去研究所有可能,挑着一些典型的题型刷刷。

解题思路的重点就是找到状态转移的过程

零钱兑换(中等)

背包问题

  • 思路一:求兑换最少次数。用dp[i]记录兑换i元需要的次数,那么它的上一个状态就是dp[i-j],j代表第j位零钱的面值,由此可得转移方程dp[i]=Min dp[i-j] + 1,因为兑换i元的状态都是由上一状态i-j推算而来,所以得找出消耗次数最少的上一状态,同时得加一才能到达i状态
var coinChange = function(coins, amount) {
    let dp = new Array(amount + 1)
    dp.fill(amount + 1)
    dp[0] = 0
    for (let i = 1; i <= amount; i++) {
        for (let j = 0; j < coins.length; j++) {
            if (coins[j] <= i) {
                dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1)
            }
        }
    }
    return dp[amount] > amount ? -1 : dp[amount]
};
  • 思路二
    • 贪心算法。贪心算法是一种特殊的动态规划,大意就是每一步取当前的(局部)最优解,所有局部最优解汇集成全局解,但是全局解不一定最优,比如该题在之前是可以用贪心的,但后来力扣新加入了一个测试用例使得该方法不再适用,不过以此来学习一下贪心的思想还是值得的
    • 比如当前零钱有1、2、5,目标值为11,第一次兑换取最大零钱5,目标值还剩5,第二次取最大零钱5,~还剩1,最后取零钱1,兑换完毕。(每一步都是当前最优的情况,多用面值大的零钱就可以减少零钱兑换数量)
var coinChange = function(coins, amount) {
    // 贪心算法,被新的测试用例pass了
    coins.sort((a, b) => a - b)
    let index = coins.length - 1
    let sum = 0
    let cal = 0
    while (true) {
        if (sum === amount) return cal
        if (sum + coins[index] <= amount) {
            sum += coins[index]
            cal++
        } else {
            index--
            if (index < 0) return -1
        }
    }
}

零钱兑换 II(中等)

背包问题

  • 思路:求兑换所有可能(组合数)。用dp[i]记录面值之和为i的硬币组合数,敲定dp[0]=1,因为只有不选取任何硬币这一种情况使得和为零。遍历coins,看i是否可以由当前元素组成,同时记录次数dp[i],直到i的面值等于amount,再用下一元素如此反复,最终dp[amount]即为所求
var change = function(amount, coins) {
    let dp = new Array(amount + 1).fill(0)
    dp[0] = 1
    // 为什么遍历coins放在外层?不能先遍历i面值?解答在题后
    for (let coin of coins) {
        for (let i = coin; i <= amount; i++) {
            dp[i] += dp[i - coin]
        }
    }
    return dp[amount]
};
  • 解答:防止重复。举例,amount=3,coins=[1, 2],如果先遍历i面值,兑换到3时会得到3=1+2=2+1两种重复的情况,因为可以从数组中随便取数。而先遍历面值1,则3=1+1+1,再面值2,3=1+2,因为定死了顺序,取1只会在2前面取

礼物的最大价值(中等)

路径问题

  • 思路:找规律会发现,利用倒推,发现每一格只能由上、左方向的格子移动而来,令grid[i][j]表示到达第i行第j列个格子所能得到的价值,由此:转移方程grid[i][j]=grid[i][j]+Max(grid[i-1][j], grid[i][j-1]),同时会发现第一行格子只能由左边格子移动而来,第一列格子只能由上边格子移动而来,所以可以对其值初始化
var maxValue = function(grid) {
    let r = grid.length
    let c = grid[0].length
    // 初始化第一行
    for (let i = 1; i < r; i++) {
        grid[i][0]= grid[i][0] + grid[i - 1][0]
    }
    // 初始化第一列
    for (let j = 1; j < c; j++) {
        grid[0][j] = grid[0][j] + grid[0][j - 1]
    }
    // 遍历整个数组
    for (let i = 1; i < r; i++) {
        for (let j = 1; j < c; j++) {
            grid[i][j] = grid[i][j] + Math.max(grid[i - 1][j], grid[i][j - 1])
        }
    }
    return grid[r - 1][c - 1]
};

三角形最短路径和(中等)

路径问题

  • 思路:同理找规律,倒推路径,这样可以不用判断边界(正推会有元素不存在的情况),若把这个三角视作一颗二叉树的话,那么当前元素只能由其左右子树移动而来,得到转移方程:dp[i][j]=dp[i][j] + Max(dp[i+1][j], dp[i+1][j+1])
var minimumTotal = function(triangle) {
    let dp = triangle
    let len = triangle.length
    // 自底向上,不用判断边界
    for (let i = len - 2; i >= 0; i--) {
        for (let j = 0; j < dp[i].length; j++) {
            dp[i][j] += Math.min(dp[i + 1][j], dp[i + 1][j + 1])
        }
    }
    return dp[0][0]
};

打家劫舍(中等)

其他

  • 思路:由于不能偷紧贴着的两家,所以偷到当前这家的时候,有两种可能,其一是偷了上上家继续偷这家;其二是偷了上家,不偷这家,此时偷盗金额按上家的算。令dp[i]表示偷到第i家的最高金额,则状态转移方程为:dp[i]=Max(dp[i-2]+nums[i], dp[i-1]),同时初始化dp[0]为第一家金额,dp[1]为前两家金额的最大值
var rob = function(nums) {
    let dp = new Array(nums.length).fill(0)
    dp[0] = nums[0]
    dp[1] = Math.max(nums[0], nums[1])
    for (let i = 2; i < nums.length; i++) {
        dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1])
    }
    return dp.sort((a, b) => b - a)[0]
};
  • 思路:空间优化。上一方法是用一个数组记录了偷每一家的最大金额,不难发现整个求解过程只要知道上上家最大金额上家最大金额便可求出当最大金额,所以可以只维护这两个值便可
var rob = function(nums) {
    if (nums.length === 1) return nums[0]
    if (nums.length === 2) return Math.max(nums[0], nums[1])
    // 维护两个值
    let first = nums[0], second = Math.max(nums[0], nums[1])
    let res = 0
    for (let i = 2; i < nums.length; i++) {
        res = Math.max(first + nums[i], second)
        first = second
        second = res
    }
    return second
};

打家劫舍II(中等)

其他

  • 思路:此时第一家和最后一家紧贴了,如果还按之前的思路按顺序偷过去的话,无法判断偷到最后一家的时候是否在之前偷了第一家,容易掉进思维陷阱,其实得换个思路——分类讨论,两种情况,一:偷了第一家就不偷最后一家;二:没偷第一家,就可以偷最后一家,核心代码不变,多了个分类的处理
var rob = function(nums) {
    let len = nums.length
    if (len === 1) return nums[0]
    if (len === 2) return Math.max(nums[0], nums[1])
    let dynamic = (nums, start, end) => {
        let first = nums[start]
        let second = Math.max(nums[start], nums[start + 1])
        for (let i = start + 2; i < end; i++) {
            let temp = second
            second = Math.max(second, first + nums[i])
            first = temp
        }
        return second
    }
    return Math.max(dynamic(nums, 0, len - 1), dynamic(nums, 1, len))
};

如果觉得对你有帮助的话,点个赞呗~

反正发文又不赚钱,交个朋友呗~

如需转载,请注明出处foolBirdd

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值