本來揹包問題,比較直觀的解釋,是用二維的轉移方程,但簡潔起見,很多地方以及下文的介紹都是用一維的轉移方程,在二維到一維的轉換中要注意,01揹包和完全揹包的計算順序,01揹包的dp數組是從後向前計算更新,完全揹包的dp數組是從w[i]開始向後更新。
要想理解計算順序的不同,先看一下二維的轉移方程是什麼樣的:
01揹包
表示前件物品恰好裝入容量爲的揹包中所能獲得的最大價值,共件物品,每件物品的重量,價值,揹包容量
邊界
完全揹包
表示前件物品恰好裝入容量爲的揹包中所能獲得的最大價值
邊界
可以看到,01揹包需要把前i-1件物品的dp狀態保留下來,而且更新的時候不能修改它,所以01揹包的一維轉移方程從後向前計算,但完全揹包需要新鮮出爐剛算出來的dp[i]一行的狀態,所以如果轉一維的話,計算順序就從小到大。
01揹包
w數組是物品的weight,v數組是物品的value,C是揹包的容量,每個物品要麼取要麼不取,定有一種取法,使得物品總價值最大,求這個最大價值。
dp數組表示重量爲i時的最大價值總和。
/// <summary>
/// 01揹包問題原問題,該函數空間複雜度 1*C
/// </summary>
/// <param name="w"> weights </param>
/// <param name="v"> values </param>
/// <param name="C"> capacity </param>
/// <returns></returns>
public int Knapsack01(int[] w, int[] v, int C)
{
var n = w.Length;
if (n == 0) return 0;
var dp = new int[C + 1];
//對於第一件物品,計算dp數組的值
for (var i = 0; i <= C; i++)
dp[i] = w[0] <= i ? v[0] : 0;
//對於之後的每一件物品
for (var i = 1; i < n; i++)
{
//從C向前算,對於每一個總重量,看不拿物品i(dp[j])的價值大,還是拿物品i後(dp[j-w[i]]+v[i])的價值大
for (var j = C; j >= w[i]; j--)
{
dp[j] = Math.Max(dp[j], dp[j - w[i]] + v[i]);
}
}
return dp[C];
}
完全揹包問題
/// <summary>
/// 完全揹包問題
/// </summary>
/// <param name="w"> weights </param>
/// <param name="v"> values </param>
/// <param name="C"> capacity </param>
/// <returns></returns>
public int KnapsackTotal(int[] w, int[] v, int C)
{
var n = w.Length;
if (n == 0) return 0;
var dp = new int[C + 1];
//對於第一件物品,計算dp數組的值
for (var i = 0; i <= C; i++)
dp[i] = w[0] <= i ? v[i] : 0;
//對於每一件物品
for (var i = 1; i < n; i++)
{
//對於每一個重量,從w[i]到C的計算順序,這個順序就蘊含着二維轉移方程裏面的前i件物品
for (var j = w[i]; j <= C; j++)
{
dp[j] = Math.Max(dp[j], dp[j - w[i]] + v[i]);
}
}
return dp[C];
}
零錢兌換(求最少硬幣個數)
給定不同面額的硬幣 coins 和一個總金額 amount。編寫一個函數來計算可以湊成總金額所需的最少的硬幣個數。如果沒有任何一種硬幣組合能組成總金額,返回 -1。
dp數組表示對於每種金額可能的最少硬幣個數。
322. Coin Change
/// <summary>
/// 類似01揹包問題
/// </summary>
public int CoinChange(int[] coins, int amount)
{
var dp = new int[amount + 1];
//初始化每一種金額所需硬幣個數爲int.MaxValue
for (var i = 0; i < amount + 1; i++)
{
dp[i] = int.MaxValue;
}
dp[0] = 0;//總金額爲0就對應一個硬幣都不需要的情況
//對於每一個硬幣
for (var i = 0; i < coins.Length; i++)
{
//對於每一個金額
for (var j = coins[i]; j < amount + 1; j++)
{
//如果j - coins[i]能拼出來
if (dp[j - coins[i]] != int.MaxValue)
{
dp[j] = Math.Min(dp[j], dp[j - coins[i]] + 1);
}
}
}
return dp[amount] == int.MaxValue ? -1 : dp[amount];
}
零錢兌換II(求硬幣組合數)
給定不同面額的硬幣和一個總金額。寫出函數來計算可以湊成總金額的硬幣組合數。假設每一種面額的硬幣有無限個。
518. Coin Change II
/// <summary>
/// 518. Coin Change II https://leetcode-cn.com/problems/coin-change-2/
/// </summary>
public int Change(int amount, int[] coins)
{
var dp = new int[amount + 1];
//總金額爲0的方法只有一種,就是啥都不選
dp[0] = 1;
for (int i = 0; i < coins.Length; i++)
{
for (int j = coins[i]; j <= amount; j++)
{
dp[j] += dp[j - coins[i]];
}
}
return dp[amount];
}
組合總和IV
給定一個由正整數組成且不存在重複數字的數組,找出和爲給定目標正整數的組合的個數。
【注意】題目要求不同順序視爲不同組合,注意兩個for循環的次序。
377. Combination Sum IV
/// <summary>
/// 377. Combination Sum IV https://leetcode.com/problems/combination-sum-iv/, google, facebook
/// 類似上一題,但這道題不同順序算作不同的組合
/// </summary>
public int CombinationSum4(int[] nums, int target)
{
var dp = new int[target + 1];
//組成和爲0的方法只有一種,就是啥都不選
dp[0] = 1;
for (int i = 1; i <= target; i++)
{
for (int j = 0; j < nums.Length; j++)
{
if (nums[j] <= i)
{
dp[i] = dp[i] + dp[i - nums[j]];
}
}
}
return dp[target];
}