算法:动态规划
概述
动态规划(Dynamic Programming, DP)是一种解决多阶段决策最优化问题的算法思想。其产生的直接背景是:传统递归分治在处理重叠子问题时存在大量重复计算(如斐波那契数列,复杂度 O(2ⁿ))。DP 通过保存已解决子问题的结果,避免重复计算,将指数级复杂度降为多项式级(如 O(n²))。
动态规划 = 暴力递归 + 备忘录(记忆化) + 表格化顺序计算 + 状态压缩
其中:
• 动态:决策过程是多阶段的,当前决策会影响未来状态。
• 规划:在给定约束下寻找最优方案。
场景
使用 DP 必须同时满足以下两个条件:
|
条件 |
定义 |
|---|---|
| 最优子结构 |
原问题的最优解包含子问题的最优解 |
| 重叠子问题 |
不同阶段会反复使用相同的子问题解 |
若不满足最优子结构(如有环图最长路径),DP 无效;若不满足重叠子问题(如快速排序),DP 无性能收益。
特点
• 空间换时间:额外存储子问题解,避免重复计算子问题,减少时间复杂度。
• 保证全局最优:只要问题满足最优子结构,DP保证找到全局最优解。
• 可压缩性:DP 表通常可降维(滚动数组),适合嵌入式内存受限环境。
核心
DP 的核心由四个要素组成:
1. 状态:用变量(如 dp[i]或dp[i][j])描述子问题。状态空间大小直接决定内存需求。
2. 状态转移方程:描述当前状态与之前状态的关系,如 dp[i] = f(dp[i-1], ...)。
3. 边界条件:最小子问题的解,作为递推的起始值。
4. 计算顺序:确保计算当前状态时,其依赖的状态已经完成。
注意:这里类似嵌入式中的状态机机制。
分析
按以下四步完成 DP 问题的分析:
1. 定义状态:明确 dp[i] 或 dp[i][j] 的含义,尽量精简状态维度。
2. 推导转移方程:分析当前状态与前面状态的关系,写出数学表达式。
3. 确定边界:找到递推起点(如 dp[0]、dp[n-1][j] 等)。
4. 确定计算顺序:根据依赖关系决定从小到大、从大到小、从上到下等顺序。
示例
爬楼梯
有 n 级台阶,每次可以跨 1 级或 2 级。问爬到第 n 级总共有多少种不同的方法。

分析

• 状态
dp[i] 表示爬到第 i 级台阶的方法总数。(i 从 0 到 n)。
• 转移
要到达第 i 级,要么从第 i-1 级跨 1 级上来,要么从第 i-2 级跨 2 级上来。
dp[i] = dp[i-1] + dp[i-2] (i ≥ 3),
• 边界
dp[0] = 1(站在地面,1 种方法,即不动)
dp[1] = 1(只能跨 1 级)
dp[2] = 2(可以跨 2 次,每次跨 1 级,或者跨 1 次,跨 2 级)
• 顺序
从第 1 级台阶向上迭代。

