目录
动态规划(DP)是一种算法技术,它将大问题分解为更简单的子问题,对整体问题的最优解决方案取决于子问题的最优解决方案。常用于求解计数问题(求方案数)和最值问题(最大价值、最小花费)等
一、DP的概念
常用于解决重叠子问题、最优子结构特征的问题
int fib(int n) {
if (n == 1 || n == 2)
return 1;
return (fib(n - 1) + fib(n - 2));
}
在递归的过程中有重复的子过程被多次计算(计算fib(4)时展开为fib(2)+fib(3),再把fib(3)展开为fib(1)和fib(2),此时fib(2)被重复计算两次)
用DP可以高效处理的问题具有两个特征:重复子问题、最优子结构
二、重复子问题(自顶向下与记忆化)
一个子问题重复计算,消耗了大量时间,用DP解决重复子问题,使每个子问题只计算一次(在追求时间高效率的同时,可能消耗更多的空间)
#include <iostream>
#include <vector>
using namespace std;
// 动态规划计算斐波那契数列的第n项
int fib2(int n) {
vector<int> dp(n + 1, -1); // 创建一个长度为n+1的向量,并初始化所有元素为-1
return f2(n, dp); // 调用f2函数计算第n项
}
// 递归计算斐波那契数列的第i项,使用动态规划存储已计算的项
int f2(int i, vector<int>& dp) {
if (i == 0) {
return 0; // 斐波那契数列的第0项是0
}
if (i == 1) {
return 1; // 斐波那契数列的第1项是1
}
if (dp[i] != -1) { // 如果已经计算过第i项,则直接返回结果
return dp[i];
}
int ans = f2(i - 1, dp) + f2(i - 2, dp); // 递归计算第i-1项和第i-2项的和
dp[i] = ans; // 存储计算结果
return ans; // 返回第i项的斐波那契数
}
int main() {
int n;
cout << "请输入斐波那契数列的项数:";
cin >> n;
cout << "斐波那契数列的第" << n << "项为:" << fib2(n) << endl;
return 0;
}
三、最优子结构(自底向上与制表递推)
大问题的最优解包含小问题的最优解,可以通过小问题的最优解推导出大问题的最优解
#include <iostream>
using namespace std;
// 计算斐波那契数列的第n项
int fib3(int n) {
if (n == 0) {
return 0;
}
if (n == 1) {
return 1;
}
vector<int> dp(n + 1);
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
int main() {
int n;
cout << "请输入斐波那契数列的项数:";
cin >> n;
cout << "斐波那契数列的第" << n << "项为:" << fib3(n) << endl;
return 0;
}
任何动态规划问题都一定对应一个有重复行为的递归(从递归入手,逐渐实现动态规划的方法)
四、例题(最低票价问题)
暴力求解
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;
int minCostTickets(vector<int>& days, vector<int>& costs) {
vector<int> dp(days.size(), 0);
vector<int> travelDays(days.size(), 0);
int day = 1;
for (int i = 0; i < days.size(); ++i) {
travelDays[i] = days[i];
if (days[i] == day) {
travelDays[i] = day++;
}
}
for (int i = 1; i < travelDays.size(); ++i) {
dp[i] = INT_MAX;
for (int k = 0; k < 3; ++k) {
int j = i;
// Find the last day we can travel with the current pass
while (j < travelDays.size() && travelDays[j] - travelDays[i] <= (k == 2 ? 30 : k == 1 ? 7 : 1)) {
j++;
}
j--; // The last day we can travel with the current pass
dp[i] = min(dp[i], costs[k] + (j < i ? 0 : dp[j]));
}
}
return dp[travelDays.size() - 1];
}
int main() {
vector<int> days = {1, 4, 6, 7, 8, 20};
vector<int> costs = {2, 7, 15};
cout << "The minimum cost is: " << minCostTickets(days, costs) << endl;
return 0;
}
自顶向下
#include <iostream>
#include <vector>
#include <unordered_map>
#include <climits>
using namespace std;
int minCostTickets(vector<int>& days, vector<int>& costs) {
unordered_map<int, int> memo; // 用于记忆化存储
return dp(days, costs, 0, memo);
}
int dp(const vector<int>& days, const vector<int>& costs, int i, unordered_map<int, int>& memo) {
if (i == days.size()) {
return 0; // 没有更多旅行日,不需要成本
}
if (memo.find(i) != memo.end()) {
return memo[i]; // 如果已经计算过,直接返回结果
}
int index = upper_bound(days.begin(), days.end(), days[i]) - days.begin(); // 找到下一个旅行日
int ans = INT_MAX;
for (int k = 0; k < 3; ++k) {
int daysCovered = (k == 0) ? 1 : (k == 1) ? 7 : 30; // 计算通行证覆盖的天数
int j = i;
while (j < index && days[j] + daysCovered > days[i]) {
j++;
}
ans = min(ans, costs[k] + dp(days, costs, j, memo));
}
memo[i] = ans; // 存储计算结果
return ans;
}
int main() {
vector<int> days = {1, 4, 6, 7, 8, 20};
vector<int> costs = {2, 7, 15};
cout << "The minimum cost is: " << minCostTickets(days, costs) << endl;
return 0;
}
自底向上
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int minCostTickets(vector<int>& days, vector<int>& costs) {
vector<int> dp(days.size(), 0); // dp[i] 表示在第 i 天旅行的最低成本
vector<int> dayIndex; // 存储每个旅行日的索引
for (int i = 0; i < days.size(); ++i) {
dayIndex.push_back(i);
}
sort(dayIndex.begin(), dayIndex.end(), [&](int a, int b) { return days[a] < days[b]; });
// 初始化dp数组,将所有旅行日的最低成本设置为一个较大的数
for (int i = 0; i < days.size(); ++i) {
dp[i] = INT_MAX;
}
// 从最后一天开始向前计算
for (int i = 0; i < days.size(); ++i) {
int day = days[dayIndex[i]];
// 如果是第一天,成本为0
if (day == 1) {
dp[dayIndex[i]] = 0;
continue;
}
// 尝试1天、7天和30天的通行证
for (int k = 0; k < 3; ++k) {
int prevDay = day - (k == 0 ? 1 : (k == 1 ? 7 : 30));
if (prevDay <= 0) {
prevDay = 1; // 确保不会越界
}
int prevIndex = find(days.begin(), days.end(), prevDay) - days.begin();
dp[dayIndex[i]] = min(dp[dayIndex[i]], costs[k] + dp[prevIndex]);
}
}
// 返回第一天的最低成本
return dp[dayIndex[0]];
}
int main() {
vector<int> days = {1, 4, 6, 7, 8, 20};
vector<int> costs = {2, 7, 15};
cout << "The minimum cost is: " << minCostTickets(days, costs) << endl;
return 0;
}