当前位置: 首页 > news >正文

动态规划中一维与二维DP表的选择:从问题本质到C++实现

        在C++的动态规划中,选择一维或二维DP表的核心依据是 状态变量的数量 和 状态转移的依赖关系。以下是清晰的判断逻辑和具体场景分析:


一、一维DP表

适用条件

  1. 单个状态变量:问题仅需一个参数定义状态(例如:数组下标、时间步、容量等)。

  2. 线性依赖:当前状态仅依赖于 前序的少数状态(如dp[i]依赖dp[i-1]dp[i-2])。

典型问题

  1. 斐波那契数列

    int fib(int n) {
        // 边界条件:如果n小于等于1,直接返回n
        // 因为斐波那契数列的第0项是0,第1项是1
        if (n <= 1) return n;
    
        // 定义一个大小为n+1的一维数组dp,用于存储斐波那契数列的值
        // dp[i]表示第i个斐波那契数
        vector<int> dp(n+1);
    
        // 初始化dp数组的前两项
        // dp[0] = 0:斐波那契数列的第0项是0
        // dp[1] = 1:斐波那契数列的第1项是1
        dp[0] = 0; dp[1] = 1;
    
        // 从第2项开始,逐个计算斐波那契数列的值
        for (int i=2; i<=n; i++) 
            // 状态转移方程:当前项等于前两项之和
            // dp[i] = dp[i-1] + dp[i-2]
            dp[i] = dp[i-1] + dp[i-2];
    
        // 返回第n个斐波那契数
        return dp[n];
    }
  2. 爬楼梯问题(每次爬1或2步):

    int climbStairs(int n) {
        // 定义一个大小为n+1的一维数组dp,用于存储爬楼梯问题的解
        // dp[i]表示爬到第i阶楼梯的方法数
        vector<int> dp(n+1);
    
        // 初始化dp数组的前两项
        // dp[0] = 1:爬到第0阶楼梯的方法数为1(理解为起点)
        // dp[1] = 1:爬到第1阶楼梯的方法数为1(只有一种方式:爬1步)
        dp[0] = 1; dp[1] = 1;
    
        // 从第2阶开始,逐个计算爬到每阶楼梯的方法数
        for (int i=2; i<=n; i++)
            // 状态转移方程:爬到第i阶楼梯的方法数等于爬到第i-1阶和第i-2阶的方法数之和
            // 因为每次可以爬1步或2步
            dp[i] = dp[i-1] + dp[i-2];
    
        // 返回爬到第n阶楼梯的方法数
        return dp[n];
    }
  3. 0-1背包问题(空间优化版)(注意:原始0-1背包是二维,但可用一维优化):

    int knapsack(vector<int>& weights, vector<int>& values, int capacity) {
        // 定义一个大小为capacity+1的一维数组dp,初始化为0
        // dp[j]表示背包容量为j时的最大价值
        vector<int> dp(capacity + 1, 0);
    
        // 遍历每个物品
        for (int i=0; i<weights.size(); i++) 
            // 逆序遍历背包容量,从最大容量到当前物品的重量
            // 逆序更新是为了确保每个物品只被使用一次(0-1背包问题)
            for (int j=capacity; j>=weights[i]; j--) 
                // 状态转移方程:选择是否将当前物品放入背包
                // dp[j]表示不放入当前物品时的最大价值
                // dp[j - weights[i]] + values[i]表示放入当前物品时的最大价值
                dp[j] = max(dp[j], dp[j - weights[i]] + values[i]);
    
        // 返回背包容量为capacity时的最大价值
        return dp[capacity];
    }

二、二维DP表

适用条件

  1. 两个状态变量:问题需要两个参数定义状态(例如:两个序列的索引、二维坐标等)。

  2. 多维依赖:当前状态依赖多个方向的状态(如左、上、左上等)。

