算法:动态规划

算法:动态规划

概述

动态规划(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. 状态

用几个变量(维度)能描述子问题?如 dp[i] 或 dp[i][j]

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;
}

运行结果

图片

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不只会拍照的程序猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值