梦想不会自己发光,真正闪耀的是那个为梦狂奔的你。献给知行的孩子们!(Eric.He著)
动态规划的本质:它是一种 “以空间换时间”的优化算法,核心是把复杂问题拆解为多个重叠的子问题,通过记录子问题的最优解(状态),避免重复计算,最终由子问题的最优解推导出原问题的最优解。
区间型动态规划是解决基于 “区间” 的最优解问题,问题的解依赖于原区间的子区间的最优解,比如 “区间合并的最值”“石子合并的最小花费”。这类问题的状态通常定义为区间的起点和终点,解题顺序是从短区间到长区间(先求解长度为 1 的区间,再求解长度为 2 的,直到原区间)。
问题描述:
算法解析:
#include <iostream>
#include <vector>
using namespace std;
int stoneMerge(vector<int>& stones) {
int n = stones.size();
if (n <= 1) return 0;
// 1. 预处理前缀和(下标从1开始,方便计算)
vector<int> sum(n + 1, 0);
for (int i = 1; i <= n; ++i) {
sum[i] = sum[i - 1] + stones[i - 1];
}
// 2. 初始化dp数组:dp[i][j]表示合并i到j堆的最小代价
vector<vector<int>> dp(n + 1, vector<int>(n + 1, 0));
// 3. 按区间长度遍历(len表示区间长度,从2到n)
for (int len = 2; len <= n; ++len) {
// i是区间起点,j = i + len - 1是区间终点
for (int i = 1; i + len - 1 <= n; ++i) {
int j = i + len - 1;
dp[i][j] = INT_MAX; // 初始化为极大值
// 枚举分割点k
for (int k = i; k < j; ++k) {
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j] + sum[j] - sum[i-1]);
}
}
}
// 合并1到n堆的最小代价
return dp[1][n];
}
int main() {
// 测试用例:石子堆 [3, 4, 3],最小代价:(3+4)+(7+3)=7+10=17
vector<int> stones1 = {3, 4, 3};
cout << stoneMerge(stones1) << endl; // 输出 17
// 测试用例:石子堆 [4, 1, 1, 4],最小代价:(1+1)+(4+2)+(6+4)=2+6+10=18
vector<int> stones2 = {4, 1, 1, 4};
cout << stoneMerge(stones2) << endl; // 输出 18
return 0;
}
17
18
问题描述:
算法解析:
#include <iostream>
#include <vector>
using namespace std;
int maxCoins(vector<int>& nums) {
// 步骤1:处理边界,首尾添加1,简化边界判断
int n = nums.size();
vector<int> new_nums(n + 2, 1); // 初始化所有元素为1
for (int i = 0; i < n; ++i) {
new_nums[i + 1] = nums[i];
}
int len = new_nums.size(); // 新数组长度为n+2
// 步骤2:初始化dp数组,dp[i][j]表示戳破(i,j)区间内气球的最大硬币数
vector<vector<int>> dp(len, vector<int>(len, 0));
// 步骤3:按区间长度从小到大遍历(长度从2开始,因为长度1时区间内无气球)
for (int length = 2; length < len; ++length) {
// 遍历所有起始位置i
for (int i = 0; i < len - length; ++i) {
int j = i + length; // 区间结束位置j
// 遍历区间内所有可能最后戳破的气球k
for (int k = i + 1; k < j; ++k) {
// 状态转移:最后戳破k,累加左右区间结果 + 戳k的硬币数
int current = dp[i][k] + dp[k][j] + new_nums[i] * new_nums[k] * new_nums[j];
dp[i][j] = max(dp[i][j], current);
}
}
}
// 最终结果是戳破(0, len-1)区间内的所有气球(即原始数组的所有气球)
return dp[0][len - 1];
}
int main() {
// 示例1:输入 [3,1,5,8],输出 167
vector<int> test1 = {3, 1, 5, 8};
cout << "Test1 result: " << maxCoins(test1) << endl; // 输出167
// 示例2:输入 [1,5],输出 10
vector<int> test2 = {1, 5};
cout << "Test2 result: " << maxCoins(test2) << endl; // 输出10
return 0;
}
Test1 result: 167
Test2 result: 10
问题描述:
算法解析:
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
int longestPalindromeSubseq(string s) {
int n = s.size();
// 步骤1:初始化dp数组,dp[i][j]表示s[i..j]的最长回文子序列长度
vector<vector<int>> dp(n, vector<int>(n, 0));
// 步骤2:初始化边界条件(单个字符的回文长度为1)
for (int i = 0; i < n; ++i) {
dp[i][i] = 1;
}
// 步骤3:按子串长度从小到大遍历(长度从2到n)
for (int len = 2; len <= n; ++len) {
// 遍历所有起始位置i
for (int i = 0; i <= n - len; ++i) {
int j = i + len - 1; // 子串结束位置j
// 情况1:两端字符相同
if (s[i] == s[j]) {
if (len == 2) {
dp[i][j] = 2; // 两个相同字符,长度直接为2
} else {
dp[i][j] = dp[i+1][j-1] + 2;
}
}
// 情况2:两端字符不同
else {
dp[i][j] = max(dp[i+1][j], dp[i][j-1]);
}
}
}
// 最终结果是整个字符串的最长回文子序列长度
return dp[0][n-1];
}
int main() {
// 示例1:输入 "bbbab",输出 4(最长回文子序列是 "bbbb")
string test1 = "bbbab";
cout << "Test1 result: " << longestPalindromeSubseq(test1) << endl; // 输出4
// 示例2:输入 "cbbd",输出 2(最长回文子序列是 "bb")
string test2 = "cbbd";
cout << "Test2 result: " << longestPalindromeSubseq(test2) << endl; // 输出2
return 0;
}
Test1 result: 4
Test2 result: 2