动态规划概念、特点、经典例题和于其它算法思想的比较

1.动态规划概念

动态规划(Dynamic Programming,DP)通过把原问题分解成相对简单的子问题的方式来解决复杂问题的方法。它的基本思想是将待求解问题分解成不同部分(即子问题),然后依据子问题的解以得出原问题的解,而子问题又可递归地分解为子子问题(到此跟分治法的思想类似,后面是不同)。通常许多子问题可能会重复出现(重复子问题),DP试图仅仅解每个子问题一次,在求得每个子问题的解后将其保存起来,下次再需要求解相同子问题时,直接查表得到,从而减少计算量,它的精髓在于记住求过的解来节省时间,体现了以空间换时间的算法思想,这也是其与分治法最大的区别。
此外,还看到有资料对DP思路的阐述是将问题分解为多个阶段,每个阶段对应一个决策。我们记录每个阶段可达的状态集合(去掉重复的),然后通过当前阶段的状态集合推导下一个阶段的状态集合,动态地往前推进,直至到达最终阶段,这一阐述和DP的名字更贴合,也指出DP适用的问题是多阶段决策最优解模型

2.动态规划适用问题的特点

2.1 最优子结构

将母问题分解为子问题后,当子问题最优时,母问题通过优化选择一定最优的情况(或者说成母问题的最优解可由子问题的最优解构建得到)。从状态的角度理解,就是后面阶段的状态可以由前面阶段的状态推导出来。如挖金矿问题中,国王面临的问题可分解成左右大臣需要解决的问题,左大臣要解决的子问题是在不挖第10个矿的情况下,给定挖矿人数,确定在前9个矿中挖取最多金子的方案,右大臣要解决的子问题是在挖第10个矿的情况下,给定挖矿人数,确定在前9个矿中挖取最多金子的方案,当左右大臣给出了各自的最优方案后,国王就可以根据子问题的最优解以及第10个矿的信息确定整体最优解。

2.2 重复子问题

不同的决策序列,到达某个相同的阶段时,可能会产生相同的状态。有时子问题是否会重复出现不好分析,也可将母问题和子问题本质上是一样的,只是输入参数不同作为依据。

2.3 无后效性

子问题的解一旦确定,就不再改变,不受它之后包含它的更大问题的求解决策影响

3. 动态规划的例子

3.1 简单例子——Fibonacci数列

#include <iostream>
#include <vector>
using namespace std;

int fib1(int n)
{// 递归版
    return n < 2 ? n : fib1(n-1) + fib1(n-2);
}

int fib2(int n, vector<int> &table)
{// 动态规划自顶向下的备忘录版,就是在递归版的基础上用一个表去记录已求出的子问题的解
    if(table[n] != -1)
        return table[n];
    if(n < 2)
        table[n] = n;
    else
        table[n] = fib2(n-1, table) + fib2(n-2, table);
    return table[n];
}
int fib3(int n)
{// 动态规划自底向上迭代版
    if(n<2)
        return n;
    const int size = n+1;
    vector<int> table(size);
    table[0] = 0, table[1] = 1;
    for(int i=2; i<=n; ++i)
        table[i] = table[i-1] + table[i-2];
    return table[n];
    // // 简化版:只用2个变量即可
    // int f=0, g=1;
    // while(0<n--)
    // {
    //     g = f+g;
    //     f = g-f;
    // }
    // return f;
}

//-----------test-----------------
int main()
{
    int i;
    while(cin>>i)
    {
        const int size = i+1;
        vector<int> table(size,-1);
        if(i>=0)
            {
                cout<<"fib1:"<<fib1(i)<<endl;
                cout<<"fib2:"<<fib2(i, table)<<endl;
                cout<<"fib3:"<<fib3(i)<<endl;
            }
    }
    return 0;
}

3.2 经典例子——背包问题

  1. 0-1背包问题

对于一组不同重量、不可分割的物品,选择一些装入包中,在满足背包最大重量限制情况下,背包中物品总重量的最大值是多少?

  • 解法1:回溯——时间复杂度O(2^n)
let maxW = -Infinity
let weight = [2,2,4,6,3] // 物品重量
let n = 5 // 物品个数
let w = 9 // 背包最大载重
/*
@param: i——物品索引
@param: cw——决策完第i个物品后的背包重量
*/
function backpackRecur(i, cw) { // 调用backpackRecur(0,0)
  if(cw == w || i==n) {
    if(cw > maxW) 
      maxW = cw
    return 
  }
  backpackRecur(i+1, cw) // 不选第i个物品
  if(cw + weight[i] <= w) 
    backpackRecur(i+1, cw+weight[i]) // 选第i个物品
}
  • 解法2:回溯+备忘录——时间复杂度O(nw)
