* 动态规划

要注意动态规划和分而治之(归并排序和快速排序算法中用到的那种)是不 同的方法。

分而治之方法是把问题分解成相互独立的子问题,然后组合它们的答 案,

而动态规划则是将问题分解成相互依赖的子问题。

用动态规划解决问题时,要遵循三个重要步骤:

(1) 定义子问题;
(2) 实现要反复执行而解决子问题的部分(这一步要参考前一节讨论的递归的步骤);

(3) 识别并求解出边界条件。

最少硬币找零问题是硬币找零问题的一个变种。硬币找零问题是给出要找零的钱数,以及可 用的硬币面额d1...dn及其数量,找出有多少种找零方法。最少硬币找零问题是给出要找零的钱数, 以及可用的硬币面额d1...dn及其数量,找到所需的最少的硬币个数。

例如,美国有以下面额(硬币):d1=1,d2=5,d3=10,d4=25。

如果要找36美分的零钱,我们可以用1个25美分、1个10美分和1个便士(1美分)。

* MinCoinChange.js

/**
 * Created by Mch on 10/4/18.
 */
function MinCoinChange(/* Array */ coins) {
    "use strict";
    this.coins = coins;
    this.cache = {};
}

/**
 * @param amount Number
 * @returns {*}
 */
MinCoinChange.prototype.makeChange = function (amount) {
    "use strict";
    var me = this, min = [], newMin, newAmount;
    if (!amount) { return []; }
    if (this.cache[amount]) { return this.cache[amount]; }

    this.coins.forEach(function(coin) {
        newAmount = amount - coin;
        if (newAmount >= 0) {
            newMin = me.makeChange(newAmount);
        }
        if (newAmount >= 0 &&
            (newMin.length < min.length - 1 || !min.length) &&
            (newMin.length || !newAmount)) {
            min = [ coin ].concat(newMin);
            console.debug("new Min " + min + ' for ' + amount);
        }
    });
    return (this.cache[amount] = min);
};

exports.MinCoinChange = MinCoinChange;

* index.js test

/**
 * Created by Mch on 10/4/18.
 */
var MinCoinChange = require ('./MinCoinChange.js').MinCoinChange;

var minCoinChange = new MinCoinChange([1, 5, 10, 25]);
console.log(minCoinChange.makeChange(36));

$ node index.js 

new Min 1 for 1

new Min 1,1 for 2

new Min 1,1,1 for 3

new Min 1,1,1,1 for 4

new Min 1,1,1,1,1 for 5

new Min 5 for 5

new Min 1,5 for 6

new Min 1,1,5 for 7

new Min 1,1,1,5 for 8

new Min 1,1,1,1,5 for 9

new Min 1,1,1,1,1,5 for 10

new Min 5,5 for 10

new Min 10 for 10

new Min 1,10 for 11

new Min 1,1,10 for 12

new Min 1,1,1,10 for 13

new Min 1,1,1,1,10 for 14

new Min 1,1,1,1,1,10 for 15

new Min 5,10 for 15

new Min 1,5,10 for 16

new Min 1,1,5,10 for 17

new Min 1,1,1,5,10 for 18

new Min 1,1,1,1,5,10 for 19

new Min 1,1,1,1,1,5,10 for 20

new Min 5,5,10 for 20

new Min 10,10 for 20

new Min 1,10,10 for 21

new Min 1,1,10,10 for 22

new Min 1,1,1,10,10 for 23

new Min 1,1,1,1,10,10 for 24

new Min 1,1,1,1,1,10,10 for 25

new Min 5,10,10 for 25

new Min 25 for 25

new Min 1,25 for 26

new Min 1,1,25 for 27

new Min 1,1,1,25 for 28

new Min 1,1,1,1,25 for 29

new Min 1,1,1,1,1,25 for 30

new Min 5,25 for 30

new Min 1,5,25 for 31

new Min 1,1,5,25 for 32

new Min 1,1,1,5,25 for 33

new Min 1,1,1,1,5,25 for 34

new Min 1,1,1,1,1,5,25 for 35

new Min 5,5,25 for 35

