动态规划 & 贪心

动态规划

原理

动态规划(Dynamic Programming, DP)的核心思想:记住过去,避免重复计算

          fib(5)
         /      \
    fib(4)     fib(3)
    /    \      /    \
 fib(3) fib(2) fib(2) fib(1)  ← 大量重复

DP vs 分治

  • 分治:子问题相互独立(如归并排序)
  • DP:子问题存在重叠,需要记忆化

三大要素

要素说明示例(斐波那契)
最优子结构问题的最优解包含子问题的最优解fib(5) = fib(4) + fib(3)
重叠子问题子问题被重复计算fib(3) 在 fib(5) 和 fib(4) 中都出现
状态转移方程问题间的递推关系dp[i] = dp[i-1] + dp[i-2]

两种实现方式

方式方向特点
自顶向下(递归 + 备忘录)从大问题向下拆解更直观,但有递归开销
自底向上(迭代填表)从小问题向上推导性能更好,无递归栈开销

解题模板

1. 定义状态(dp[i] 的含义)
2. 找出状态转移方程
3. 确定初始条件 / base case
4. 确定遍历顺序
5. 返回目标结果

经典题型

类型特征示例
一维 DP单序列递推斐波那契、爬楼梯、打家劫舍
二维 DP双序列 / 网格最长公共子序列、最小路径和
背包问题选与不选决策0-1 背包、完全背包
区间 DP区间内合并求最优最长回文子串、石子合并
树形 DP树结构上递推二叉树最大路径和

贪心

原理

贪心算法(Greedy)的核心思想:局部最优 → 全局最优

每一步都选当前最优的决策,
期望最终结果就是全局最优。

DP vs 贪心

对比动态规划贪心
决策方式考虑所有可能后选最优只选当前最优
子问题依赖子问题结果独立决策,不依赖
适用范围更广有限(需满足贪心选择性质)
证明无需需要证明贪心策略正确性

贪心正确性条件

  1. 贪心选择性质 — 通过局部最优能得到全局最优
  2. 最优子结构 — 同 DP,子问题的最优解是原问题最优解的一部分

经典题型

类型特征贪心策略
区间调度不重叠区间最多数量按结束时间最早选
跳跃游戏能否跳到末尾 / 最少步数维护最远可达位置
买卖股票多笔交易求最大利润每天涨幅都吃
分发饼干满足最多孩子最小饼干满足最小胃口

题目

动态规划

  • 70. 爬楼梯 — 一维 DP,斐波那契递推 | 时间 O(n) 空间 O(1)(优化后)
var climbStairs = function(n) {
    if(n<=2) return n
    return climbStairs(n-1) + climbStairs(n-2)
};
var climbStairs = function(n) {
    const dp = []
    dp[0] = 1
    dp[1] = 2
    if(n<=2) return dp[n-1]
    for(let i =2 ; i< n;i++) {
        temp = dp[0] + dp[1]
        dp[0] = dp[1]
        dp[1] = temp
    }
    return dp[1]
};
var rob = function(nums) {
    if(nums.length === 1) return nums[0]
    const dp = [nums[0], Math.max(nums[0], nums[1])]
    for(let i = 2; i < nums.length; i++) {
        dp[i] = Math.max(dp[i-1], dp[i-2]+nums[i])
    }
    return dp[dp.length-1]
};

空间优化:dp[i] 只依赖 dp[i-1] 和 dp[i-2],可用两个变量滚动替代数组 → O(1):

var rob = function(nums) {
    if(nums.length === 1) return nums[0]
    const dp = [nums[0], Math.max(nums[0], nums[1])]
    for(let i = 2; i < nums.length; i++) {
        let temp = Math.max(dp[1], dp[0]+nums[i])
        dp[0] = dp[1]
        dp[1] = temp
    }
    return dp[dp.length-1]
};
  • 0-1 背包 — 二维 DP,选与不选的经典决策(可用 416. 分割等和子集作为入门)
/**
 * @param {number[]} nums
 * @return {boolean}
 */
var canPartition = function(nums) {
    const count = nums.reduce((a,b)=>a+b)
    const mid = count / 2
    if(Math.floor(mid) !== mid) return false
const dp = new Array(nums.length).fill(false).map(() => new Array(mid + 1).fill(false));
    if(nums[0] <=  mid) {
        dp[0][nums[0]] = true
    }
    for (let i = 0; i < nums.length; i++) {
        dp[i][0] = true;
    }
    for(let i = 1; i < nums.length; i++) {
        for(let j = 0; j <=  mid; j++) {
            dp[i][j] = dp[i-1][j]
            if(nums[i] < j) {
                dp[i][j] = dp[i][j] || dp[i-1][j - nums[i]]
            } else if (nums[i] === j) {
                dp[i][j] = true
            }
        }
    }
    return dp[nums.length-1][mid]
};

贪心

var findContentChildren = function(g, s) {
    g.sort((a, b) => a-b)
    s.sort((a,b) => a-b)
    let res=0
    for(let i=0; i< s.length; i++){
        if(g[res]<=s[i]) {
            res++
        }
    }
    return res
};
var maxProfit = function(prices) {
    let res = 0
    for(let i=1;i<prices.length;i++) {
        if(prices[i] > prices[i-1]) {
            res+=prices[i] - prices[i-1]
        }
    }
    return res
};

相关笔记