算法:动态规划

算法整理六——动态规划 一、概述 基本思想是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。 动态规划算法的有效性依赖于问题本身所具有的两个重要性质:最优子结构、重叠子问题 1、最优子结构:当问题的最优解包含了其子问题的最优解时,称该问题具有最优子结构性质。 2、重叠子问题: 在解问题时,每次产生的子问题并不总是新问题,有些子问题被反复计算多次。动态规划算法(自底向上)正是利用了这种子问题的重叠性质,对每一个子问题只解一次,而后将其解保存在一个表格中,在以后尽可能多地利用这些子问题的解。 阅读详情

算法:动态规划

概述

动态规划(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;
}

运行结果

图片

算法:分治,动态规划,贪心,回溯 1.分治 1.1 基本概念 即分而治之,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。 1.2 分治法适用的情况 1) 该问题的规模缩小到一定的程度就可以容易地解决 2) 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。 3) 利用该问题分解出的子问题的解可以合并为该问题的解; 4) 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。 第一条特征是绝大 阅读详情

相关推荐

C++的算法:动态规划算法

C++的算法:动态规划算法

全栈工程师Linda的博客 1308

【算法】动态规划(Dynamic Programming)

文章目录什么是动态规划算法数字三角形经典递归解法 什么是动态规划算法 总体思想 (1)动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题 (2)经分解得到的子问题往往不是互相独立的,有些子问题被重复计算多次 (3)如果能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,就可以避免大量重复计算,从而得到多项式时间算法(备忘录法) (4)图解: 使用动态规划来求解的问题需要具备的基本要素包括: (1)重复子问题 递归算法求解问题时,每次产生的子问题并不总是新问题,有些子

高朗的博客 4万+

JAVA算法:切割木棒—递归算法与动态规划算法

JAVA算法:切割木棒—递归算法与动态规划算法 给定一根长度为N的木棒和一系列价格,其中包含所有小于N的尺寸的价格。通过切割木棒和出售木棒来确定可获得的最大值。 例如,如果木棒的长度为8,不同部分的值如下所示,则可获得的最大值为22(通过切割两段长度2和6) 长度 1 2 3 4 5 6 7 8 价值 1 5 8 9...

梅森上校的博客 业精于勤荒于嬉,形成于思毁于随。 2413

(一)、动态规划的基本思想

拉肚肚 2098

算法设计与分析:动态规划算法思想的应用

本关任务:给定2个字符串,试求出这2个字符串的最长公共子串的长度。 本关任务:给定一个矩阵matrix,从左上角开始每次只能向右或者向下走,最后到达右下角的位置,路径上所有的数字累加起来就是路径和,返回所有的路径中最小的路径和。...

Junds0的博客 1268

使用 DeepSeek 配合 即梦AI 生成视频的详细教程

访问即梦AI官网,选择“AI作图”功能,将DeepSeek生成的提示词逐条粘贴至输入框,选择适配的模型(如“通用XL Pro”)和参数(如画幅比例、风格强度)1113。“我要制作一个关于普洱茶文化的宣传视频,请设计一个30秒的脚本,包含时间轴、画面描述、文案和分镜提示词,以表格形式输出。:云南古茶树群,清晨薄雾,金色晨光,露珠特写,国风山水画质感,16:9画幅,使用Nikon D850参数模拟513。:结合DeepSeek生成的知识点脚本与即梦AI的插画风格,制作科普动画211。

weixin_48995695的博客 3万+

【算法】动态规划

动态规划、最长公共子序列

小匠码农 1万+

基本模型 PINNs : Physics Informed Neural Networks

系列最开始当然要提到很经典的文章 —— Physics-informed neural networks: A deep learning framework for solving forward and inverse problems involving nonlinear partial differential equations 。 这篇文章是布朗大学的助理教授Maziar Rais...

waitingwinter的博客 3万+

动态规划算法

动态规划(Dynamic programming),是一种在数学、计算机科学和经济学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划算法是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推(或者说分治)的方式去解决。动态规划对于子问题重叠的情况特别有效,因为它将子问题的解保存在表格中,当需要某个子问题的解时,直接取值即可,从而避免重复计算。动态规划是一种灵活的方法,不存在一种万能的动态规划算法可以解决各类最优化问题(每种算法都有它的缺陷)。

crossoverpptx的博客 6724

ESP32 DMX512与RDM协议栈开发指南

DMX512-A是专业灯光控制领域广泛采用的串行通信标准,基于RS-485物理层实现确定性单向数据传输;RDM(Remote Device Management)则在其基础上扩展双向、可寻址、可配置的智能管理能力,满足现代舞台设备远程固件升级、自动发现与参数配置需求。二者共用同一总线但时序与帧结构严格隔离,对硬件定时精度、中断响应延迟及电气隔离提出极高要求。在嵌入式实践中,ESP32凭借Wi-Fi/蓝牙连接能力与丰富外设资源,成为构建低成本智能DMX节点的理想平台。esp_dmx协议栈正是面向该场景的高性能