典型问题

  1. 最长公共子序列(LCS)

    int lcs(string& s1, string& s2) {
        // 获取两个字符串的长度
        int m = s1.size(), n = s2.size();
    
        // 定义一个大小为(m+1) x (n+1)的二维数组dp,初始化为0
        // dp[i][j]表示s1的前i个字符和s2的前j个字符的最长公共子序列长度
        vector<vector<int>> dp(m+1, vector<int>(n+1, 0));
    
        // 遍历s1的每个字符
        for (int i=1; i<=m; i++) {
            // 遍历s2的每个字符
            for (int j=1; j<=n; j++) {
                // 如果当前字符相等
                if (s1[i-1] == s2[j-1]) 
                    // 状态转移方程:当前字符相等时,LCS长度加1
                    dp[i][j] = dp[i-1][j-1] + 1;
                else 
                    // 状态转移方程:当前字符不相等时,取左边或上边的最大值
                    dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
            }
        }
    
        // 返回s1和s2的最长公共子序列长度
        return dp[m][n];
    }
  2. 编辑距离

    int minDistance(string s1, string s2) {
        // 获取两个字符串的长度
        int m = s1.size(), n = s2.size();
    
        // 定义一个大小为(m+1) x (n+1)的二维数组dp,初始化为0
        // dp[i][j]表示将s1的前i个字符转换为s2的前j个字符所需的最小操作数
        vector<vector<int>> dp(m+1, vector<int>(n+1, 0));
    
        // 初始化dp表的第一列:将s1的前i个字符转换为空字符串需要i次删除操作
        for (int i=0; i<=m; i++) dp[i][0] = i;
    
        // 初始化dp表的第一行:将空字符串转换为s2的前j个字符需要j次插入操作
        for (int j=0; j<=n; j++) dp[0][j] = j;
    
        // 遍历s1的每个字符
        for (int i=1; i<=m; i++) {
            // 遍历s2的每个字符
            for (int j=1; j<=n; j++) {
                // 如果当前字符相等
                if (s1[i-1] == s2[j-1]) 
                    // 状态转移方程:当前字符相等时,不需要操作,直接继承dp[i-1][j-1]
                    dp[i][j] = dp[i-1][j-1];
                else 
                    // 状态转移方程:当前字符不相等时,取插入、删除、替换操作的最小值,并加1
                    dp[i][j] = 1 + min({dp[i-1][j], dp[i][j-1], dp[i-1][j-1]});
            }
        }
    
        // 返回将s1转换为s2所需的最小操作数
        return dp[m][n];
    }
  3. 矩阵路径问题(带障碍物)

    int uniquePathsWithObstacles(vector<vector<int>>& grid) {
        // 获取网格的行数和列数
        int m = grid.size(), n = grid[0].size();
    
        // 定义一个大小为m x n的二维数组dp,初始化为0
        // dp[i][j]表示从起点(0,0)到点(i,j)的路径数量
        vector<vector<int>> dp(m, vector<int>(n, 0));
    
        // 初始化起点(0,0)的路径数量
        // 如果起点不是障碍物(grid[0][0] == 0),则dp[0][0] = 1,否则为0
        dp[0][0] = grid[0][0] == 0 ? 1 : 0;
    
        // 遍历网格的每个点
        for (int i=0; i<m; i++) {
            for (int j=0; j<n; j++) {
                // 如果当前点是障碍物,跳过
                if (grid[i][j] == 1) continue;
    
                // 如果上方有格子(i>0),则从上方格子到达当前点的路径数累加到dp[i][j]
                if (i>0) dp[i][j] += dp[i-1][j];
    
                // 如果左方有格子(j>0),则从左方格子到达当前点的路径数累加到dp[i][j]
                if (j>0) dp[i][j] += dp[i][j-1];
            }
        }
    
        // 返回到达终点(m-1,n-1)的路径数量
        return dp[m-1][n-1];
    }

三、如何快速判断维度?

  1. 分析问题的状态定义

    • 若问题需要同时跟踪 两个独立的变量(如两个字符串的位置、二维坐标),用二维。

    • 若问题只需一个变量(如时间、容量),用一维。

  2. 观察状态转移方程

    • 若转移方程涉及 dp[i][j] 依赖 dp[i-1][j-1] 或 dp[i][j-1],必须用二维。

    • 若转移方程仅依赖 dp[i-1] 或 dp[i-2],可以尝试一维。

  3. 特殊情况

    • 有些二维问题可以通过 滚动数组 优化为一维(如0-1背包)。

    • 但若状态依赖关系复杂(如需要左上角的值),则无法优化。


四、总结

维度适用场景示例问题状态依赖
一维DP单一状态变量,线性依赖斐波那契、爬楼梯、0-1背包优化dp[i]依赖dp[i-1]
二维DP两个状态变量,多维依赖LCS、编辑距离、矩阵路径dp[i][j]依赖左、上、左上

五、一维DP表示例:斐波那契数列

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

int fibonacci(int n) {
    if (n <= 1) return n; // 边界条件:斐波那契数列的前两项为0和1

    vector<int> dp(n + 1, 0); // 定义一维DP表,dp[i]表示第i个斐波那契数
    dp[0] = 0; // 初始化第0项
    dp[1] = 1; // 初始化第1项

    for (int i = 2; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2]; // 状态转移方程:当前项等于前两项之和
    }

    return dp[n]; // 返回第n项的结果
}

int main() {
    int n = 10;
    cout << "Fibonacci(" << n << ") = " << fibonacci(n) << endl; // 输出结果
    return 0;
}

代码解析

  1. 边界条件

    • 斐波那契数列的前两项是固定的:dp[0] = 0dp[1] = 1

    • 如果n <= 1,直接返回n,避免不必要的计算。

  2. DP表定义

    • vector<int> dp(n + 1, 0):定义一个大小为n+1的一维数组,初始值为0。

    • dp[i]表示第i个斐波那契数。

  3. 状态转移方程

    • dp[i] = dp[i - 1] + dp[i - 2]:当前项等于前两项之和。

    • 这是斐波那契数列的核心递推关系。

  4. 空间优化

    • 由于dp[i]只依赖于dp[i-1]dp[i-2],可以进一步优化为只使用两个变量,而不是整个数组。


六、二维DP表示例:最长公共子序列(LCS)

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

