前言
动态规划(Dynamic Programming,简称 DP)是一种解决多阶段决策过程最优化问题的方法。它是一种将复杂问题分解成重叠子问题的策略,通过维护每个子问题的最优解来推导出问题的最优解。
动态规划的主要思想是利用已求解的子问题的最优解来推导出更大问题的最优解,从而避免了重复计算。因此,动态规划通常采用自底向上的方式进行求解,先求解出小规模的问题,然后逐步推导出更大规模的问题,直到求解出整个问题的最优解。
动态规划通常包括以下几个基本步骤:
- 定义状态:将问题划分为若干个子问题,并定义状态表示子问题的解;
- 定义状态转移方程:根据子问题之间的关系,设计状态转移方程,即如何从已知状态推导出未知状态的计算过程;
- 确定初始状态:定义最小的子问题的解;
- 自底向上求解:按照状态转移方程,计算出所有状态的最优解;
- 根据最优解构造问题的解。
动态规划可以解决许多实际问题,例如最短路径问题、背包问题、最长公共子序列问题、编辑距离问题等。同时,动态规划也是许多其他算法的核心思想,例如分治算法、贪心算法等。
动态规划是一种解决多阶段决策过程最优化问题的方法,它将复杂问题分解成重叠子问题,通过维护每个子问题的最优解来推导出问题的最优解。动态规划包括定义状态、设计状态转移方程、确定初始状态、自底向上求解和构造问题解等步骤。动态规划可以解决许多实际问题,也是其他算法的核心思想之一。
一、三步问题
三步问题。有个小孩正在上楼梯,楼梯有n阶台阶,小孩一次可以上1阶、2阶或3阶。实现一种方法,计算小孩有多少种上楼梯的方式。结果可能很大,你需要对结果模1000000007。
示例1:
输入:n = 3
输出:4
说明: 有四种走法
示例2:
输入:n = 5
输出:13
来源:力扣(LeetCode)。
1.1、思路
三步问题,采用动态规划(思路和上楼梯问题一致),n层楼梯的走法可以分为三种情况:
- 剩余一层楼梯要走然后一步走一层。
- 剩余两层楼梯要走,然后一步走两层。
- 剩余三层楼梯要走,然后一步走三层。
状态方程:
dp[n] = dp[n-1] + dp[n-2] + dp[n-3]
1.2、代码实现
class Solution {
public:
int waysToStep(int n) {
if(n==1)
return 1;
else if(n==2)
return 2;
vector<long> dp(n+1,0);
dp[1]=1;
dp[2]=2;
dp[3]=4;
for(int i=4;i<=n;i++)
{
dp[i]=((dp[i-1]+dp[i-2]+dp[i-3])%1000000007);
}
return dp[n];
}
};
时间复杂度:O(n)。
空间复杂度:O(n)。
二、 连续数列
给定一个整数数组,找出总和最大的连续数列,并返回总和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
来源:力扣(LeetCode)。
2.1、思路
假设 nums 数组的长度是 n,下标从 0 到 n−1。
我们用 f(i) 代表以第 i 个数结尾的「连续子数组的最大和」,那么很显然我们要求的答案就是:
因此我们只需要求出每个位置的 f(i),然后返回 f 数组中的最大值即可。那么我们如何求 f(i) 呢?我们可以考虑 nums[i] 单独成为一段还是加入 f(i−1) 对应的那一段,这取决于 nums[i] 和 f(i−1)+nums[i] 的大小,我们希望获得一个比较大的,于是可以写出这样的动态规划转移方程:
不难给出一个时间复杂度 O(n)、空间复杂度 O(n) 的实现,即用一个 f 数组来保存 f(i) 的值,用一个循环求出所有 f(i)。考虑到 f(i) 只和 f(i−1) 相关,于是我们可以只用一个变量 pre 来维护对于当前 f(i) 的 f(i−1) 的值是多少,从而让空间复杂度降低到 O(1),这有点类似「滚动数组」的思想。
2.2、代码实现
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int pre = 0, maxAns = nums[0];
for (const auto &x: nums) {
pre = max(pre + x, x);
maxAns = max(maxAns, pre);
}
return maxAns;
}
};
时间复杂度:O(n),其中 n 为 nums 数组的长度。
空间复杂度:O(1)。我们只需要常数空间存放若干变量。
三、按摩师
一个有名的按摩师会收到源源不断的预约请求,每个预约都可以选择接或不接。在每次预约服务之间要有休息时间,因此她不能接受相邻的预约。给定一个预约请求序列,替按摩师找到最优的预约集合(总预约时间最长),返回总的分钟数。
示例 1:
输入: [1,2,3,1]
输出: 4
解释: 选择 1 号预约和 3 号预约,总时长 = 1 + 3 = 4。
示例 2:
输入: [2,7,9,3,1]
输出: 12
解释: 选择 1 号预约、 3 号预约和 5 号预约,总时长 = 2 + 9 + 1 = 12。
示例 3:
输入: [2,1,4,5,3,1,1,3]
输出: 12
解释: 选择 1 号预约、 3 号预约、 5 号预约和 8 号预约,总时长 = 2 + 4 + 3 + 3 = 12。
来源:力扣(LeetCode)。
3.1、思路
定义 dp[i][0] 表示考虑前 i 个预约,第 i 个预约不接的最长预约时间,dp[i][1] 表示考虑前 i 个预约,第
i 个预约接的最长预约时间。
从前往后计算 dp 值,假设我们已经计算出前 i−1 个 dp 值,考虑计算 dp[i][0/1] 的答案。
首先考虑 dp[i][0] 的转移方程,由于这个状态下第 i 个预约是不接的,所以第 i−1 个预约接或不接都可以,故可以从 dp[i−1][0] 和 dp[i−1][1] 两个状态转移过来,转移方程即为:
dp[i][0]=max(dp[i−1][0],dp[i−1][1])
对于 dp[i][1] ,由于这个状态下第 i 个预约要接,根据题目要求按摩师不能接受相邻的预约,所以第
i−1 个预约不能接受,故我们只能从 dp[i−1][0] 这个状态转移过来,转移方程即为:
dp[i][1]=dp[i−1][0]+nums。 其中 nums 表示第 i 个预约的时长。
最后答案即为 :
max(dp[n][0],dp[n][1]) ,其中 n 表示预约的个数。
再回来看转移方程,我们发现计算 dp[i][0/1] 时,只与前一个状态 dp[i−1][0/1] 有关,所以我们可以不用开数组,只用两个变量 分别存储 dp[i−1][0] 和 dp[i−1][1] 的答案,然后去转移更新答案即可。
3.2、代码实现
class Solution {
public:
int massage(vector<int>& nums) {
int n = (int)nums.size();
if (!n) {
return 0;
}
int dp0 = 0, dp1 = nums[0];
for (int i = 1; i < n; ++i){
int tdp0 = max(dp0, dp1); // 计算 dp[i][0]
int tdp1 = dp0 + nums[i]; // 计算 dp[i][1]
dp0 = tdp0; // 用 dp[i][0] 更新 dp_0
dp1 = tdp1; // 用 dp[i][1] 更新 dp_1
}
return max(dp0, dp1);
}
};
时间复杂度:O(n),其中 n 为预约的个数。有 2n 个状态需要计算,每次状态转移需要 O(1) 的时间,所以一共需要 O(2n)=O(n) 的时间复杂度。
空间复杂度:O(1),只需要常数的空间存放若干变量。
总结
动态规划(Dynamic Programming)是一种解决多阶段决策最优化问题的方法,它将复杂问题分解成重叠子问题并通过维护每个子问题的最优解来推导出问题的最优解。动态规划可以解决许多实际问题,例如最短路径问题、背包问题、最长公共子序列问题、编辑距离问题等。
动态规划的基本思想是利用已求解的子问题的最优解来推导出更大问题的最优解,从而避免了重复计算。它通常采用自底向上的方式进行求解,先求解出小规模的问题,然后逐步推导出更大规模的问题,直到求解出整个问题的最优解。
动态规划通常包括以下几个基本步骤:
- 定义状态:将问题划分为若干个子问题,并定义状态表示子问题的解;
- 定义状态转移方程:根据子问题之间的关系,设计状态转移方程,即如何从已知状态推导出未知状态的计算过程;
- 确定初始状态:定义最小的子问题的解;
- 自底向上求解:按照状态转移方程,计算出所有状态的最优解;
- 根据最优解构造问题的解。
动态规划的时间复杂度通常为 O ( n 2 ) O(n^2) O(n2)或 O ( n 3 ) O(n^3) O(n3),空间复杂度为O(n),其中n表示问题规模。在实际应用中,为了减少空间复杂度,通常可以使用滚动数组等技巧来优化动态规划算法。