let maxW = -Infinity
let weight = [2,2,4,6,3] // 物品重量
let n = 5 // 物品个数
let w = 9 // 背包最大载重
let mem = new Array(n).fill(null)
for(let row of mem) {
  row = new Array(w+1).fill(false)
}
function backpackMem(i, cw) {
  if(cw == w || i==n) {
    if(cw > maxW) 
      maxW = cw
    return 
  }
  if(mem[i][cw]) return
  mem[i][cw] = true
  backpackRecur(i+1, cw) // 不选第i个物品
  if(cw + weight[i] <= w) 
    backpackRecur(i+1, cw+weight[i]) // 选第i个物品
}
  • 解法3:动态规划自底向上填表——时间复杂度O(nw),空间复杂度O(nw)
let maxW = -Infinity
let weight = [2,2,4,6,3] // 物品重量
let n = 5 // 物品个数
let w = 9 // 背包最大载重

let state = new Array(n).fill(null)
for(let i=0; i<n; ++i) {
  state[i] = new Array(w+1).fill(false)
}
function backpackDP() {
  // 初始化状态表
  state[0][0] = true
  if(weight[0] < w)
    state[0][weight[0]] = true
  for(let i=1; i<n; i++) {
    for(let j=0; j<=w; j++) {
      if(state[i-1][j]) // 不选第i个物品
        state[i][j] = state[i-1][j]
    }
    for(let j=0; j<=w-weight[i]; j++) {
      if(state[i-1][j]) // 选第i个物品,不能与上面合并,因为下一句的修改在后续循环中可能会被上面的覆盖
        state[i][j+weight[i]] = state[i-1][j]
    }
  }
  for(let j=w; j>=0; j--) {
    if(state[n-1][j]) {
      maxW = j
//      return 
      // 返回决策序列
      let res = []
      for(let i=n-1; i>0; --i) {
        if(j-weight[i] >= 0 && state[i][j] !== state[i-1][j]) {
          res.unshift(i)
          j = j-weight[i]
        }
      }
      if(j > 0)
        res.unshift(0)
      return res
    }
  }
}
  • 解法4:动态规划优化空间复杂度
let maxW = -Infinity
let weight = [2,2,4,6,3] // 物品重量
let n = 5 // 物品个数
let w = 9 // 背包最大载重

let state = new Array(w+1).fill(false)

function backpackDP(i, cw) {
  // 初始化状态表
  state[0] = true
  if(weight[0] < w)
    state[weight[0]] = true
  for(let i=1; i<n; i++) {
    for(let j=w-weight[i]; j>=0; j--) { //注意是从后往前的顺序,否则上一轮循环算好的结果可能在被使用前被覆盖掉
      if(state[j]) // 选第i个物品
        state[j+weight[i]] = state[j]
    }
  }
  for(let j=w; j>=0; j--) {
    if(state[j]) {
      maxW = j
      return 
    }
  }
}
  1. 带价值的升级版0-1背包问题

对于一组不同重量、不同价值、不可分割的物品,选择一些装入包中,在满足背包最大重量限制情况下,背包中物品总价值的最大值是多少?

  • 解法1:回溯(递归)
let maxVal = -Infinity
let weight = [2,2,4,6,3] // 物品重量
let value = [3,4,8,9,6]
let n = 5 // 物品个数
let w = 9 // 背包最大载重
/*
@param: i——物品索引
@param: cw——决策完第i个物品后的背包重量
@param: cv——决策完第i个物品后的背包价值
*/
function backpackRecur(i, cw, cv) {
  if(cw==w || i==n) {
    if(cv > maxVal) 
      maxVal = cv
    return
  }
  backpackRecur(i+1, cw, cv) // 不选第i个物品
  if(cw+weight[i] <= w) { // 选第i个物品
    backpackRecur(i+1, cw+weight[i], cv+value[i])
  }
}
  • 解法2:动态规划自顶向下递归+备忘录
let maxVal = -1
let weight = [2,2,4,6,3] // 物品重量
let value = [3,4,8,9,6]
let n = 5 // 物品个数
let w = 9 // 背包最大载重
let mem = new Array(n).fill(null)
for(let i=0; i<n; ++i) {
  mem[i] = new Array(w+1).fill(-1) // mem[i][j]表示用i+1个物品组成重量不超过j的组合所具有的最大价值
}
// 状态转移方程为f(i, j) = max(f(i-1, j), f(i-1, j-weight[i])+value[i]), j>=weight[i];f(i, j) = f(i-1, j), j<weight[i]
function backpackDpRecur(i, j) { // 调用backpackDpRecur(n-1, w)
  if(mem[i][j] != -1)
    return mem[i][j]
  if(i===0)
    maxVal =  j>=weight[0] ? value[i] : 0
  else if(j===0)
    maxVal = 0
  else
    maxVal = j<weight[i] ? backpackDpRecur(i-1,j) : Math.max(backpackDpRecur(i-1,j), backpackDpRecur(i-1,j-weight[i])+value[i])
  mem[i][j] = maxVal
  return maxVal
}
  • 解法3:动态规划自底向上填表法
