动态规划 Dynamic Programming
简介
动态规划是一种计算机编程技术,它将一个算法问题分解为子问题,保存结果,并对子问题进行优化,以找到整体解决方案。动态规划通常用于寻找算法查询的最大和最小范围。动态规划的核心思想是将复杂问题分解为简单的子问题,并将结果存储起来以便后续使用,从而提高算法的效率。动态规划算法通常用于解决可以分解为重叠子问题或最优子结构的问题。
动态规划算法的工作原理是将复杂问题分解为简单的子问题,然后寻找这些子问题的最优解。通过存储解决方案,可以避免重复计算相同的问题,从而提高效率。动态规划可以通过自顶向下或自底向上两种方法实现。
- 自顶向下方法通过递归地构建解决方案,并在需要时使用子问题的解决方案。
- 自底向上方法则是通过将问题分解为较小的子问题,并尝试解决具有最小数学值的方程,然后逐步解决具有最大值的方程。
动态规划算法通常用于解决可以分解为更小问题并且这些子问题重叠的问题。它适用于需要重复计算以前计算值的子问题,并且通过存储计算值来节省时间并提供更快的解决方案的情况。
动态规划算法与递归的区别在于,递归是一种解决问题的方法,而动态规划是递归解决方案的优化技术。动态规划适用于需要重复调用相同输入的递归函数的情况。
一维动态规划
爬楼梯
题目描述:
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
动态规划的转移方程:
f(x)=f(x−1)+f(x−2)
class Solution {
public:
int climbStairs(int n) {
if (n == 0 || n == 1) {
return 1;
}
int result = 0, n_1 = 1, n_2 = 1;
for (int i = 2; i <= n; i++) {
result = n_1 + n_2;
n_2 = n_1;
n_1 = result;
}
return result;
}
};
打家劫舍
题目描述:
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
状态转移方程:
class Solution {
public:
int rob(vector<int>& nums) {
if (nums.empty()) {
return 0;
}
int size = nums.size();
if (size == 1) {
return nums[0];
}
vector<int> dp = vector<int>(size, 0);
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
for (int i = 2; i < size; ++i) {
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[size - 1];
}
};
多维动态规划
最小路径和
题目描述:
给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
状态转移方程:
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
if (grid.size() == 0 || grid[0].size() == 0) {
return 0;
}
int rows = grid.size(), columns = grid[0].size();
auto dp = vector<vector<int>>(rows, vector<int>(columns));
dp[0][0] = grid[0][0];
for (int i = 1; i < rows; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
for (int j = 1; j < columns; j++) {
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
for (int i = 1; i < rows; i++) {
for (int j = 1; j < columns; j++) {
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
return dp[rows - 1][columns - 1];
}
};
三角形最小路径和
题目描述:
给定一个三角形 triangle ,找出自顶向下的最小路径和。
每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 i 或 i + 1
状态转移方程
class Solution {
public:
int minimumTotal(vector<vector<int>>& triangle) {
int n = triangle.size();
vector<vector<int>> f(n, vector<int>(n));
f[0][0] = triangle[0][0];
for (int i = 1; i < n; i++) {
f[i][0] = f[i - 1][0] + triangle[i][0];
for (int j = 1; j < i; j++) {
f[i][j] = min(f[i - 1][j - 1], f[i - 1][j]) + triangle[i][j];
}
f[i][i] = f[i - 1][i - 1] + triangle[i][i];
}
return *min_element(f[n-1].begin(), f[n - 1].end());
}
};
最长回文子串
题目描述:
给你一个字符串 s,找到 s 中最长的回文子串。
如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。
状态转移方程:
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class Solution {
public:
string longestPalindrome(string s) {
int n = s.size();
if (n < 2) {
return s;
}
int maxLen = 1;
int begin = 0;
// dp[i][j] 表示 s[i..j] 是否是回文串
vector<vector<int>> dp(n, vector<int>(n));
// 初始化:所有长度为 1 的子串都是回文串
for (int i = 0; i < n; i++) {
dp[i][i] = true;
}
// 递推开始
// 先枚举子串长度
for (int L = 2; L <= n; L++) {
// 枚举左边界,左边界的上限设置可以宽松一些
for (int i = 0; i < n; i++) {
// 由 L 和 i 可以确定右边界,即 j - i + 1 = L 得
int j = L + i - 1;
// 如果右边界越界,就可以退出当前循环
if (j >= n) {
break;
}
if (s[i] != s[j]) {
dp[i][j] = false;
} else {
if (j - i < 3) {
dp[i][j] = true;
} else {
dp[i][j] = dp[i + 1][j - 1];
}
}
// 只要 dp[i][L] == true 成立,就表示子串 s[i..L] 是回文,此时记录回文长度和起始位置
if (dp[i][j] && j - i + 1 > maxLen) {
maxLen = j - i + 1;
begin = i;
}
}
}
return s.substr(begin, maxLen);
}
};
买卖股票的最佳时机 II
题目描述:
给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候最多只能持有一股股票。你也可以先购买,然后在同一天出售。
返回 你能获得的最大利润 。
状态转移方程:
代码实现:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
int dp[n][2];
dp[0][0] = 0, dp[0][1] = -prices[0];
for (int i = 1; i < n; ++i) {
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
}
return dp[n - 1][0];
}
};