int lcs(string& s1, string& s2) {
    int m = s1.size(), n = s2.size(); // 获取两个字符串的长度
    vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0)); // 定义二维DP表,dp[i][j]表示s1[0..i-1]和s2[0..j-1]的LCS长度

    for (int i = 1; i <= m; i++) { // 遍历s1的每个字符
        for (int j = 1; j <= n; j++) { // 遍历s2的每个字符
            if (s1[i - 1] == s2[j - 1]) { // 如果当前字符相等
                dp[i][j] = dp[i - 1][j - 1] + 1; // 状态转移:LCS长度加1
            } else { // 如果当前字符不相等
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]); // 状态转移:取左边或上边的最大值
            }
        }
    }

    return dp[m][n]; // 返回最终结果
}

int main() {
    string s1 = "abcde";
    string s2 = "ace";
    cout << "LCS length: " << lcs(s1, s2) << endl; // 输出结果
    return 0;
}

代码解析

  1. DP表定义

    • vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0)):定义一个大小为(m+1) x (n+1)的二维数组,初始值为0。

    • dp[i][j]表示字符串s1的前i个字符和s2的前j个字符的最长公共子序列长度。

  2. 状态转移方程

    • 如果s1[i-1] == s2[j-1],则dp[i][j] = dp[i-1][j-1] + 1

    • 如果s1[i-1] != s2[j-1],则dp[i][j] = max(dp[i-1][j], dp[i][j-1])

  3. 初始化

    • dp[0][j]dp[i][0]都初始化为0,因为空字符串与任何字符串的LCS长度为0。

  4. 遍历顺序

    • 外层循环遍历s1,内层循环遍历s2,确保每个子问题都被正确计算。

  5. 结果

    • dp[m][n]即为两个字符串的最长公共子序列长度。


七、动态规划的核心知识点

  1. 状态定义

    • 明确dp[i]dp[i][j]表示什么。例如:

      • 斐波那契数列:dp[i]表示第i个斐波那契数。

      • LCS问题:dp[i][j]表示s1[0..i-1]s2[0..j-1]的最长公共子序列长度。

  2. 状态转移方程

    • 描述当前状态与之前状态的关系。例如:

      • 斐波那契数列:dp[i] = dp[i-1] + dp[i-2]

      • LCS问题:dp[i][j] = dp[i-1][j-1] + 1(如果字符相等),否则dp[i][j] = max(dp[i-1][j], dp[i][j-1])

  3. 初始化

    • 根据问题的边界条件初始化DP表。例如:

      • 斐波那契数列:dp[0] = 0dp[1] = 1

      • LCS问题:dp[0][j] = 0dp[i][0] = 0

  4. 遍历顺序

    • 确保在计算dp[i][j]时,所依赖的状态已经计算完成。

  5. 空间优化

    • 如果状态转移只依赖于有限的前几个状态,可以优化空间复杂度。例如:

      • 斐波那契数列:用两个变量代替整个数组。

      • 0-1背包问题:用一维数组代替二维数组。


八、总结

  • 一维DP表:适用于状态变量单一、依赖关系简单的问题(如斐波那契数列、爬楼梯问题)。

  • 二维DP表:适用于状态变量有两个、依赖关系复杂的问题(如LCS、编辑距离)。

  • 核心步骤:定义状态、写出状态转移方程、初始化、确定遍历顺序、优化空间(可选)。

相关文章:

  • STM32_GPIO系统外设学习
  • C++学习——栈(一)
  • linux centos8 安装redis 卸载redis
  • 分布式锁—7.Curator的分布式锁
  • 在昇腾GPU上部署DeepSeek大模型与OpenWebUI:从零到生产的完整指南
  • java调用c++
  • Unity--Cubism Live2D模型使用
  • 使用Simulink搭建无人机串级PI控制的步骤
  • 创新算法!BKA-Transformer-BiLSTM黑翅鸢优化算法多变量时间序列预测
  • vue-cli3+vue2+elementUI+avue升级到vite+vue3+elementPlus+avue总结
  • 从离散迭代到连续 常微分方程(Ordinary Differential Equation, ODE):梯度流
  • OpenManus:解锁测试工程师的效率密码——实践与应用指南
  • 【linux网络编程】套接字编程API详细介绍
  • 2025年主流原型工具测评:墨刀、Axure、Figma、Sketch
  • 广度优先遍历(BFS):逐层探索的智慧
  • 密码学系列 - 利用CPU指令加速
  • 【多源BFS问题】01 矩阵
  • ESP8266 NodeMCU 与 Atmega16 微控制器连接以发送电子邮件
  • c#面试题整理
  • 静态时序分析:SDC约束命令set_ideal_latency详解
  • 青浦网站制作su35/百度一下就知道
  • 苏州企业建设网站公司/在线域名解析ip地址
  • 个人网站建站/磁力搜索引擎torrentkitty
  • 太原网站建设维护/链接生成二维码
  • 个人网站的制作代码/百度资源搜索资源平台
  • 做网站一天能赚多少钱/seo博客写作