欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 财经 > 金融 > 动态规划DP

动态规划DP

2025/6/13 20:47:52 来源:https://blog.csdn.net/m0_70403090/article/details/148596535  浏览:    关键词:动态规划DP

斐波那契数列模型

斐波那契数列是动态规划的入门模型,其核心思想是当前状态由前几个状态线性组合而成。

  • 状态定义:通常使用一维 dp 数组,dp[i] 表示第 i 个位置的数值。
  • 状态转移方程dp[i] = dp[i-1] + dp[i-2] + ...
  • 初始化:根据递推公式,需要预先定义好前几个无法被推导出的状态值。

经典例题:第 N 个泰波那契数 (LeetCode 1137)

泰波那契序列 Tn 定义为:T0=0,T1=1,T2=1,且在 n≥0 的条件下 Tn+3=Tn+Tn+1+Tn+2。

C++ 优化代码 (滚动数组/状态压缩)

对于斐波那契类型的题目,由于 dp[i] 的计算只依赖于前几个固定状态,可以使用滚动数组(状态压缩)将空间复杂度从 O(N) 优化到 O(1)。

C++

#include <iostream>class Solution {
public:int tribonacci(int n) {if (n == 0) return 0;if (n == 1 || n == 2) return 1;// 使用状态压缩,仅保留最近的三个状态int a = 0, b = 1, c = 1;int d; // 用于计算新状态for (int i = 3; i <= n; ++i) {d = a + b + c;// 状态向前滚动a = b;b = c;c = d;}return c; // 最终结果存储在 c 中}
};// int main() {
//     Solution sol;
//     std::cout << sol.tribonacci(25) << std::endl; // 输出: 1389537
//     return 0;
// }

路径问题

路径问题通常涉及在二维网格中,从起点到终点寻找特定路径(如总数、最小代价等)。

  • 状态定义dp[i][j] 通常表示到达网格位置 (i, j) 时的路径属性(如路径总数)。
  • 状态转移方程dp[i][j] 的值通常由其上方 dp[i-1][j] 和左方 dp[i][j-1] 的状态转移而来。
  • 初始化:通常需要初始化网格的边界条件,例如第一行和第一列的路径值。

经典例题:不同路径 (LeetCode 62)

一个机器人位于一个 m x n 网格的左上角,机器人每次只能向下或向右移动一步。问总共有多少条不同的路径可以到达网格的右下角。

C++ 优化代码 (空间优化)

观察到 dp[i][j] 的计算只依赖于当前行和上一行的数据,因此可以将二维 dp 数组压缩为一维。

C++

#include <iostream>
#include <vector>class Solution {
public:int uniquePaths(int m, int n) {// 空间优化:使用一维数组std::vector<int> dp(n, 1); // 初始化第一行全为1for (int i = 1; i < m; ++i) {for (int j = 1; j < n; ++j) {// 当前格的路径数 = 上方格的路径数 + 左方格的路径数// dp[j] 在更新前是上一行的值 (dp[i-1][j])// dp[j-1] 是当前行前一列的值 (dp[i][j-1])dp[j] = dp[j] + dp[j - 1];}}return dp[n - 1];}
};// int main() {
//     Solution sol;
//     std::cout << sol.uniquePaths(3, 7) << std::endl; // 输出: 28
//     return 0;
// }

简单多状态 DP

当单个状态无法完整描述问题时,需要定义多个状态。这类问题中,每个位置 i 可能处于不同的状态(例如,持有/不持有,接/不接),需要用多个 dp 数组或 dp 数组的多个维度来表示。

  • 状态定义dp[i][state] 表示在位置 i 处于 state 状态时的最优值。例如,f[i] 表示在 i 处进行某操作的最优值,g[i] 表示在 i 处不进行该操作的最优值。
  • 状态转移方程:根据不同状态间的依赖关系分别建立转移方程。
  • 初始化:根据问题的初始条件,初始化 dp 数组在 i=0 时的各个状态值。

经典例题:按摩师 (LeetCode 面试题 17.16)

一个按摩师不能接受相邻的预约。给定一个代表预约时长的数组,求能够获得的最长总预约时长。

C++ 优化代码 (状态压缩)

由于 dp[i] 只依赖于 dp[i-1],可以将空间复杂度优化到 O(1)。

C++

#include <iostream>
#include <vector>
#include <algorithm>class Solution {
public:int massage(std::vector<int>& nums) {if (nums.empty()) {return 0;}if (nums.size() == 1) {return nums[0];}// f: 接受当前预约的最大时长// g: 不接受当前预约的最大时长int g = 0, f = nums[0];for (size_t i = 1; i < nums.size(); ++i) {int temp_g = std::max(f, g); // 新的g[i]int temp_f = g + nums[i];   // 新的f[i]g = temp_g;f = temp_f;}return std::max(f, g);}
};// int main() {
//     Solution sol;
//     std::vector<int> nums = {2, 7, 9, 3, 1};
//     std::cout << sol.massage(nums) << std::endl; // 输出: 12
//     return 0;
// }

子数组/子串系列

