找零問題與動態規劃

今天巖巖拋出了一道 code war 上的題目,大意以下:javascript

一個函數接收兩個參數,第一個參數是數字,第二個參數是數字數組,求數組裏的數字加起來等於第一個參數的全部狀況,能夠無限次使用數組裏的數字。 譬如 5, [1, 2, 5],總共有java

  • 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 嘛,因此已經能夠勉強推導他的思路:ui

dp 是一個長爲 amount + 1 的表,依次用來記錄組合成 0、一、二、三、、、amount 各有多少種狀況,dp[0] 初始爲 1,其餘都初始爲 0。spa

接下來循環硬幣值,記錄從當前硬幣值到 amount 的全部組合狀況。狀態轉移方程爲:dp[i] += dp[i - coin]。什麼意思呢?假設咱們求 [1, 2, 5] 這三個面值組成 5 的狀況,如今我先拿出一個 2,那我是否是隻要再有一個 3 就能夠獲得 5 了,那我只要計算有多少種組合成 3 的狀況就行了,即當 coin = 2 的時候,dp[5]~new~ = dp[5]~old~ + dp[3],以此類推。.net

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] 有什麼用,其實這就是動態規劃的精髓,會把子問題的解記錄(緩存)下來,由於這些子問題會在計算過程當中屢次用到,就不須要每次都計算了。code

上述解法的大致思路其實和下面這個樸素遞歸是類似的,都是把問題分解爲子問題進行求解,動態規劃強就強在會緩存子問題的解避免重複計算從而提升效率。

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))
}

複製代碼

之後當你碰到一個問題,它能夠分解爲多個子問題,而且子問題有重疊時,就用動態規劃吧。

啊,我真是太菜了……一個動態規劃的題搞了半天……

相關文章
相關標籤/搜索