let maxVal = -1
let weight = [2,2,4,6,3] // 物品重量
let value = [3,4,8,9,6]
let n = 5 // 物品个数
let w = 9 // 背包最大载重
let state = new Array(n).fill(null)
for(let i=0; i<n; ++i) {
  state[i] = new Array(w+1).fill(-1) // state[i][j]表示用i+1个物品组成重量不超过j的组合所具有的最大价值
}
function backpackDP() {
  // 初始化状态
  state[0][0] = 0
  // if(weight[0] < w)
  //   state[0][weight[0]] = value[0]
  for(let j=weight[0]; j<w; ++j)
    state[0][j] = value[0]
  for(let i=1; i<n; ++i) {
    // for(let j=0; j<=w; ++j) {
    //   if(state[i-1][j] != -1) // 不选第i个物品
    //     state[i][j] = state[i-1][j]
    // }
    // for(let j=0; j<=w-weight[i]; ++j) {
    //   if(state[i-1][j] != -1) {// 选第i个物品
    //     let tmp = state[i-1][j] + value[i]
    //     if(tmp > state[i][j+weight[i]]) // state[i][j+weight[i]]可能已有值,选择价值最大的
    //       state[i][j+weight[i]] = tmp
    //   }
    // }
    for(let j=0; j<=w; ++j) {
      if(j < weight[i])
        state[i][j] = state[i-1][j]
      else{
        state[i][j] = Math.max(state[i-1][j], state[i-1][j-weight[i]]+value[i])
      }
    }
  } 
  for(let j=w; j>=0; --j) {
    if(state[n-1][j] > maxVal) {
      maxVal = state[n-1][j]
      return maxVal
    }
  }
}
  • 解法4:动态规划优化空间复杂度
let maxVal = -1
let weight = [2,2,4,6,3] // 物品重量
let value = [3,4,8,9,6]
let n = 5 // 物品个数
let w = 9 // 背包最大载重
let state = new Array(w+1).fill(-1)

function backpackDP() {
  // 初始化状态
  state[0] = 0
  // if(weight[0] < w)
  //   state[weight[0]] = value[0]
  for(let j=weight[0]; j<w; ++j)
    state[j] = value[0]
  for(let i=1; i<n; ++i) {
    for(let j=w; j>=weight[i]; --j) {
      // if(state[j] != -1) {// 选第i个物品
      //   let tmp = state[j] + value[i]
      //   if(tmp > state[j+weight[i]])
      //     state[j+weight[i]] = tmp
      // }
        state[j] = Math.max(state[j], state[j-weigth[i]]+value[i])
    }
  } 
  for(let j=w; j>=0; --j) {
    if(state[j] > maxVal) {
      maxVal = state[j]
      return maxVal
    }
  }
}

4 动态规划解题思路和方法

从前面的例子中可以看出,求解动态规划问题的思路是定义状态并写出状态转移方程,然后可以采用自顶向下的递归+备忘录方法或者自底向上的填写状态转移表方法。

对于递归+备忘录的方法,注意根据状态转移方程寻找边界条件作为递归终止条件,比如状态转移方程中下标索引的边界值

对于填写状态转移表方法,也要注意状态转移表的初始化

5 动态规划和贪心、回溯、分治算法的比较

5.1 动态规划

  1. 核心思想:将问题分解成多个阶段,记录各阶段可达的状态集合,通过当前阶段的状态集合推导下一阶段的状态集合,动态向前推进,空间换时间
  2. 适用问题:多阶段决策最优解问题
  3. 算法特点:最优子结构、无后效性、重复子问题
  4. 经典应用:斐波那契数列、背包问题

5.2 贪心

  1. 核心思想:只走一次,每一步都选择当前对自己最优的决策
  2. 适用问题:多阶段决策最优解问题,更具体来说是针对定义了限制值和期望值的一组数据,从中选出几个数据,在满足限制值的情况下期望值最大,如纸币找零、限重背包价值最大问题
  3. 算法特点:最优子结构、无后效性、贪心选择性(通过局部最优的选择能产生全局最优)
  4. 经典应用:霍夫曼编码、区间覆盖

5.3 回溯

  1. 核心思想:穷举搜索比较并且可回退
  2. 适用问题:适用范围很广但时间复杂度是指数级的
  3. 算法特点:适用广,复杂度高
  4. 经典应用:深度优先搜索

5.4 分治

  1. 核心思想:分而治之再合并结果
  2. 适用问题:也较为广泛
  3. 算法特点:子问题结构相似但不重叠
  4. 经典应用:二分搜索、MapReduce

