前言
摘自:代码随想录
动态规划五部曲:
- 确定dp数组(dp table)及其下标的含义
- 确定递推公式
- 初始化dp数组
- 确定遍历顺序
- 举例推导dp数组
1 最少硬币数
动规五部曲分析如下:
- 确定dp数组以及下标的含义
dp[j]:凑足总额为 j 所需钱币的最少个数为dp[j]
- 确定递推公式
凑足总额为j - coins[i]的最少个数为dp[j - coins[i]],
那么只需要加上一个钱币coins[i]即dp[j - coins[i]] + 1就是dp[j](考虑coins[i])
所以dp[j] 要取所有 dp[j - coins[i]] + 1 中最小的。
递推公式:dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
- dp数组如何初始化
首先凑足总金额为0所需钱币的个数一定是0,那么dp[0] = 0;
其他下标对应的数值呢?
考虑到递推公式的特性,dp[j]必须初始化为一个最大的数,否则就会在min(dp[j - coins[i]] + 1, dp[j])比较的过程中被初始值覆盖。
所以下标非0的元素都是应该是最大值。
int[] dp = new int[amount + 1];
// 往数组dp里面填充某个数,这里选择amount+1,就是最大的值
Arrays.fill(dp, amount+1);
dp[0] = 0;
- 确定遍历顺序
有两种方式:
第一种:外循环遍历金额,内循环遍历硬币面额。
第二种:外循环遍历硬币面面额,内循环遍历金额。
这两种遍历顺序对应的意义如下:
-
外循环遍历金额,内循环遍历硬币面额:
这种遍历顺序的意义是在计算找零过程中,我们首先考虑金额的变化,然后再考虑不同的硬币面额。
也就是说,我们固定一个金额,尝试使用不同的硬币面额来找零。这样做的好处是可以利用之前已经计算出来的金额的最少硬币数,快速得到当前金额的最优解。由于金额是从小到大递增的,所以我们在计算每个金额的最优解时,可以利用前面较小金额的最优解已经被计算出来的特点。
// 遍历金额
for (int i = 1; i <= amount; i++) {
// 遍历硬币面额
for (int j = 0; j < coins.length; j++) {
if (coins[j] <= i) {
dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
}
}
}
-
外循环遍历硬币面额,内循环遍历金额:
这种遍历顺序的意义是在计算找零过程中,我们首先考虑不同的硬币面额,然后再考虑不同的金额。
也就是说,我们固定一个硬币面额,尝试在不同的金额下进行找零。这样做的好处是可以保证我们将所有可能的硬币面额都考虑到,并且在计算每个金额的最优解时,可以利用之前已经计算出来的较小金额的最优解。由于硬币面额是从小到大递增的,所以我们在计算每个金额的最优解时,可以利用之前较小硬币面额的最优解已经被计算出来的特点。
// 遍历硬币面额
for (int coin : coins){
// 遍历金额
for (int i = 1; i <= amount; i++) {
if(coin <= i){
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
全部代码如下:
第一种:
class Solution {
public int coinChange(int[] coins, int amount) {
// 初始化dp数组
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1);
dp[0] = 0;
// 遍历金额
for (int i=1; i <= amount; i++) {
// 遍历硬币面额
for (int j=0; j < coins.length; j++){
if(coins[j] <= i){
dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
}
第二种:
class Solution {
public int coinChange(int[] coins, int amount) {
// 初始化dp数组
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1);
dp[0] = 0;
// 遍历硬币面额
for (int coin : coins){
// 遍历金额
for (int i = 1; i <= amount; i++) {
if(coin <= i){
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
}
2 最长连续递增子序列
动规五部曲分析如下:
- 确定dp数组以及下标的含义
dp数组:表示以当前元素为结尾的最长连续递增序列的长度。
dp[i]表示以nums[i]为结尾的最长连续递增序列的长度。
- 确定递推公式
如果nums[i] > nums[i-1],则dp[i] = dp[i-1] + 1;否则dp[i] = 1。
- dp数组如何初始化
我们将dp数组的所有元素初始化为1,因为每个元素都可以作为一个单独的递增序列。
- 确定遍历顺序
从第二个元素开始遍历:
for(int i=0; i < nums.length