weixin_34945060的博客 316

STM32F103VET6实战:用W25Q64外部Flash实现Bootloader远程升级(含完整代码解析)

本文详细介绍了基于STM32F103VET6与W25Q64外部Flash的Bootloader远程升级实现方案,包含硬件选型、存储分区策略、双校验机制及完整代码解析。通过SPI接口实现固件存储与升级,适用于物联网设备开发,提升OTA功能可靠性。

weixin_42620340的博客 356

实验三:动态规划算法

一、实验目的: 1.通过动态规划算法的示例程序理解动态规划算法的基本思想; 2.运用动态规划算法解决实际问题加深对动态规划算法的理解和运用; 二、实验环境:VC++6.0 三、实验内容:1.源代码如下: #include #include #include<string.h> #include<stdio.h> using namespace std; #define ...

lililih的博客 1976

DESeq2实战:如何用LRT检验分析基因表达随时间变化的模式(附完整代码)

本文详细介绍了如何使用DESeq2中的LRT检验分析基因表达随时间变化的模式,包括实验设计、数据预处理、模型构建、结果提取与可视化等完整流程。通过实战案例和完整代码,帮助生物信息学研究者掌握时间序列RNA-seq分析的核心技术,特别适合需要快速上手的实践者。

weixin_42620255的博客 465

24 | 动态规划(上):只需四步,搞定动态规划算法设计

同时,它们也分别代表了学会一个动态规划问题的四个方面。

qq_37756660的博客 1901

【PG高可用】一 PostgreSQL高可用方案repmgr部署(非常详细)

repmgr安装部署

DBA之路的博客 7052

51单片机 步进电机正反转控制+Proteus仿真

51单片机 步进电机正反转控制+Proteus仿真 Proteus仿真 实例代码 /*************** ******************/ #include <reg52.h> #define uint unsigned int #define uchar unsigned char uchar code FFW[]= { 0x01,0x03,0x02,0x06,0x04,0x0c,0x08,0x09 }; uchar code REV[]= { 0x09,0x

perseverance51博客 2787

四大算法:贪心、分治、回溯、动态规划

常见的四种算法:贪心算法、回溯算法、分治、动态规划

qq_45179149的博客 2374

C++怎么学?零基础也可以,一份完整学习路线图助你搞定!

提供一条清晰、系统且实践导向的C++学习路径。从基础语法、面向对象、泛型编程、异常处理,逐步深入STL、智能指针、并发编程等现代C++高级特性。无论是新手还是希望提升,通过系统学习与项目实践,助您将理论知识转化为实际解决问题的能力。

Lion_Long的博客 4022

转:动态规划与贪心算法的区别

本来这次是该总结动态规划的,但在学习过程中发现动态规划和上一节的贪心算法有很大联系,而在算法设计过程中主要是对两种算法的选择,所以决定这次以对比的方式做总结,既可以更深入地了解动态规划,又可以对贪心算法有个新的认识。 背景介绍:这两种算法都是选择性算法,就是从一个候选集合中选择适当的元素加入解集合。 贪心算法的选择策略即贪心选择策略,通过对候选解按照一定的规则进行排序,然后就可以按照这个排好的顺序进行选择了,选择过程中仅需确定当前元素是否要选取,与后面的元素是什么没有关系。 动态规划的选择策略是试探性

2051

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

数据结构与算法学习之路:背包问题的贪心算法和动态规划算法

背包问题的动态规划算法和贪心算法

追风筝的吃货,汪~ 1675

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

五大常用算法之二:动态规划算法

一、基本概念 动态规划过程是:每次决策依赖于当前状态,又随即引起状态的转移。一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划。 二、基本思想与策略 基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可...

weixin_30815427的博客 1719

吃透动态规划:从原理到实战,告别算法恐惧

状态定义是动态规划的核心,也是最容易出错的一步。我们需要用一个数组(或变量)来表示“子问题的答案”,这个数组就叫dp数组,其中dp[i]代表的含义就是“状态”。dp[i] = 爬到第i阶楼梯的不同方法数。很多人拿到题后直接想“递推公式”,却忽略了状态定义。如果dp[i]的含义不明确,后续的转移方程必然出错。解决办法:先把“dp[i]代表什么”写在纸上,确保符合子问题的逻辑。把复杂的原问题拆解成多个重复的子问题,记住子问题的答案,再通过子问题的最优解组合出原问题的解。

2503_94108264的博客 898
上一篇: 深入理解AFDX(ARINC 664 Part7):从原理到实现(下篇)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

打赏作者

不只会拍照的程序猿

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

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

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

打赏作者

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

抵扣说明:

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

余额充值