代码
/* *
* @ 爬楼梯问题,有 n 级台阶,
* @ 每次可以跨 1 级或 2 级。问爬到第 n 级总共有多少种不同的方法
* @ n - 需要求解的最终台阶位置
* @ 返回爬到 n 级台阶的方法总和
* */
int climb_stairs(int n)
{
/* 状态定义,dp[i] 表示爬到第 i 级台阶的方法总和 */
int i, dp[MAX_STAIR+1] = { 0 };
if (n > MAX_STAIR - 1)
return -1;
/* 边界条件 */
dp[0] = 1; /* 原地不动,值并无什么实际意义 */
dp[1] = 1; /* 上第一级台阶,只有一种方法 */
dp[2] = 2; /* 上第二级台阶,可以分两次,每次上一级,也可一次上2级台阶*/
/* 状态转移方程,dp[i] = dp[i-1] + dp[i-2],
到达i级台阶的方法,从i-1级台阶跨1步,或者从i-2级台阶跨2步 */
for (i=3; i<=n; i++) {
dp[i] = dp[i-2] + dp[i-1];
}
return (dp[n]);
}
优化
分析:当前台阶只与前两级台阶的值有关,从节省内存角度出发,不考虑记录所有台阶的值,只保存最新 2 级台阶的值即可。
/* *
* @ 爬楼梯问题优化,当前台阶只与前两级台阶的值有关,
* @ 从节省内存角度出发,不考虑记录所有台阶的值,只保存最新2级台阶的值即可
* @ n - 需要求解的最终台阶位置
* @ 返回爬到 n 级台阶的方法总和
* */
int climb_stairs_optimize(int n)
{
/* pre_2、pre_1 分别记录爬到当前台阶的前两级、前一级方法总和值 */
int i, pre_2, pre_1, curr;
/* 边界条件 */
pre_2 = 1; /* 上第一级台阶,只有一种方法 */
pre_1 = 2; /* 上第二级台阶,可以分两次,每次上一级,也可一次上2级台阶*/
/* 状态转移方程,curr = pre_2 + pre_1 */
for (i=3; i<=n; i++) {
curr = pre_2 + pre_1;
/* 更新前两级台阶方法值,为下一次计算做准备 */
pre_2 = pre_1;
pre_1 = curr;
}
return (curr);
}
最大子数组和
给定一个整数数组 nums,找出一个连续子数组(至少包含一个元素),使该子数组的和最大,返回这个最大和。

分析

• 状态
dp[i] 表示以第 i 个元素结尾的连续子数组的最大和。
• 转移
对于 nums[i],要么把它接到前面的子数组后面(dp[i-1] + nums[i]),要么自己单独成为一个新子数组(nums[i])。取较大者。
dp[i] = max(dp[i-1] + nums[i], nums[i])
• 边界
dp[0] = nums[0](以第一个元素结尾的子数组只能是它自己)
• 顺序
从第 1 个元素向后迭代。

代码
/* *
* @ 最大子数组和问题,给定一个整数数组nums,
* @ 找出一个连续子数组(至少包含一个元素),使该子数组的和最大,返回这个最大和。
* @ nums - 数据 n - 元素个数
* @ 返回最大连续子数组和
* */
int max_sub_array_sum(int *nums, int n)
{
/* 状态定义,dp[i] 表示以第 i 个元素结尾的连续子数组的最大和 */
int i, max, dp[MAX_ARR_NUM+1] = { 0 };
if (n > MAX_ARR_NUM - 1)
return -1;
/* 边界条件 */
dp[0] = nums[0]; /* 第一个元素结尾的子链就是它自己 */
max = dp[0]; /* 记录最大连续子数组和 */
/* 状态转移方程,dp[i] = MAX(dp[i-1]+nums[i], nums[i]),
当前最大值=取接上一个子数组或者单独成新子数组的最大值 */
for (i=1; i<n; i++) {
dp[i] = MAX(dp[i-1]+nums[i], nums[i]);
max = MAX(dp[i], max);
}
return (max);
}
优化
当前元素下的数组最大值,只与上一个元素结尾子数组的最大值有关,从节省内存角度出发,不考虑记录所有元素结尾子数组的值,只保存上一个元素结尾子数组最大值即可。
/* *
* @ 最大子数组和问题优化,当前元素下的数组最大值,只与上一个元素结尾子数组的最大值有关,
* @ 从节省内存角度出发,不考虑记录所有元素结尾子数组的值,只保存上一个元素结尾子数组最大值即可
* @ nums - 数据 n - 元素个数
* @ 返回最大连续子数组和
* */
int max_sub_array_sum_optimize(int *nums, int n)
{
/* pre表示上一个元素最大值 */
int i, pre, max;
/* 边界条件 */
pre = nums[0]; /* 第一个元素就是当前最大值 */
max = nums[0]; /* 记录最大子数组和的值 */
/* 状态转移方程,curr = pre_2 + pre_1 */
for (i=1; i<n; i++) {
pre = MAX(pre+nums[i], nums[i]);
max = MAX(pre, max);
}
return (max);
}
不同路径
一个机器人位于 m x n 网格的左上角(起点 (0,0)),它每次只能向右或向下移动一步,要到达右下角 (m-1, n-1)。问总共有多少条不同的路径?

