c++动态规划算法详解

一、动态规划的核心本质

动态规划(Dynamic Programming,简称 DP)是一种将复杂问题分解为若干个重叠的子问题,通过求解子问题的最优解来推导原问题最优解的算法思想。

1. 核心特征(必须满足)

  • 最优子结构:原问题的最优解包含其子问题的最优解(子问题的最优解能推导出原问题的最优解)。
  • 重叠子问题:求解原问题时会反复用到同一个子问题的解(这是 DP 能优化的关键,避免重复计算)。
  • 无后效性:某一阶段的状态确定后,不受后续决策的影响(当前状态只和过去有关,和未来无关)。

2. DP vs 暴力递归(核心区别)

特性 暴力递归 动态规划
子问题处理 重复计算相同子问题 记录子问题解(记忆化)
时间复杂度 指数级(如O(2n)) 多项式级(如O(n))
空间复杂度 递归栈开销 额外空间存储子问题解

二、动态规划的解题四步走(核心方法论)

掌握这四步,就能解决绝大多数 DP 问题,逻辑清晰且不易出错:

步骤 1:定义状态(最关键)

  • dp[i]/dp[i][j]表示 "以 i 为结尾 / 在 i,j 状态下的最优解 / 满足条件的结果"。
  • 状态定义的核心:让子问题能够通过状态关联起来

步骤 2:推导状态转移方程(核心逻辑)

  • 描述 "大问题如何由小问题推导而来",是 DP 的核心公式。
  • 例如:dp[i] = dp[i-1] + dp[i-2](斐波那契数列)。

步骤 3:初始化状态(避免边界错误)

  • 确定 DP 数组的初始值(边界条件),是计算的起点。
  • 例如:斐波那契数列中dp[0]=0, dp[1]=1

步骤 4:确定遍历顺序(保证计算顺序正确)

  • 确保计算dp[i]时,其依赖的dp[i-1]/dp[i-2]已经被计算过。

三、经典案例(C++ 实现,从易到难)

案例 1:斐波那契数列(入门级)

问题描述

求斐波那契数列的第 n 项,定义:F(0)=0,F(1)=1,F(n)=F(n−1)+F(n−2)(n≥2)。

解题步骤
  1. 状态定义dp[i]表示第 i 项斐波那契数。
  2. 转移方程dp[i] = dp[i-1] + dp[i-2](i≥2)。
  3. 初始化dp[0]=0, dp[1]=1
  4. 遍历顺序:从 2 到 n 依次计算。
C++ 代码实现
cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

// 普通版
int fib(int n) {
    if(n < 0) return -1;
    if(n == 0) return 0;
    if(n == 1) return 1;
    
    vector<int> dp(n+1);
    dp[0] = 0;
    dp[1] = 1;
    
    for(int i=2; i<=n; i++) {
        dp[i] = dp[i-1] + dp[i-2];
    }
    return dp[n];
}

// 空间优化版
int fib_opt(int n) {
    if(n < 0) return -1;
    if(n == 0) return 0;
    if(n == 1) return 1;
    
    int a=0, b=1, res;
    for(int i=2; i<=n; i++) {
        res = a + b;
        a = b;
        b = res;
    }
    return res;
}

int main() {
    int n=10;
    cout << fib(n) << endl;    // 55
    cout << fib_opt(n) << endl;// 55
    return 0;
}
代码解释
  • 普通版:用vector存储所有子问题的解,直观但占用 O (n) 空间。
  • 优化版:由于dp[i]只依赖dp[i-1]dp[i-2],只需用两个变量滚动更新,空间复杂度降为 O (1)(DP 常见优化技巧)。

案例 2:爬楼梯(基础应用)

问题描述

假设你正在爬楼梯,需要 n 阶才能到达楼顶。每次可以爬 1 阶或 2 阶,问有多少种不同的方法爬到楼顶?

解题步骤
  1. 状态定义dp[i]表示爬到第 i 阶的方法数。
  2. 转移方程dp[i] = dp[i-1] + dp[i-2](最后一步要么爬 1 阶,要么爬 2 阶)。
  3. 初始化dp[1]=1(1 阶只有 1 种方法),dp[2]=2(2 阶有两种:1+1 或 2)。
  4. 遍历顺序:从 3 到 n 依次计算。
C++ 代码实现
cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

int climb(int n) {
    if(n <= 0) return 0;
    if(n == 1) return 1;
    if(n == 2) return 2;
    
    vector<int> dp(n+1);
    dp[1] = 1;
    dp[2] = 2;
    
    for(int i=3; i<=n; i++) {
        dp[i] = dp[i-1] + dp[i-2];
    }
    return dp[n];
}

int main() {
    int n=5;
    cout << climb(n) << endl; // 8
    return 0;
}

案例 3:最大子数组和(中等难度,经典 DP)

问题描述

给定一个整数数组nums,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

解题步骤
  1. 状态定义dp[i]表示以第 i 个元素结尾的连续子数组的最大和。
  2. 转移方程dp[i] = max(nums[i], dp[i-1] + nums[i])(要么从当前元素重新开始,要么接上前面的子数组)。
  3. 初始化dp[0] = nums[0](第一个元素的最大子数组和就是自己)。
  4. 遍历顺序:从 1 到 n-1 依次计算,同时记录全局最大值。
C++ 代码实现
cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

int maxSub(int a[], int n) {
    if(n == 0) return 0;
    
    vector<int> dp(n);
    dp[0] = a[0];
    int mx = dp[0];
    
    for(int i=1; i<n; i++) {
        dp[i] = max(a[i], dp[i-1]+a[i]);
        mx = max(mx, dp[i]);
    }
    return mx;
}

