今天巖巖丟擲了一道 code war 上的題目,大意如下:
一個函式接收兩個引數,第一個引數是數字,第二個引數是數字陣列,求陣列裡的數字加起來等於第一個引數的所有情況,可以無限次使用陣列裡的數字。
譬如 5, [1, 2, 5],總共有
- 1 + 1 + 1 + 1 + 1 = 5
- 1 + 1 + 1 + 2 = 5
- 1 + 2 + 2 = 5
- 5 = 5
這樣 4 種情況,所以返回 4。
第一個引數為 0 的時候,返回 1。
後來我發現在 leet code 也有類似的題,是個找零問題,就是不同面值的硬幣組合成一個數有多少種情況。還挺有意思的,我就做了一下,用了遞迴:
const change = function (sum, arr) {
const nums = arr.sort((a, b) => a - b);
return add(sum, nums);
};
const add = function (sum, nums) {
const min = nums[0];
if (sum === 0 || sum === min) return 1;
let res = 0;
if (nums.includes(sum)) res += 1;
for (let i = 0; i < nums.length; i++) {
const last = sum - nums[i];
if (last >= nums[i]) res += add(last, nums.slice(i));
}
return res;
}
複製程式碼
在 code war 上是成功提交了,但在 leet code 上卻超時了,效能太差了。然後巖巖在 code war 的答案裡看到了這樣一段程式碼:
const countChange = (m, c) => {
const a = [1].concat(Array(m).fill(0));
for (let i = 0, l = c.length; i < l; i++)
for (let x = c[i], j = x; j <= m; j++)
a[j] += a[j - x];
return a[m];
}
複製程式碼
看得我倆十分懵逼,苦思半天還是懵逼。於是我上網搜到了相同解法的 C++ 版本:
using namespace std;
class Solution
{
public:
int change(int amount, vector<int>& coins)
{
vector<int> dp(amount + 1, 0);
dp[0] = 1;
for (int coin : coins)
{
for (int i = coin; i <= amount; ++i)
{
dp[i] += dp[i - coin];
}
}
return dp[amount];
}
};
複製程式碼
文章 說是用的動態規劃(知道了這點很關鍵),雖然沒具體解釋,但這個版本的命名比上面那個 JS 版本實在是好懂太多了,dp 就是 dynamic programming 嘛,所以已經可以勉強推導他的思路:
dp 是一個長為 amount + 1 的表,依次用來記錄組合成 0、1、2、3、、、amount 各有多少種情況,dp[0] 初始為 1,其他都初始為 0。
接下來迴圈硬幣值,記錄從當前硬幣值到 amount 的所有組合情況。狀態轉移方程為:dp[i] += dp[i – coin]。什麼意思呢?假設我們求 [1, 2, 5] 這三個面值組成 5 的情況,現在我先拿出一個 2,那我是不是隻要再有一個 3 就可以得到 5 了,那我只要計算有多少種組合成 3 的情況就好了,即當 coin = 2 的時候,dp[5]~new~ = dp[5]~old~ + dp[3],以此類推。
for (int coin : coins)
{
for (int i = coin; i <= amount; ++i)
{
dp[i] += dp[i - coin];
}
}
複製程式碼
以上這段程式碼的意思就是先計算我拿出 1 的時候,組合成 12345 的情況數,再計算當我拿出 2 的時候,組合成 2345 的情況數(加上拿出 1 時候的情況數),再計算拿出 5 的時候,組合成 5 的情況數(加上拿出 1 和拿出 2 時候的情況數),最後得出的 dp[5] 就是我們想要的結果。你可能會問我們要的是 dp[5],中間的 dp[1]dp[2]…dp[4] 有什麼用,其實這就是動態規劃的精髓,會把子問題的解記錄(快取)下來,因為這些子問題會在計算過程中多次用到,就不需要每次都計算了。
上述解法的大體思路其實和下面這個樸素遞迴是相似的,都是把問題分解為子問題進行求解,動態規劃強就強在會快取子問題的解避免重複計算從而提高效率。
const countChange = function(money, coins) {
if (money < 0 || coins.length === 0)
return 0
else if (money === 0)
return 1
else
return countChange(money - coins[0], coins) + countChange(money, coins.slice(1))
}
複製程式碼
以後當你碰到一個問題,它可以分解為多個子問題,並且子問題有重疊時,就用動態規劃吧。
啊,我真是太菜了……一個動態規劃的題搞了半天……