70. 爬樓梯(進階版)
卡碼網(wǎng):57. 爬樓梯(opens new window)
假設你正在爬樓梯。需要 n 階你才能到達樓頂。
每次你可以爬至多m (1 <= m < n)個臺階。你有多少種不同的方法可以爬到樓頂呢?
注意:給定 n 是一個正整數(shù)。
輸入描述:輸入共一行,包含兩個正整數(shù),分別表示n, m
輸出描述:輸出一個整數(shù),表示爬到樓頂?shù)姆椒〝?shù)。
輸入示例:3 2
輸出示例:3
提示:
當 m = 2,n = 3 時,n = 3 這表示一共有三個臺階,m = 2 代表你每次可以爬一個臺階或者兩個臺階。
此時你有三種方法可以爬到樓頂。
1 階 + 1 階 + 1 階段
1 階 + 2 階
2 階 + 1 階
思路
之前講這道題目的時候,因為還沒有講背包問題,所以就只是講了一下爬樓梯最直接的動規(guī)方法(斐波那契)。
這次終于講到了背包問題,我選擇帶錄友們再爬一次樓梯!
這道題目 我們在動態(tài)規(guī)劃:爬樓梯 (opens new window)中已經(jīng)講過一次了,這次我又給本題加點料,力扣上沒有原題,所以可以在卡碼網(wǎng)57. 爬樓梯 (opens new window)上來刷這道題目。
我們之前做的 爬樓梯 是只能至多爬兩個臺階。
這次改為:一步一個臺階,兩個臺階,三個臺階,…,直到 m個臺階。問有多少種不同的方法可以爬到樓頂呢?
這又有難度了,這其實是一個完全背包問題。
1階,2階,… m階就是物品,樓頂就是背包。
每一階可以重復使用,例如跳了1階,還可以繼續(xù)跳1階。
問跳到樓頂有幾種方法其實就是問裝滿背包有幾種方法。
此時大家應該發(fā)現(xiàn)這就是一個完全背包問題了!
和昨天的題目動態(tài)規(guī)劃:377. 組合總和 Ⅳ (opens new window)基本就是一道題了。
動規(guī)五部曲分析如下:
確定dp數(shù)組以及下標的含義
dp[i]:爬到有i個臺階的樓頂,有dp[i]種方法。
確定遞推公式
在動態(tài)規(guī)劃:494.目標和 (opens new window)、 動態(tài)規(guī)劃:518.零錢兌換II (opens new window)、動態(tài)規(guī)劃:377. 組合總和 Ⅳ (opens new window)中我們都講過了,求裝滿背包有幾種方法,遞推公式一般都是dp[i] += dp[i - nums[j]];
本題呢,dp[i]有幾種來源,dp[i - 1],dp[i - 2],dp[i - 3] 等等,即:dp[i - j]
那么遞推公式為:dp[i] += dp[i - j]
dp數(shù)組如何初始化
既然遞歸公式是 dp[i] += dp[i - j],那么dp[0] 一定為1,dp[0]是遞歸中一切數(shù)值的基礎所在,如果dp[0]是0的話,其他數(shù)值都是0了。
下標非0的dp[i]初始化為0,因為dp[i]是靠dp[i-j]累計上來的,dp[i]本身為0這樣才不會影響結果
確定遍歷順序
這是背包里求排列問題,即:1、2 步 和 2、1 步都是上三個臺階,但是這兩種方法不一樣!
所以需將target放在外循環(huán),將nums放在內(nèi)循環(huán)。
每一步可以走多次,這是完全背包,內(nèi)循環(huán)需要從前向后遍歷。
舉例來推導dp數(shù)組
介于本題和動態(tài)規(guī)劃:377. 組合總和 Ⅳ (opens new window)幾乎是一樣的,這里我就不再重復舉例了。
以上分析完畢,C++代碼如下:
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n, m;
while (cin >> n >> m) {
vector<int> dp(n + 1, 0);
dp[0] = 1;
for (int i = 1; i <= n; i++) { // 遍歷物品
for (int j = 1; j <= m; j++) { // 遍歷背包
if (i - j >= 0) dp[i] += dp[i - j];
}
}
cout << dp[n] << endl;
}
}
322. 零錢兌換
力扣題目鏈接(opens new window)
給定不同面額的硬幣 coins 和一個總金額 amount。編寫一個函數(shù)來計算可以湊成總金額所需的最少的硬幣個數(shù)。如果沒有任何一種硬幣組合能組成總金額,返回 -1。
你可以認為每種硬幣的數(shù)量是無限的。
示例 1:
輸入:coins = [1, 2, 5], amount = 11
輸出:3
解釋:11 = 5 + 5 + 1
示例 2:
輸入:coins = [2], amount = 3
輸出:-1
示例 3:
輸入:coins = [1], amount = 0
輸出:0
示例 4:
輸入:coins = [1], amount = 1
輸出:1
示例 5:
輸入:coins = [1], amount = 2
輸出:2
提示:
1 <= coins.length <= 12
1 <= coins[i] <= 2^31 - 1
0 <= amount <= 10^4
思路
在動態(tài)規(guī)劃:518.零錢兌換II (opens new window)中我們已經(jīng)兌換一次零錢了,這次又要兌換,套路不一樣!
題目中說每種硬幣的數(shù)量是無限的,可以看出是典型的完全背包問題。
動規(guī)五部曲分析如下:
確定dp數(shù)組以及下標的含義
dp[j]:湊足總額為j所需錢幣的最少個數(shù)為dp[j]
確定遞推公式
湊足總額為j - coins[i]的最少個數(shù)為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數(shù)組如何初始化
首先湊足總金額為0所需錢幣的個數(shù)一定是0,那么dp[0] = 0;
其他下標對應的數(shù)值呢?
考慮到遞推公式的特性,dp[j]必須初始化為一個最大的數(shù),否則就會在min(dp[j - coins[i]] + 1, dp[j])比較的過程中被初始值覆蓋。
所以下標非0的元素都是應該是最大值。
代碼如下:
vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0;
確定遍歷順序
本題求錢幣最小個數(shù),那么錢幣有順序和沒有順序都可以,都不影響錢幣的最小個數(shù)。
所以本題并不強調(diào)集合是組合還是排列。
如果求組合數(shù)就是外層for循環(huán)遍歷物品,內(nèi)層for遍歷背包。
如果求排列數(shù)就是外層for遍歷背包,內(nèi)層for循環(huán)遍歷物品。
在動態(tài)規(guī)劃專題我們講過了求組合數(shù)是動態(tài)規(guī)劃:518.零錢兌換II (opens new window),求排列數(shù)是動態(tài)規(guī)劃:377. 組合總和 Ⅳ (opens new window)。
所以本題的兩個for循環(huán)的關系是:外層for循環(huán)遍歷物品,內(nèi)層for遍歷背包或者外層for遍歷背包,內(nèi)層for循環(huán)遍歷物品都是可以的!
那么我采用coins放在外循環(huán),target在內(nèi)循環(huán)的方式。
本題錢幣數(shù)量可以無限使用,那么是完全背包。所以遍歷的內(nèi)循環(huán)是正序
綜上所述,遍歷順序為:coins(物品)放在外循環(huán),target(背包)在內(nèi)循環(huán)。且內(nèi)循環(huán)正序。
舉例推導dp數(shù)組
以輸入:coins = [1, 2, 5], amount = 5為例
dp[amount]為最終結果。
以上分析完畢,C++ 代碼如下:
// 版本一
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0;
for (int i = 0; i < coins.size(); i++) { // 遍歷物品
for (int j = coins[i]; j <= amount; j++) { // 遍歷背包
if (dp[j - coins[i]] != INT_MAX) { // 如果dp[j - coins[i]]是初始值則跳過
dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
}
}
}
if (dp[amount] == INT_MAX) return -1;
return dp[amount];
}
};
時間復雜度: O(n * amount),其中 n 為 coins 的長度
空間復雜度: O(amount)
對于遍歷方式遍歷背包放在外循環(huán),遍歷物品放在內(nèi)循環(huán)也是可以的,我就直接給出代碼了
// 版本二
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0;
for (int i = 1; i <= amount; i++) { // 遍歷背包
for (int j = 0; j < coins.size(); j++) { // 遍歷物品
if (i - coins[j] >= 0 && dp[i - coins[j]] != INT_MAX ) {
dp[i] = min(dp[i - coins[j]] + 1, dp[i]);
}
}
}
if (dp[amount] == INT_MAX) return -1;
return dp[amount];
}
};
Python:
先遍歷物品 后遍歷背包
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
dp = [float('inf')] * (amount + 1) # 創(chuàng)建動態(tài)規(guī)劃數(shù)組,初始值為正無窮大
dp[0] = 0 # 初始化背包容量為0時的最小硬幣數(shù)量為0
for coin in coins: # 遍歷硬幣列表,相當于遍歷物品
for i in range(coin, amount + 1): # 遍歷背包容量
if dp[i - coin] != float('inf'): # 如果dp[i - coin]不是初始值,則進行狀態(tài)轉移
dp[i] = min(dp[i - coin] + 1, dp[i]) # 更新最小硬幣數(shù)量
if dp[amount] == float('inf'): # 如果最終背包容量的最小硬幣數(shù)量仍為正無窮大,表示無解
return -1
return dp[amount] # 返回背包容量為amount時的最小硬幣數(shù)量
先遍歷背包 后遍歷物品
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
dp = [float('inf')] * (amount + 1) # 創(chuàng)建動態(tài)規(guī)劃數(shù)組,初始值為正無窮大
dp[0] = 0 # 初始化背包容量為0時的最小硬幣數(shù)量為0
for i in range(1, amount + 1): # 遍歷背包容量
for j in range(len(coins)): # 遍歷硬幣列表,相當于遍歷物品
if i - coins[j] >= 0 and dp[i - coins[j]] != float('inf'): # 如果dp[i - coins[j]]不是初始值,則進行狀態(tài)轉移
dp[i] = min(dp[i - coins[j]] + 1, dp[i]) # 更新最小硬幣數(shù)量
if dp[amount] == float('inf'): # 如果最終背包容量的最小硬幣數(shù)量仍為正無窮大,表示無解
return -1
return dp[amount] # 返回背包容量為amount時的最小硬幣數(shù)量
先遍歷物品 后遍歷背包(優(yōu)化版)
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for coin in coins:
for i in range(coin, amount + 1): # 進行優(yōu)化,從能裝得下的背包開始計算,則不需要進行比較
# 更新湊成金額 i 所需的最少硬幣數(shù)量
dp[i] = min(dp[i], dp[i - coin] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
先遍歷背包 后遍歷物品(優(yōu)化版)
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for i in range(1, amount + 1): # 遍歷背包容量
for coin in coins: # 遍歷物品
if i - coin >= 0:
# 更新湊成金額 i 所需的最少硬幣數(shù)量
dp[i] = min(dp[i], dp[i - coin] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
279.完全平方數(shù)
力扣題目鏈接(opens new window)
給定正整數(shù) n,找到若干個完全平方數(shù)(比如 1, 4, 9, 16, …)使得它們的和等于 n。你需要讓組成和的完全平方數(shù)的個數(shù)最少。
給你一個整數(shù) n ,返回和為 n 的完全平方數(shù)的 最少數(shù)量 。
完全平方數(shù) 是一個整數(shù),其值等于另一個整數(shù)的平方;換句話說,其值等于一個整數(shù)自乘的積。例如,1、4、9 和 16 都是完全平方數(shù),而 3 和 11 不是。
示例 1:
輸入:n = 12
輸出:3
解釋:12 = 4 + 4 + 4
示例 2:
輸入:n = 13
輸出:2
解釋:13 = 4 + 9
提示:
1 <= n <= 10^4
思路
可能剛看這種題感覺沒啥思路,又平方和的,又最小數(shù)的。
我來把題目翻譯一下:完全平方數(shù)就是物品(可以無限件使用),湊個正整數(shù)n就是背包,問湊滿這個背包最少有多少物品?
感受出來了沒,這么濃厚的完全背包氛圍,而且和昨天的題目動態(tài)規(guī)劃:322. 零錢兌換 (opens new window)就是一樣一樣的!
動規(guī)五部曲分析如下:
確定dp數(shù)組(dp table)以及下標的含義
dp[j]:和為j的完全平方數(shù)的最少數(shù)量為dp[j]
確定遞推公式
dp[j] 可以由dp[j - i * i]推出, dp[j - i * i] + 1 便可以湊成dp[j]。
此時我們要選擇最小的dp[j],所以遞推公式:dp[j] = min(dp[j - i * i] + 1, dp[j]);
dp數(shù)組如何初始化
dp[0]表示 和為0的完全平方數(shù)的最小數(shù)量,那么dp[0]一定是0。
有同學問題,那0 * 0 也算是一種啊,為啥dp[0] 就是 0呢?
看題目描述,找到若干個完全平方數(shù)(比如 1, 4, 9, 16, …),題目描述中可沒說要從0開始,dp[0]=0完全是為了遞推公式。
非0下標的dp[j]應該是多少呢?
從遞歸公式dp[j] = min(dp[j - i * i] + 1, dp[j]);中可以看出每次dp[j]都要選最小的,所以非0下標的dp[j]一定要初始為最大值,這樣dp[j]在遞推的時候才不會被初始值覆蓋。
確定遍歷順序
我們知道這是完全背包,
如果求組合數(shù)就是外層for循環(huán)遍歷物品,內(nèi)層for遍歷背包。
如果求排列數(shù)就是外層for遍歷背包,內(nèi)層for循環(huán)遍歷物品。
在動態(tài)規(guī)劃:322. 零錢兌換 (opens new window)中我們就深入探討了這個問題,本題也是一樣的,是求最小數(shù)!
所以本題外層for遍歷背包,內(nèi)層for遍歷物品,還是外層for遍歷物品,內(nèi)層for遍歷背包,都是可以的!
我這里先給出外層遍歷背包,內(nèi)層遍歷物品的代碼:
ector<int> dp(n + 1, INT_MAX);
dp[0] = 0;
for (int i = 0; i <= n; i++) { // 遍歷背包
for (int j = 1; j * j <= i; j++) { // 遍歷物品
dp[i] = min(dp[i - j * j] + 1, dp[i]);
}
}
舉例推導dp數(shù)組
已輸入n為5例,dp狀態(tài)圖如下:
dp[0] = 0 dp[1] = min(dp[0] + 1) = 1 dp[2] = min(dp[1] + 1) = 2 dp[3] = min(dp[2] + 1) = 3 dp[4] = min(dp[3] + 1, dp[0] + 1) = 1 dp[5] = min(dp[4] + 1, dp[1] + 1) = 2
最后的dp[n]為最終結果。
以上動規(guī)五部曲分析完畢C++代碼如下:
// 版本一
class Solution {
public:
int numSquares(int n) {
vector<int> dp(n + 1, INT_MAX);
dp[0] = 0;
for (int i = 0; i <= n; i++) { // 遍歷背包
for (int j = 1; j * j <= i; j++) { // 遍歷物品
dp[i] = min(dp[i - j * j] + 1, dp[i]);
}
}
return dp[n];
}
};
時間復雜度: O(n * √n)
空間復雜度: O(n)
同樣我在給出先遍歷物品,在遍歷背包的代碼,一樣的可以AC的。
// 版本二
class Solution {
public:
int numSquares(int n) {
vector<int> dp(n + 1, INT_MAX);
dp[0] = 0;
for (int i = 1; i * i <= n; i++) { // 遍歷物品
for (int j = i * i; j <= n; j++) { // 遍歷背包
dp[j] = min(dp[j - i * i] + 1, dp[j]);
}
}
return dp[n];
}
};
Python:
先遍歷物品, 再遍歷背包
class Solution:
def numSquares(self, n: int) -> int:
dp = [float('inf')] * (n + 1)
dp[0] = 0
for i in range(1, n + 1): # 遍歷背包
for j in range(1, int(i ** 0.5) + 1): # 遍歷物品
# 更新湊成數(shù)字 i 所需的最少完全平方數(shù)數(shù)量
dp[i] = min(dp[i], dp[i - j * j] + 1)
return dp[n]
先遍歷背包, 再遍歷物品文章來源:http://www.zghlxwxcb.cn/news/detail-796415.html
class Solution:
def numSquares(self, n: int) -> int:
dp = [float('inf')] * (n + 1)
dp[0] = 0
for i in range(1, int(n ** 0.5) + 1): # 遍歷物品
for j in range(i * i, n + 1): # 遍歷背包
# 更新湊成數(shù)字 j 所需的最少完全平方數(shù)數(shù)量
dp[j] = min(dp[j - i * i] + 1, dp[j])
return dp[n]
其他版本文章來源地址http://www.zghlxwxcb.cn/news/detail-796415.html
class Solution:
def numSquares(self, n: int) -> int:
# 創(chuàng)建動態(tài)規(guī)劃數(shù)組,初始值為最大值
dp = [float('inf')] * (n + 1)
# 初始化已知情況
dp[0] = 0
# 遍歷背包容量
for i in range(1, n + 1):
# 遍歷完全平方數(shù)作為物品
j = 1
while j * j <= i:
# 更新最少完全平方數(shù)的數(shù)量
dp[i] = min(dp[i], dp[i - j * j] + 1)
j += 1
# 返回結果
return dp[n]
到了這里,關于leetcode 動態(tài)規(guī)劃(爬樓梯、零錢兌換、完全平方數(shù))的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!