// 空间优化版
int maxSub_opt(int a[], int n) {
    if(n == 0) return 0;
    
    int pre = a[0], mx = pre;
    for(int i=1; i<n; i++) {
        pre = max(a[i], pre+a[i]);
        mx = max(mx, pre);
    }
    return mx;
}

int main() {
    int a[] = {-2,1,-3,4,-1,2,1,-5,4};
    int n = sizeof(a)/sizeof(a[0]);
    cout << maxSub(a, n) << endl;    // 6
    cout << maxSub_opt(a, n) << endl;// 6
    return 0;
}
关键解释
  • 转移方程的逻辑:如果dp[i-1] + nums[i]nums[i]小,说明前面的子数组是 "拖后腿" 的,不如直接从nums[i]重新开始。
  • 全局最大值:dp[i]只表示以 i 结尾的最大和,最终答案需要遍历所有dp[i]找最大值。

案例 4:0-1 背包问题(进阶核心)

问题描述

有 n 件物品和一个容量为 V 的背包。第 i 件物品的重量是w[i],价值是v[i]。每件物品只能选一次,问将哪些物品装入背包,可使总价值最大?

解题步骤
  1. 状态定义dp[i][j]表示前 i 件物品,在背包容量为 j 时的最大价值。
  2. 转移方程
    • 不选第 i 件物品:dp[i][j] = dp[i-1][j]
    • 选第 i 件物品(前提 j≥w [i]):dp[i][j] = dp[i-1][j - w[i]] + v[i]
    • 最终:dp[i][j] = max(不选, 选)
  3. 初始化
    • dp[0][j] = 0(0 件物品,价值为 0)
    • dp[i][0] = 0(容量为 0,价值为 0)
  4. 遍历顺序:先遍历物品(1 到 n),再遍历容量(1 到 V)。
C++ 代码实现
cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

// 二维版
int bag(int w[], int v[], int n, int V) {
    vector<vector<int>> dp(n+1, vector<int>(V+1, 0));
    
    for(int i=1; i<=n; i++) {
        for(int j=1; j<=V; j++) {
            if(j < w[i-1]) {
                dp[i][j] = dp[i-1][j];
            } else {
                dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i-1]] + v[i-1]);
            }
        }
    }
    return dp[n][V];
}

// 一维优化版
int bag_opt(int w[], int v[], int n, int V) {
    vector<int> dp(V+1, 0);
    
    for(int i=0; i<n; i++) {
        for(int j=V; j>=w[i]; j--) {
            dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
        }
    }
    return dp[V];
}

int main() {
    int w[] = {2,3,4,5};  // 重量
    int v[] = {3,4,5,6};  // 价值
    int n = sizeof(w)/sizeof(w[0]);
    int V = 8;            // 容量
    cout << bag(w, v, n, V) << endl;    // 10
    cout << bag_opt(w, v, n, V) << endl;// 10
    return 0;
}
关键解释
  • 一维数组优化的核心:倒序遍历容量,避免同一个物品被多次选择(如果正序,dp[j - w[i]]已经被更新,会重复选当前物品,变成完全背包)。
  • 0-1 背包是 DP 的核心模型,很多问题(如分割等和子集、目标和)都可以转化为背包问题。

四、动态规划的常见优化技巧

  1. 空间优化
    • 滚动数组:如斐波那契、爬楼梯的 O (1) 空间优化。
    • 一维数组代替二维数组:如 0-1 背包的一维 DP。
  2. 状态压缩:合并重复状态,减少 DP 数组维度。
  3. 单调队列优化:针对某些区间最值的转移方程(如滑动窗口最大值类 DP)。

五、如何判断一个问题是否能用 DP 解决?

  1. 问题具有最优子结构(求最值、计数类问题);
  2. 问题存在重叠子问题(暴力递归会重复计算);
  3. 问题满足无后效性(当前状态不受未来决策影响)。

总结

  1. 核心思想:动态规划的本质是 "分解子问题 + 记录子问题解 + 推导原问题解",核心是避免重复计算。
  2. 解题四步:定义状态 → 推导转移方程 → 初始化 → 确定遍历顺序(这是解决所有 DP 问题的通用框架)。
  3. C++ 实现要点
    • 优先用vector存储 DP 数组,避免数组越界;
    • 先写直观的二维 DP,再优化为一维(空间);
    • 注意边界条件(如 n=0、容量 = 0 等)和数组下标(0/1 起始)。

掌握这些内容后,你可以从简单 DP 问题入手,逐步挑战中等、困难级别的题目(如最长递增子序列、编辑距离、完全背包等),核心是多练、多总结状态定义和转移方程的规律。

相关推荐
清 澜2 小时前
深度学习连续剧——手搓梯度下降法
c++·人工智能·面试·职场和发展·梯度
不想看见4042 小时前
Single Number位运算基础问题--力扣101算法题解笔记
数据结构·算法
靠沿2 小时前
【优选算法】专题十二——栈
算法
愚者游世2 小时前
<algorithm> 中 remove、remove_if、remove_copy、remove_copy_if 详解
c++·学习·程序人生·职场和发展·visual studio
无心水2 小时前
【任务调度:框架】10、2026最新!分布式任务调度选型决策树:再也不纠结选哪个
人工智能·分布式·算法·决策树·机器学习·架构·2025博客之星
我头发还没掉光~3 小时前
【C++写详细总结①】从for循环到算法初步
数据结构·c++·算法
【数据删除】3483 小时前
计算机复试学习笔记 Day41
笔记·学习·算法
上海锟联科技3 小时前
什么是DAS分布式光纤声波传感系统?原理与应用解析
数据结构·分布式·算法·分布式光纤传感
篮l球场3 小时前
LRU 缓存
算法·leetcode