分析

• 状态
dp[i][j] 表示从起点 (0,0) 走到格子 (i,j) 的不同路径数。
• 转移
要到达 (i,j),只能从它的上方 (i-1,j) 向下走一步,或者从左方 (i,j-1) 向右走一步。因此:
dp[i][j] = dp[i-1][j] + dp[i][j-1] (当 i>0 且 j>0)
• 边界
第一行 (0,j):只能一直向右,所以 dp[0][j] = 1
第一列 (i,0):只能一直向下,所以 dp[i][0] = 1
dp[0][0] = 1
• 顺序
从第 (0,0)出发迭代。

代码
/* *
* @ 不同路径问题,一个机器人位于m x n网格的左上角(起点 (0,0)),
* @ 它每次只能向右或向下移动一步,要到达右下角 (m-1, n-1)。问总共有多少条不同的路径?
* @ m - 网格行数 n - 网格列数
* @ 返回不同路径数量总和
* */
int unique_paths(int m, int n)
{
/* 状态定义,dp[i][j] 表示到达格子(i,j)有多少条路径 */
int i, j, dp[MAX_ARR_NUM+1][MAX_ARR_NUM+1] = { {0} };
if ((n > MAX_ARR_NUM - 1) || (m > MAX_ARR_NUM - 1))
return -1;
/* 边界条件,第 1 行和第 1 列都只有一种路径 */
for (i=0; i<m; i++)
dp[i][0] = 1;
for (i=0; i<n; i++)
dp[0][i] = 1;
/* 状态转移方程,dp[i][j] = dp[i-1][j] + dp[i][j-1];,
要到达当前格子的方法=到达上边格子的方法+到达右边格子的方法 */
for (i=1; i<m; i++) {
for (j=1; j<n; j++) {
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return (dp[m-1][n-1]);
}
优化
将二维数组压缩成一维,逐行计算各个格子不同路径总数,此时计算(i, j)格子时,(i, j-1)的格子(左边)已更新,未更新的(i, j)实际存储为上面的格子的路径数。

/* *
* @ 不同路径问题优化,将二维数组压缩成一维,逐行计算各个格子不同路径总数
* @ 此时计算(i,j)格子时,(i,j-1)的格子(左边)已更新,未更新的(i,j)实际存储为上面的格子的路径数
* @ m - 网格行数 n - 网格列数
* @ 返回不同路径数量总和
* */
int unique_paths_optimize(int m, int n)
{
/* 状态定义,dp[i] 表示到达指定格子有多少条路径,每次存储一行的数据 */
int i, j, dp[MAX_ARR_NUM+1] = {0};
if (n > MAX_ARR_NUM - 1)
return -1;
/* 边界条件,第 1 行只有一种路径 */
for (j=0; j<n; j++)
dp[j] = 1;
/* 状态转移方程,dp[j] = dp[j-1](右边) + dp[j](上边);,
要到达当前格子的方法=到达上边格子的方法+到达右边格子的方法 */
for (i=1; i<m; i++) {
for (j=1; j<n; j++) {
dp[j] = dp[j-1] + dp[j];
}
}
return (dp[n-1]);
}
0-1 背包问题
有 N 件物品和一个容量为 W 的背包。第 i 件物品的重量是 wt[i],价值是 val[i]。每件物品只能选一次(0-1 背包)。求在不超过背包容量的前提下,能装入的最大总价值。

分析

• 状态
dp[i][j] 表示:考虑前 i 件物品(索引 1~i),背包容量为 j 时,能获得的最大价值。
• 转移
对于第 i 件物品(重量 wt[i-1],价值 val[i-1]),有两种选择:
取两者最大值:
dp[i][j] = max(dp[i-1][j], dp[i-1][j - wt[i-1]] + val[i-1]) (当 j ≥ wt[i-1])
不装:dp[i][j] = dp[i-1][j]
装(前提 j ≥ wt[i-1]):dp[i][j] = dp[i-1][j - wt[i-1]] + val[i-1]
• 边界
第一行没有物品可选,价值为0。所以 dp[0][j] = 0
第一列容量为0,价值为0。所以 dp[i][0] = 0
• 顺序
从dp[1][1]开始顺序迭代。
代码
/* *
* @ 0-1背包问题,有N件物品和一个容量为W的背包。第i件物品的重量是wt[i],价值是val[i]。
* @ 每件物品只能选一次(0-1 背包)。求在不超过背包容量的前提下,能装入的最大总价值。
* @ W - 容量上限 wt - 物品重量 N - 物品数 val - 物品价值
* @ 返回最大总价值
* */
int knapsack(int W, int wt[], int N, int val[])
{
/* 状态定义,dp[i][j] 表示考虑前 i 件物品,重量不超过 j 的最大价值 */
int i, j, dp[MAX_N+1][MAX_C+1] = { {0} };
if ((N > MAX_N - 1) || (W > MAX_C - 1))
return -1;
/* 边界条件,背包重量限制为0时或者背包没有物品时价值均为0 */
for (i=0; i<N; i++)
dp[i][0] = 0;
for (j=0; j<W; j++)
dp[0][j] = 0;
/* 状态转移方程,dp[i][j] = MAX(dp[i-1][j], dp[i-1][j-wt[i-1]] + val[i-1]),
面对第 i 件物品(wt[i-1], val[i-1]),取不装dp[i-1][j],或者装dp[i-1][j-wt[i-1]]+val[i-1]的最大值 */
for (i=1; i<=N; i++) {
for (j=W; j>=wt[i-1]; j--)
dp[i][j] = MAX(dp[i-1][j], dp[i-1][j-wt[i-1]] + val[i-1]);
}
return (dp[N][W]);
}
优化
观察计算 dp[i][j] 时,只用到了 第 i-1 行 的 dp[i-1][j] 和 dp[i-1][j - wt[i-1]]。也就是只依赖上一行的数据,不会用到更早的行。所以我们只需要保留一行(当前正在计算的行)就够了。
注意:当你计算 dp[j] 时,右边的 dp[j - wt[i]] 可能是已经更新过的(因为容量小先算)。如果 dp[j - wt[i]] 已经包含了当前物品 i,那么 dp[j] 就会再次尝试加入物品 i,相当于同一物品可以用多次,这就变成了完全背包,而不是 0-1 背包(每个物品只能用一次)。所以我们只能倒叙遍历,保证j - wt[i-1]更新在j更新之后。
/* *
* @ 0-1背包问题优化,观察上述状态转移方程:dp[i][j] = MAX(dp[i-1][j], dp[i-1][j-wt[i-1]] + val[i-1])
* @ 计算dp[i][j]时,只用到了第i-1行的dp[i-1][j]和dp[i-1][j - wt[i-1]]。
* @ 即只依赖上一行的数据,不会用到更早的行。所以,我们只需要保留一行(当前正在计算的行)就够了。
* @ 注意:j-wt[i-1] < j,如果顺序遍历,计算j重量的时候,j - wt[i-1]必然已经更新过,此时会变成完全背包。
* @ 所以我们只能倒叙遍历,保证j - wt[i-1]更新在j更新之后。
* @ W - 容量上限 wt - 物品重量 N - 物品数 val - 物品价值
* @ 返回最大总价值
* */
int knapsack_optimize(int W, int wt[], int N, int val[])
{
/* 状态定义,dp[i] 表示考虑单个物品,不同重量下的最大价值 */
int i, j, dp[MAX_C+1] = {0};
if (W > MAX_C - 1)
return -1;
/* 边界条件,背包没有物品时价值为0 */
for (i=0; i<=W; i++)
dp[i] = 0;
/* 状态转移方程,dp[j] = MAX(dp[j], dp[j-wt[i-1]] + val[i-1]);,
商品最大价值 = 取装与不装分别获取的价值的最大值 */
for (i=1; i<=N; i++) {
for (j=W; j>=wt[i-1]; j--)
dp[j] = MAX(dp[j], dp[j-wt[i-1]] + val[i-1]);
}
return (dp[W]);
}
总结
动态规划是一种“填表”算法:它把大问题拆成一系列相互依赖的子问题,自底向上(或自顶向下带备忘录)依次计算出每个子问题的答案,最终组合出原问题的解。它的时间复杂度通常与状态数量呈多项式关系,远优于指数级的暴力递归。
DP 是解决重叠子问题最优化问题的标准方法,核心四要素:状态、转移方程、边界、顺序。
|
步骤 |
要回答的问题 |
|---|---|
|
1. 状态 |
用几个变量(维度)能描述子问题?如 |
|
2. 转移 |
当前状态 = 前面某些状态的某种组合(+、max、min等) |
|
3. 边界 |
边界(i=0、j=0、容量=0 时)的值是什么? |
|
4. 顺序 |
最终确定递推方向,从下往上,从每行左到右? |
注意:计算顺序不唯一。你也可以从上往下,但状态定义和转移方程会变(比如定义为从顶部走到(i,j)的最大和)。两种顺序都可以,但边界不同。
完整代码
/**
* @Filename : study_dp.c
* @Revision : $Revision: 1.00 $
* @Author : Feng(更多编程相关的知识和源码见微信公众号:不只会拍照的程序猿,欢迎订阅)
* @Description : 动态规划学习
**/
#include <stdio.h>
#include <string.h>
#define MAX_STAIR 30 /* 台阶最大值 */
#define MAX_ARR_NUM 30 /* 数组最大值 */
#define MAX_N 10 /* 最大物品数量 */
#define MAX_C 30 /* 最大背包重量 */
#define MAX(x, y) (x) > (y) ? (x) : (y)
#define MIN(x, y) (x) < (y) ? (x) : (y)
#define ARR_SIZE(x) sizeof(x)/sizeof(x[0])
/* *
* @ 爬楼梯问题,有 n 级台阶,
* @ 每次可以跨 1 级或 2 级。问爬到第 n 级总共有多少种不同的方法
* @ n - 需要求解的最终台阶位置
* @ 返回爬到 n 级台阶的方法总和
* */
int climb_stairs(int n)
{
/* 状态定义,dp[i] 表示爬到第 i 级台阶的方法总和 */
int i, dp[MAX_STAIR+1] = { 0 };
if (n > MAX_STAIR - 1)
return -1;
/* 边界条件 */
dp[0] = 1; /* 原地不动,值并无什么实际意义 */
dp[1] = 1; /* 上第一级台阶,只有一种方法 */
dp[2] = 2; /* 上第二级台阶,可以分两次,每次上一级,也可一次上2级台阶*/
/* 状态转移方程,dp[i] = dp[i-1] + dp[i-2],
到达i级台阶的方法,从i-1级台阶跨1步,或者从i-2级台阶跨2步 */
for (i=3; i<=n; i++) {
dp[i] = dp[i-2] + dp[i-1];
}
return (dp[n]);
}
/* *
* @ 爬楼梯问题优化,当前台阶只与前两级台阶的值有关,
* @ 从节省内存角度出发,不考虑记录所有台阶的值,只保存最新2级台阶的值即可
* @ n - 需要求解的最终台阶位置
* @ 返回爬到 n 级台阶的方法总和
* */
int climb_stairs_optimize(int n)
{
/* pre_2、pre_1 分别记录爬到当前台阶的前两级、前一级方法总和值 */
int i, pre_2, pre_1, curr;
/* 边界条件 */
pre_2 = 1; /* 上第一级台阶,只有一种方法 */
pre_1 = 2; /* 上第二级台阶,可以分两次,每次上一级,也可一次上2级台阶*/
/* 状态转移方程,curr = pre_2 + pre_1 */
for (i=3; i<=n; i++) {
curr = pre_2 + pre_1;
/* 更新前两级台阶方法值,为下一次计算做准备 */
pre_2 = pre_1;
pre_1 = curr;
}
return (curr);
}
/* *
* @ 最大子数组和问题,给定一个整数数组nums,
* @ 找出一个连续子数组(至少包含一个元素),使该子数组的和最大,返回这个最大和。
* @ nums - 数据 n - 元素个数
* @ 返回最大连续子数组和
* */
int max_sub_array_sum(int *nums, int n)
{
/* 状态定义,dp[i] 表示以第 i 个元素结尾的连续子数组的最大和 */
int i, max, dp[MAX_ARR_NUM+1] = { 0 };
if (n > MAX_ARR_NUM - 1)
return -1;
/* 边界条件 */
dp[0] = nums[0]; /* 第一个元素结尾的子链就是它自己 */
max = dp[0]; /* 记录最大连续子数组和 */
/* 状态转移方程,dp[i] = MAX(dp[i-1]+nums[i], nums[i]),
当前最大值=取接上一个子数组或者单独成新子数组的最大值 */
for (i=1; i<n; i++) {
dp[i] = MAX(dp[i-1]+nums[i], nums[i]);
max = MAX(dp[i], max);
}
return (max);
}
/* *
* @ 最大子数组和问题优化,当前元素下的数组最大值,只与上一个元素结尾子数组的最大值有关,
* @ 从节省内存角度出发,不考虑记录所有元素结尾子数组的值,只保存上一个元素结尾子数组最大值即可
* @ nums - 数据 n - 元素个数
* @ 返回最大连续子数组和
* */
int max_sub_array_sum_optimize(int *nums, int n)
{
/* pre表示上一个元素最大值 */
int i, pre, max;
/* 边界条件 */
pre = nums[0]; /* 第一个元素就是当前最大值 */
max = nums[0]; /* 记录最大子数组和的值 */
/* 状态转移方程,curr = pre_2 + pre_1 */
for (i=1; i<n; i++) {
pre = MAX(pre+nums[i], nums[i]);
max = MAX(pre, max);
}
return (max);
}
/* *
* @ 不同路径问题,一个机器人位于m x n网格的左上角(起点 (0,0)),
* @ 它每次只能向右或向下移动一步,要到达右下角 (m-1, n-1)。问总共有多少条不同的路径?
* @ m - 网格行数 n - 网格列数
* @ 返回不同路径数量总和
* */
int unique_paths(int m, int n)
{
/* 状态定义,dp[i][j] 表示到达格子(i,j)有多少条路径 */
int i, j, dp[MAX_ARR_NUM+1][MAX_ARR_NUM+1] = { {0} };
if ((n > MAX_ARR_NUM - 1) || (m > MAX_ARR_NUM - 1))
return -1;
/* 边界条件,第 1 行和第 1 列都只有一种路径 */
for (i=0; i<m; i++)
dp[i][0] = 1;
for (i=0; i<n; i++)
dp[0][i] = 1;
/* 状态转移方程,dp[i][j] = dp[i-1][j] + dp[i][j-1];,
要到达当前格子的方法=到达上边格子的方法+到达右边格子的方法 */
for (i=1; i<m; i++) {
for (j=1; j<n; j++) {
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return (dp[m-1][n-1]);
}
/* *
* @ 不同路径问题优化,将二维数组压缩成一维,逐行计算各个格子不同路径总数
* @ 此时计算(i,j)格子时,(i,j-1)的格子(左边)已更新,未更新的(i,j)实际存储为上面的格子的路径数
* @ m - 网格行数 n - 网格列数
* @ 返回不同路径数量总和
* */
int unique_paths_optimize(int m, int n)
{
/* 状态定义,dp[i] 表示到达指定格子有多少条路径,每次存储一行的数据 */
int i, j, dp[MAX_ARR_NUM+1] = {0};
if (n > MAX_ARR_NUM - 1)
return -1;
/* 边界条件,第 1 行只有一种路径 */
for (j=0; j<n; j++)
dp[j] = 1;
/* 状态转移方程,dp[j] = dp[j-1](右边) + dp[j](上边);,
要到达当前格子的方法=到达上边格子的方法+到达右边格子的方法 */
for (i=1; i<m; i++) {
for (j=1; j<n; j++) {
dp[j] = dp[j-1] + dp[j];
}
}
return (dp[n-1]);
}
/* *
* @ 0-1背包问题,有N件物品和一个容量为W的背包。第i件物品的重量是wt[i],价值是val[i]。
* @ 每件物品只能选一次(0-1 背包)。求在不超过背包容量的前提下,能装入的最大总价值。
* @ W - 容量上限 wt - 物品重量 N - 物品数 val - 物品价值
* @ 返回最大总价值
* */
int knapsack(int W, int wt[], int N, int val[])
{
/* 状态定义,dp[i][j] 表示考虑前 i 件物品,重量不超过 j 的最大价值 */
int i, j, dp[MAX_N+1][MAX_C+1] = { {0} };
if ((N > MAX_N - 1) || (W > MAX_C - 1))
return -1;
/* 边界条件,背包重量限制为0时或者背包没有物品时价值均为0 */
for (i=0; i<N; i++)
dp[i][0] = 0;
for (j=0; j<W; j++)
dp[0][j] = 0;
/* 状态转移方程,dp[i][j] = MAX(dp[i-1][j], dp[i-1][j-wt[i-1]] + val[i-1]),
面对第 i 件物品(wt[i-1], val[i-1]),取不装dp[i-1][j],或者装dp[i-1][j-wt[i-1]]+val[i-1]的最大值 */
for (i=1; i<=N; i++) {
for (j=W; j>=wt[i-1]; j--)
dp[i][j] = MAX(dp[i-1][j], dp[i-1][j-wt[i-1]] + val[i-1]);
}
return (dp[N][W]);
}
/* *
* @ 0-1背包问题优化,观察上述状态转移方程:dp[i][j] = MAX(dp[i-1][j], dp[i-1][j-wt[i-1]] + val[i-1])
* @ 计算dp[i][j]时,只用到了第i-1行的dp[i-1][j]和dp[i-1][j - wt[i-1]]。
* @ 即只依赖上一行的数据,不会用到更早的行。所以,我们只需要保留一行(当前正在计算的行)就够了。
* @ 注意:j-wt[i-1] < j,如果顺序遍历,计算j重量的时候,j - wt[i-1]必然已经更新过,此时会变成完全背包。
* @ 所以我们只能倒叙遍历,保证j - wt[i-1]更新在j更新之后。
* @ W - 容量上限 wt - 物品重量 N - 物品数 val - 物品价值
* @ 返回最大总价值
* */
int knapsack_optimize(int W, int wt[], int N, int val[])
{
/* 状态定义,dp[i] 表示考虑单个物品,不同重量下的最大价值 */
int i, j, dp[MAX_C+1] = {0};
if (W > MAX_C - 1)
return -1;
/* 边界条件,背包没有物品时价值为0 */
for (i=0; i<=W; i++)
dp[i] = 0;
/* 状态转移方程,dp[j] = MAX(dp[j], dp[j-wt[i-1]] + val[i-1]);,
商品最大价值 = 取装与不装分别获取的价值的最大值 */
for (i=1; i<=N; i++) {
for (j=W; j>=wt[i-1]; j--)
dp[j] = MAX(dp[j], dp[j-wt[i-1]] + val[i-1]);
}
return (dp[W]);
}
int main(void)
{
int n = 11;
int r = 3, c = 7;
int nums[] = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
int W = 10; // 背包容量
int wt[] = {2, 3, 4, 5}; // 物品重量
int val[] = {3, 4, 5, 6}; // 物品价值
int N = ARR_SIZE(wt);
int size = ARR_SIZE(nums);
printf("爬 %d 级台阶的方法数: %d\n", n, climb_stairs(n));
printf("爬 %d 级台阶的方法数(优化版本): %d\n", n, climb_stairs_optimize(n));
printf("最大子数组和: %d\n", max_sub_array_sum(nums, size));
printf("最大子数组和(优化版本): %d\n", max_sub_array_sum_optimize(nums, size));
printf("%dx%d 网格的不同路径数: %d\n", r, c, unique_paths(r, c));
printf("%dx%d 网格的不同路径数(优化版本): %d\n", r, c, unique_paths_optimize(r, c));
printf("最大背包价值: %d\n", knapsack(W, wt, N, val));
printf("最大背包价值(优化版本): %d\n", knapsack_optimize(W, wt, N, val));
return 0;
}
运行结果


689

被折叠的 条评论
为什么被折叠?