new Min 10,25 for 35

new Min 1,10,25 for 36

[ 1, 10, 25 ]

 

* MinCoins.php

<?php

class MinCoins {
    const MAX_INTEGER = 2147483647;
    /**
     * @param array $arr
     * @param int $aim
     * @return int
     */
    public static function minCoins1(array $arr, int $aim) {
        if (!is_array($arr) || count($arr)===0 || is_null($arr)) {
            return -1;
        }
        $n = count($arr);
        $max = self::MAX_INTEGER;
        $dp = new SplFixedArray($n);
        for ($i = 0; $i < $dp->getSize(); $i++) {
            $dp->offsetSet($i, new SplFixedArray($aim+1));
        }
        for ($j = 1; $j <= $aim; $j++) {
            $dp->offsetGet(0)->offsetSet($j, $max);
            if ($j - $arr[0] >= 0 && $dp->offsetGet(0)->offsetGet($j-$arr[0]) !== $max) {
                $dp->offsetGet(0)->offsetSet($j, $dp->offsetGet(0)->offsetGet($j-$arr[0])+1);
            }
        }
        for ($i = 1; $i < $n; $i++) {
            for ($j = 1; $j <= $aim; $j++) {
                $left = $max;
                if ($j - $arr[$i]>=0 && $dp->offsetGet($i)->offsetGet($j-$arr[$i]) !== $max) {
                    $left = $dp->offsetGet($i)->offsetGet($j-$arr[$i]) + 1;
                }
                $dp->offsetGet($i)->offsetSet($j, min($left, $dp->offsetGet($i-1)->offsetGet($j)));
            }
        }
        return $dp->offsetGet($n-1)->offsetGet($aim) !== $max ?
            $dp->offsetGet($n-1)->offsetGet($aim) : -1;
    }

// 空间压缩
    public static function minCoins2(array $arr, int $aim) {
        if (!is_array($arr) || count($arr)===0 || is_null($arr)) {
            return -1;
        }
        $n = count($arr);
        $max = self::MAX_INTEGER;;
        $dp = new SplFixedArray($aim + 1);
        for ($j = 1; $j <= $aim; $j++) {
            $dp[$j] = $max;
            if ($j-$arr[0]>=0 && $dp[$j-$arr[0]] !== $max) {
                $dp[$j] = $dp[$j-$arr[0]] + 1;
            }
        }
        for ($i = 1; $i < $n; $i++) {
            for ($j = 1; $j <= $aim; $j++) {
                $left = $max;
                if ($j-$arr[$i]>=0 && $dp[$j-$arr[$i]] !== $max) {
                    $left = $dp[$j-$arr[$i]] + 1;
                }
                $dp[$j] = min($left, $dp[$j]);
            }
        }
        return $dp[$aim] !== $max ? $dp[$aim]: -1;
    }

    public static function minCoins3(array $arr, int $aim) {
        if (!is_array($arr) || count($arr)===0 || is_null($arr)) {
            return -1;
        }
        $max = self::MAX_INTEGER;;

        $dp = new SplFixedArray($aim+1);
        for ($j = 1; $j <= $aim; $j++) {
            $dp[$j] = $max;
        }
        if ($arr[0] <= $aim) {
            $dp[$arr[0]] = 1;
        }
        // 左上角的某个位置
        for ($i = 1, $n = count($arr); $i < $n; $i++) {
            for ($j = $aim; $j > 0; $j--) {
                $leftup = $max;
                if ($j-$arr[$i]>=0 && $dp[$j-$arr[$i]] !== $max) {
                    $leftup = $dp[$j-$arr[$i]]+1;
                }
                $dp[$j] = min($leftup, $dp[$j]);
            }
        }
        return $dp[$aim] !== $max ? $dp[$aim] : -1;
    }


}

* test:

<?php
echo MinCoins::minCoins1([1,5,10,20,50], 93).PHP_EOL;  // 6
echo MinCoins::minCoins2([1,5,10,20,50], 93).PHP_EOL;  // 6


// SplFixedArray 赋值, 遍历 用法测试
// http://php.net/manual/en/class.splfixedarray.php