前言
动态规划(Dynamic Programming,简称 DP)是一种解决多阶段决策过程最优化问题的方法。它是一种将复杂问题分解成重叠子问题的策略,通过维护每个子问题的最优解来推导出问题的最优解。
动态规划的主要思想是利用已求解的子问题的最优解来推导出更大问题的最优解,从而避免了重复计算。因此,动态规划通常采用自底向上的方式进行求解,先求解出小规模的问题,然后逐步推导出更大规模的问题,直到求解出整个问题的最优解。
动态规划通常包括以下几个基本步骤:
- 定义状态:将问题划分为若干个子问题,并定义状态表示子问题的解;
- 定义状态转移方程:根据子问题之间的关系,设计状态转移方程,即如何从已知状态推导出未知状态的计算过程;
- 确定初始状态:定义最小的子问题的解;
- 自底向上求解:按照状态转移方程,计算出所有状态的最优解;
- 根据最优解构造问题的解。
动态规划可以解决许多实际问题,例如最短路径问题、背包问题、最长公共子序列问题、编辑距离问题等。同时,动态规划也是许多其他算法的核心思想,例如分治算法、贪心算法等。
动态规划是一种解决多阶段决策过程最优化问题的方法,它将复杂问题分解成重叠子问题,通过维护每个子问题的最优解来推导出问题的最优解。动态规划包括定义状态、设计状态转移方程、确定初始状态、自底向上求解和构造问题解等步骤。动态规划可以解决许多实际问题,也是其他算法的核心思想之一。
一、获取生成数组中的最大值
给你一个整数 n 。按下述规则生成一个长度为 n + 1 的数组 nums :
nums[0] = 0
nums[1] = 1
当 2 <= 2 * i <= n 时,nums[2 * i] = nums[i]
当 2 <= 2 * i + 1 <= n 时,nums[2 * i + 1] = nums[i] + nums[i + 1]
返回生成数组 nums 中的 最大 值。
示例 1:
输入:n = 7
输出:3
解释:根据规则:
nums[0] = 0
nums[1] = 1
nums[(1 * 2) = 2] = nums[1] = 1
nums[(1 * 2) + 1 = 3] = nums[1] + nums[2] = 1 + 1 = 2
nums[(2 * 2) = 4] = nums[2] = 1
nums[(2 * 2) + 1 = 5] = nums[2] + nums[3] = 1 + 2 = 3
nums[(3 * 2) = 6] = nums[3] = 2
nums[(3 * 2) + 1 = 7] = nums[3] + nums[4] = 2 + 1 = 3
因此,nums = [0,1,1,2,1,3,2,3],最大值 3
示例 2:
输入:n = 2
输出:1
解释:根据规则,nums[0]、nums[1] 和 nums[2] 之中的最大值是 1
示例 3:
输入:n = 3
输出:2
解释:根据规则,nums[0]、nums[1]、nums[2] 和 nums[3] 之中的最大值是 2
来源:力扣(LeetCode)。
1.1、思路
当 i 大于等于2时:
1.2、代码实现
class Solution {
public:
int getMaximumGenerated(int n) {
if(n<=0)
return 0;
vector<int> dp(n+1,0);
dp[0]=0;
dp[1]=1;
int MaxCount=dp[1];
for(int i=2;i<=n;i++)
{
if(i&1)
dp[i]=dp[i/2]+dp[i/2+1];
else
dp[i]=dp[i/2];
MaxCount=max(MaxCount,dp[i]);
}
return MaxCount;
}
};
时间复杂度:O(n)。
空间复杂度:O(n)。
二、传递信息
小朋友 A 在和 ta 的小伙伴们玩传信息游戏,游戏规则如下:
有 n 名玩家,所有玩家编号分别为 0 ~ n-1,其中小朋友 A 的编号为 0
每个玩家都有固定的若干个可传信息的其他玩家(也可能没有)。传信息的关系是单向的(比如 A 可以向 B 传信息,但 B 不能向 A 传信息)。
每轮信息必须需要传递给另一个人,且信息可重复经过同一个人
给定总玩家数 n,以及按 [玩家编号,对应可传递玩家编号] 关系组成的二维数组 relation。返回信息从小 A (编号 0 ) 经过 k 轮传递到编号为 n-1 的小伙伴处的方案数;若不能到达,返回 0。
示例 1:
输入:n = 5, relation = [[0,2],[2,1],[3,4],[2,3],[1,4],[2,0],[0,4]], k = 3
输出:3
解释:信息从小 A 编号 0 处开始,经 3 轮传递,到达编号 4。共有 3 种方案,分别是 0->2->0->4, 0->2->1->4, 0->2->3->4。
示例 2:
输入:n = 3, relation = [[0,2],[2,1]], k = 2
输出:0
解释:信息不能从小 A 处经过 2 轮传递到编号 2
来源:力扣(LeetCode)。
2.1、思路
这道题是计数问题,可以使用动态规划的方法解决。
定义动态规划的状态 dp[i][j] 为经过 i 轮传递到编号 j 的玩家的方案数,其中 0≤i≤k,0≤j<n。
由于从编号 0 的玩家开始传递,当 i=0 时,一定位于编号 0 的玩家,不会传递到其他玩家,因此动态规划的边界情况如下:
对于传信息的关系 [src,dst],如果第 i 轮传递到编号 src 的玩家,则第 i+1 轮可以从编号 src 的玩家传递到编号 dst 的玩家。因此在计算 dp[i+1][dst] 时,需要考虑可以传递到编号 dst 的所有玩家。由此可以得到动态规划的状态转移方程,其中 0≤i<k:
最终得到 dp[k][n−1] 即为总的方案数。
2.2、代码实现
class Solution {
public:
int numWays(int n, vector<vector<int>>& relation, int k) {
vector<vector<int>> dp(k + 1, vector<int>(n));
dp[0][0] = 1;
for (int i = 0; i < k; i++) {
for (auto& edge : relation) {
int src = edge[0], dst = edge[1];
dp[i + 1][dst] += dp[i][src];
}
}
return dp[k][n - 1];
}
};
上述实现的空间复杂度是 O(kn)。由于当 i>0 时,dp[i][] 的值只和 dp[i−1][] 的值有关,因此可以将二维数组变成一维数组,将空间复杂度优化到 O(n)。
class Solution {
public:
int numWays(int n, vector<vector<int>>& relation, int k) {
vector<int> dp(n);
dp[0] = 1;
for (int i = 0; i < k; i++) {
vector<int> next(n);
for (auto& edge : relation) {
int src = edge[0], dst = edge[1];
next[dst] += dp[src];
}
dp = next;
}
return dp[n - 1];
}
};
时间复杂度:O(km)。其中 m 为 relation 数组的长度。
空间复杂度:O(n)。
三、下载插件
小明打算给自己的 VS code 安装使用插件,初始状态下带宽每分钟可以完成 1 个插件的下载。假定每分钟选择以下两种策略之一:
- 使用当前带宽下载插件
- 将带宽加倍(下载插件数量随之加倍)
请返回完成下载 n 个插件最少需要多少分钟。
注意:实际的下载的插件数量可以超过 n 个
示例 1:
输入:n = 2
输出:2
解释:
以下两个方案,都能实现 2 分钟内下载 2 个插件
方案一:第一分钟带宽加倍,带宽可每分钟下载 2 个插件;第二分钟下载 2 个插件
方案二:第一分钟下载 1 个插件,第二分钟下载 1 个插件
示例 2:
输入:n = 4
输出:3
解释:
最少需要 3 分钟可完成 4 个插件的下载,以下是其中一种方案:
第一分钟带宽加倍,带宽可每分钟下载 2 个插件;
第二分钟下载 2 个插件;
第三分钟下载 2 个插件。
来源:力扣(LeetCode)。
思路:令 dp[i] 表示下载 i 个插件需要的最少分钟数,则 dp[i] = dp[(i + 1) / 2] + 1。
代码实现:
class Solution {
public:
int leastMinutes(int n) {
vector<int> dp(n + 1); // dp[i] 表示下载 i 个插件需要的最少分钟数
dp[1] = 1;
for (int i = 2; i <= n; ++i)
dp[i] = dp[(i + 1) / 2] + 1;
return dp[n];
}
};
时间复杂度:O(n)。
空间复杂度:O(n)。
总结
动态规划(Dynamic Programming)是一种解决多阶段决策最优化问题的方法,它将复杂问题分解成重叠子问题并通过维护每个子问题的最优解来推导出问题的最优解。动态规划可以解决许多实际问题,例如最短路径问题、背包问题、最长公共子序列问题、编辑距离问题等。
动态规划的基本思想是利用已求解的子问题的最优解来推导出更大问题的最优解,从而避免了重复计算。它通常采用自底向上的方式进行求解,先求解出小规模的问题,然后逐步推导出更大规模的问题,直到求解出整个问题的最优解。
动态规划通常包括以下几个基本步骤:
- 定义状态:将问题划分为若干个子问题,并定义状态表示子问题的解;
- 定义状态转移方程:根据子问题之间的关系,设计状态转移方程,即如何从已知状态推导出未知状态的计算过程;
- 确定初始状态:定义最小的子问题的解;
- 自底向上求解:按照状态转移方程,计算出所有状态的最优解;
- 根据最优解构造问题的解。
动态规划的时间复杂度通常为 O ( n 2 ) O(n^2) O(n2)或 O ( n 3 ) O(n^3) O(n3),空间复杂度为O(n),其中n表示问题规模。在实际应用中,为了减少空间复杂度,通常可以使用滚动数组等技巧来优化动态规划算法。