子数组或子串问题要求处理连续的元素片段。

  • 状态定义dp[i] 通常表示以 nums[i] 结尾的子数组所具有的某种性质(如最大和、最大乘积)。
  • 状态转移方程dp[i] 的值通常与 dp[i-1]nums[i] 本身有关。
  • 返回值:最终结果不一定是 dp[n-1],因为最大/最优的子数组可能在任何位置结束,所以需要在遍历过程中记录全局最优解。

经典例题:最大子数组和 (LeetCode 53)

找出一个具有最大和的连续子数组,并返回其最大和。

C++ 优化代码 (状态压缩)

dp[i] 的计算只依赖于 dp[i-1],可以进行状态压缩。

C++

#include <iostream>
#include <vector>
#include <algorithm>class Solution {
public:int maxSubArray(std::vector<int>& nums) {if (nums.empty()) return 0;int max_so_far = nums[0];int current_max = nums[0];for (size_t i = 1; i < nums.size(); ++i) {// 状态转移:决定是“自立门户”还是“加入组织”// 如果前一个子数组的和是负数,则不如从当前元素开始重新计算current_max = std::max(nums[i], current_max + nums[i]);// 更新全局最大值max_so_far = std::max(max_so_far, current_max);}return max_so_far;}
};// int main() {
//     Solution sol;
//     std::vector<int> nums = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
//     std::cout << sol.maxSubArray(nums) << std::endl; // 输出: 6
//     return 0;
// }

子序列问题

子序列问题与子数组不同,它处理的元素不要求连续

  • 状态定义dp[i] 通常表示以 nums[i] 结尾的子序列所具有的性质(例如,最长递增子序列的长度)。
  • 状态转移方程dp[i] 的计算通常需要遍历 j0i-1 的所有 dp[j],找出满足条件的 dp[j] 来更新 dp[i]
  • 初始化:通常将 dp 数组的所有元素初始化为一个基本值(如 1)。

经典例题:最长递增子序列 (LeetCode 300)

找到一个整数数组中最长严格递增子序列的长度。

C++ 优化代码 (贪心 + 二分查找)

此题存在一个更优的解法,时间复杂度为 O(NlogN)。维护一个“耐心排序”的 tails 数组,tails[i] 存储长度为 i+1 的所有递增子序列中末尾元素的最小值。

C++

#include <iostream>
#include <vector>
#include <algorithm>class Solution {
public:int lengthOfLIS(std::vector<int>& nums) {if (nums.empty()) return 0;std::vector<int> tails;tails.push_back(nums[0]);for (size_t i = 1; i < nums.size(); ++i) {if (nums[i] > tails.back()) {// 如果当前元素比所有已知递增子序列的末尾都大,// 它可以扩展最长的那个序列,形成一个更长的序列。tails.push_back(nums[i]);} else {// 否则,在 tails 数组中找到第一个大于或等于 nums[i] 的元素,// 并用 nums[i] 替换它。这表示我们找到了一个长度相同但末尾更小的递增子序列。auto it = std::lower_bound(tails.begin(), tails.end(), nums[i]);*it = nums[i];}}return tails.size();}
};// int main() {
//     Solution sol;
//     std::vector<int> nums = {10, 9, 2, 5, 3, 7, 101, 18};
//     std::cout << sol.lengthOfLIS(nums) << std::endl; // 输出: 4
//     return 0;
// }

回文串问题

回文串问题关注字符串中正读和反读都相同的子串或子序列。

  • 状态定义:通常使用二维 dp 数组,dp[i][j] 表示字符串 s 中从索引 ij 的子串 s[i...j] 是否为回文串。
  • 状态转移方程dp[i][j] 的真假取决于 s[i] == s[j] 以及 dp[i+1][j-1] 是否为真。
  • 填表顺序:由于 dp[i][j] 依赖于 dp[i+1][j-1](左下角的值),遍历时需要保证左下角的值先被计算。通常采用从下到上、从左到右的遍历顺序。

经典例题:最长回文子串 (LeetCode 5)

找到一个字符串中最长的回文子串。

C++ 优化代码 (中心扩展法)

动态规划的空间和时间复杂度均为 O(N2)。中心扩展法空间复杂度更优,为 O(1)。它以每个字符或两个字符之间为中心,向两边扩展,寻找最长的回文串。

C++

#include <iostream>
#include <string>
#include <algorithm>class Solution {
public:std::string longestPalindrome(std::string s) {if (s.length() < 1) return "";int start = 0, max_len = 1;for (int i = 0; i < s.length(); ++i) {// 奇数长度的回文串int len1 = expandAroundCenter(s, i, i);// 偶数长度的回文串int len2 = expandAroundCenter(s, i, i + 1);int len = std::max(len1, len2);if (len > max_len) {start = i - (len - 1) / 2;max_len = len;}}return s.substr(start, max_len);}private:int expandAroundCenter(const std::string& s, int left, int right) {while (left >= 0 && right < s.length() && s[left] == s[right]) {left--;right++;}return right - left - 1;}
};// int main() {
//     Solution sol;
//     std::cout << sol.longestPalindrome("babad") << std::endl; // 输出: "bab" 或 "aba"
//     return 0;
// }

两个数组的 DP 问题

这类问题涉及两个输入数组(或字符串),状态定义需要同时考虑两个数组的索引。

  • 状态定义dp[i][j] 表示第一个数组的前 i 个元素和第二个数组的前 j 个元素之间的某种关系(如最长公共子序列长度)。
  • 状态转移方程dp[i][j] 的计算通常依赖于 dp[i-1][j]dp[i][j-1]dp[i-1][j-1]
  • 初始化:通常需要处理一个或两个数组为空的情况,这可以通过扩展 dp 表的大小(增加一行一列)来简化。

经典例题:最长公共子序列 (LeetCode 1143)

返回两个字符串的最长公共子序列的长度。

C++ 优化代码 (空间优化)

dp[i][j] 的计算只依赖于上一行和当前行的数据,可以将空间复杂度从 O(MN) 优化到 O(min(M,N))。

C++

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>class Solution {
public:int longestCommonSubsequence(std::string text1, std::string text2) {int m = text1.length();int n = text2.length();// 确保 dp 数组大小为较小维度,节省空间if (m < n) {return longestCommonSubsequence(text2, text1);}std::vector<int> dp(n + 1, 0);for (int i = 1; i <= m; ++i) {int prev_row_prev_col = 0; // 模拟 dp[i-1][j-1]for (int j = 1; j <= n; ++j) {int temp = dp[j]; // 临时保存 dp[i-1][j]if (text1[i - 1] == text2[j - 1]) {dp[j] = prev_row_prev_col + 1;} else {dp[j] = std::max(dp[j], dp[j - 1]);}prev_row_prev_col = temp;}}return dp[n];}
};// int main() {
//     Solution sol;
//     std::cout << sol.longestCommonSubsequence("abcde", "ace") << std::endl; // 输出: 3
//     return 0;
// }

0/1 背包问题

给定一组物品,每个物品只有一个,有各自的重量和价值。在限定的总重量内,如何选择物品使得总价值最高。

  • 状态定义dp[i][j] 表示从前 i 个物品中选择,放入容量为 j 的背包中所能获得的最大价值。
  • 状态转移方程dp[i][j] = max(dp[i-1][j], dp[i-1][j - weight[i]] + value[i])。这代表两种决策:不放第 i 个物品,或放第 i 个物品。

经典例题:分割等和子集 (LeetCode 416)

判断一个只包含正整数的非空数组是否可以被分割成两个子集,使得两个子集的元素和相等。这可以转化为一个 0/1 背包问题:能否从数组中选出一些数,其和恰好等于数组总和的一半。

C++ 优化代码 (空间优化)

可以将 dp 数组压缩到一维。关键在于内层循环必须从后往前遍历,以保证在计算 dp[j] 时,所依赖的 dp[j - weight[i]] 仍然是上一轮(i-1)的状态。

C++

#include <iostream>
#include <vector>
#include <numeric>class Solution {
public:bool canPartition(std::vector<int>& nums) {int sum = std::accumulate(nums.begin(), nums.end(), 0);if (sum % 2 != 0) return false;int target = sum / 2;std::vector<bool> dp(target + 1, false);dp[0] = true;for (int num : nums) {for (int j = target; j >= num; --j) {// dp[j] = dp[j] (不选num) || dp[j - num] (选num)dp[j] = dp[j] || dp[j - num];}}return dp[target];}
};// int main() {
//     Solution sol;
//     std::vector<int> nums = {1, 5, 11, 5};
//     std::cout << std::boolalpha << sol.canPartition(nums) << std::endl; // 输出: true
//     return 0;
// }

完全背包问题

与 0/1 背包不同,完全背包问题中的每种物品可以无限次地选取。

  • 状态定义:同 0/1 背包。
  • 状态转移方程dp[i][j] = max(dp[i-1][j], dp[i][j - weight[i]] + value[i])。注意,第二个选项是 dp[i] 而非 dp[i-1],这表示在考虑第 i 个物品时,我们可以在已经放过第 i 个物品的背包中继续放。

经典例题:零钱兑换 (LeetCode 322)

给你一个整数数组 coins 表示不同面额的硬币,以及一个整数 amount 表示总金额。计算并返回可以凑成总金额所需的最少的硬币个数。

C++ 优化代码 (空间优化)

同样可以压缩到一维。与 0/1 背包不同的是,内层循环必须从前往后遍历,以确保 dp[j - coin] 是本轮(i)更新过的值,这恰好体现了物品可重复选取的特性。

C++

#include <iostream>
#include <vector>
#include <algorithm>class Solution {
public:int coinChange(std::vector<int>& coins, int amount) {int max_val = amount + 1;std::vector<int> dp(amount + 1, max_val);dp[0] = 0;for (int coin : coins) {for (int j = coin; j <= amount; ++j) {dp[j] = std::min(dp[j], dp[j - coin] + 1);}}return dp[amount] > amount ? -1 : dp[amount];}
};// int main() {
//     Solution sol;
//     std::vector<int> coins = {1, 2, 5};
//     std::cout << sol.coinChange(coins, 11) << std::endl; // 输出: 3
//     return 0;
// }

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词