参考资料:

  1. 动态规划理论:一篇文章带你彻底搞懂最优子结构、无后效性和重复子问题
<think>好的,用户想了解动态规划算法例题。我需要先回顾动态规划的基本概念,然后找一个合适的例子来讲解。动态规划通常用于优化问题,比如最短路、最长子序列等。用户可能对递归记忆化不太熟悉,所以得选个简单易懂的例子。 首先,斐波那契数列是个经典例子,但可能太简单了。或者选背包问题,不过0-1背包对新手来说可能有点难。也许最长公共子序列(LCS)比较合适?或者找零钱问题?选哪个呢? 考虑用户的需求,他们可能希望看到步骤分解,如何将问题分解为子问题,构建状态转移方程,以及填表过程。硬币找零问题比较直观,容易理解状态转移。比如,用最少的硬币组成某个金额。 确定例题后,我需要按步骤解释:问题描述、状态定义、转移方程、初始条件、填表过程、结果获取。还要注意边界情况,比如金额为0时需要0个硬币,或者无法找零的情况。 另外,用户可能想知道动态规划的优势,比如避免重复计算,时间复杂度优化。需要比较递归动态规划区别,说明为什么DP更高效。 还要检查是否遵循了系统级指令:数学公式用$...$,独立公式用$$...$$,结构清晰,用中文。确保LaTeX语法正确,比如下标、上标是否正确,方程对齐是否合适。 可能遇到的问题:用户对状态转移方程的理解困难,需要详细解释每个符号的含义。或者对填表过程不清楚,可以画个表格示例,但文本中只能用文字描述。 最后,总结动态规划的适用场景,以及解决这个例题的关键步骤,帮助用户举一反三。确认回答是否覆盖了问题分析、解决步骤优化原理,确保用户能跟着一步步理解。</think>### 动态规划算法例题详解 动态规划(Dynamic Programming, DP)常用于解决具有**重叠子问题****最优子结构**的问题。下面通过一个经典例题**“零钱兑换”**来逐步解析动态规划的核心思想与应用步骤。 --- #### **例题描述** 给定不同面额的硬币数组 $coins$ 一个总金额 $amount$,求凑出 $amount$ 所需的最少硬币个数。若无法凑出,返回 $-1$。 **示例**: - 输入:$coins = [1, 2, 5]$, $amount = 11$ - 输出:$3$(解释:$5 + 5 + 1 = 11$) --- #### **动态规划解题步骤** 1. **定义状态** 设 $dp[i]$ 表示凑出金额 $i$ 所需的最少硬币个数。 **目标**:求 $dp[amount]$。 2. **状态转移方程** 对于每个金额 $i$,遍历所有硬币 $coin$: - 若 $coin \leq i$,则 $dp[i] = \min(dp[i], dp[i - coin] + 1)$ **解释**:选择当前硬币 $coin$ 后,剩余金额为 $i - coin$,其最优解为 $dp[i - coin] + 1$(加1是因为使用了当前硬币)。 3. **初始化** - $dp[0] = 0$(金额0不需要硬币) - 其他 $dp[i]$ 初始化为 $\infty$(表示暂未找到解) 4. **填表顺序** 从小到大遍历金额 $i$,逐步更新 $dp$ 数组。 --- #### **代码实现(Python)** ```python def coinChange(coins, amount): dp = [float('inf')] * (amount + 1) dp[0] = 0 for i in range(1, amount + 1): for coin in coins: if coin <= i: dp[i] = min(dp[i], dp[i - coin] + 1) return dp[amount] if dp[amount] != float('inf') else -1 ``` --- #### **复杂度分析** - 时间复杂度:$O(n \times k)$,其中 $n$ 为金额,$k$ 为硬币种类数。 - 空间复杂度:$O(n)$,用于存储 $dp$ 数组。 --- #### **关键点总结** 1. **重叠子问题**:多次计算相同子问题(例如 $dp[5]$ 可能被多次调用),动态规划通过存储中间结果避免重复计算。 2. **最优子结构**:大问题的最优解依赖于子问题的最优解(例如 $dp[11]$ 依赖 $dp[10]$ 或 $dp[9]$)。 3. **边界条件处理**:初始化 $dp[0] = 0$ 是递推的基础。 --- #### **其他经典动态规划问题** 1. **最长递增子序列**(LIS):求序列中最长的严格递增子序列长度。 2. **背包问题**:在容量限制下选择物品最大化价值。 3. **编辑距离**:计算将一个字符串转换为另一个字符串的最小操作次数。 动态规划的核心在于**将问题分解为子问题,并利用历史状态推导当前状态**。通过练习经典例题,可以逐步掌握其设计技巧。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值