一、什么是动态规划?
动态规划(Dynamic Programming),简称DP,是运筹学的一个分支,通常用来解决多阶段决策过程最优化问题。动态规划的基本思想就是将原问题转换为一系列相互联系的子问题,然后通过逐层递推来求得最后的解。
二、动态规划的相关概念
(1)子问题和原问题
原问题就是你要求解的这个问题本身,子问题是和原问题相似但规模较小的问题(原问题本身就是子问题的最复杂的情形,即子问题的特例)。
例如:要求出F(10),那么求出F(10)就是原问题,求出F(k)(k<=10)都是子问题。
(2)状态
状态就是子问题中会变化的某个量,可以把状态看成我们要求解的问题的自变量。
例如:我们要求的F(10),那么这里的自变量10就是一个状态。
(3)状态转移方程
能够表示状态之间转移关系的方程,一般利用关于状态的某个函数建立起来。
例如:F(n) = F(n - 1) + F(n - 2),当n>2且n为整数时;当n = 1或 2 时,F(n) = 1,这种最简单的初始条件一般称为边界条件,也被称为基本方程。
(4)DP数组
DP数组也可以叫“子问题数组”,因为DP数组中的每一个元素都对应一个子问题的结果,DP数组的下标一般就是该子问题对应的状态。
根据上面的概念,我们可以明确一点:动态规划中的每一个状态一定是由上一个状态推导出来的,这一点就区别于贪心算法,贪心算法一般没有状态推导,而是直接从局部选最优。
三、动态规划问题的一般解题步骤
1.确定DP数组以及其下标的含义
2.确定状态转移方程
3.确定DP数组最简单的情况(DP数组的初始化)
4.确定数组的遍历顺序
5.举例推导DP数组
例题如下
题目来源:洛谷P1164
题目信息可化简如下:拥有金钱数为M,现有N种菜品,每种菜品有自己的价格,求有几种选择菜品的方案能够刚好花光金钱M。
首先我们明确,当N == 1且M == 该菜品的价格时,方案数就只有一个;若M != 菜品的价格,那么就没有成立的方案。因此,我们需要根据给定的M和当前菜品的价格进行讨论,我们就可以定义dp数组dp[i][j]。首先明确dp数组的含义,dp[i][j]表示用 j 元吃前 i 种菜,然后我们再定义数组a[i]来存放每道菜的价格。
根据以上分析,我们还需要确定状态转移方程。关于如何确定状态转移方程,需要根据给定的M和当前菜品的价格进行讨论。容易得到以下情况:
(1)若拥有的钱M与第 i 种菜价格相同,方案数为用M吃前 i - 1 种菜的方案数 + 用M只吃第 i 种菜
即:dp[i][j] = dp[i - 1][j] + 1;
(2)若拥有的钱M大于第 i 种菜的价格,方案数为不吃第 i 种菜的方案数 + 吃第 i 种菜的方案数
即:dp[i][j] = dp[i - 1][j] + dp[i - 1][j - a[i]];
(3)若拥有的钱M小于第 i 种菜的价格,方案数就为用M吃前 i - 1 种菜的方案数
即:dp[i][j] = dp[i - 1][j];
根据以上思路完成状态转移方程的构建后,直接输出dp[N][M]即为答案。参考代码如下:
#include <iostream>
using namespace std;
int a[101], f[101][10001] = {0};
int dp(int n, int m) {
//i表示有i种菜,j表示有j元;f[i][j]表示用j元吃前i种菜
//a[i]表示第i种菜的价格
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
if (j == a[i]) {
//若拥有的钱与第i种菜价格相同,方案数为吃前i-1种菜方案数+只吃第i种菜
f[i][j] = f[i - 1][j] + 1;
} else if (j > a[i]) {
//若拥有的钱充足,方案数为不吃第i种菜+吃第i种菜
f[i][j] = f[i - 1][j] + f[i - 1][j - a[i]];
} else if (j < a[i]) {
//若拥有的钱不够吃第i种菜,那么方案数就为吃前i-1种菜的方案数
f[i][j] = f[i - 1][j];
}
}
}
return f[n][m];
}
int main() {
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
}
int res = dp(n, m);
cout << res;
return 0;
}
待续