1. 爬楼梯问题:

从地面第0层,爬到第10层。每次只能上1节或者2节台阶。共有多少种上法。

爬到任意一层,或者说爬到10层的时候,考虑一下你上一步是从哪里来的。你可能从第9层上来的,也可能从第8层上来,但不可能从第7,或者6等等上来。因为题目规定一次只能上1或者2个台阶。所以如何上到第10级台阶转变为:如何上到第9级的个数 + 如何上到第8级的个数。写成函数

 f(10) = f(10-1)+f(10-2) = f(9) + f(8)

可以用递归(从第10级开始),或者递推(从第0级开始)解决。

How to Compute the Min Cost of Climbing Stairs via Dynamic Programming Algorithm? | Algorithms, Blockchain and Cloud (helloacm.com)

Coding Exercise - Climbing Stairs - Fibonacci Numbers - C++ - Online Judge | Algorithms, Blockchain and Cloud (helloacm.com)


2. 兑换钱问题:

有面值为2,3,5的硬币,每种无数个。要拿出正好21元。有多少种?

是不是和爬楼梯问题一样。问题转换成:一次只能上2,3,5级台阶,上到21层,共有多少种?

要理解这个问题的求解,第一:需要理解了我要到达的状态,与那个状态的前一个是什么的关系。比如我要上到21层,肯定是从 19层(21-2),或者18层( 21-3),或者16层(21-5)上来的。也就是说兑换成21元,肯定是从 21-2=19,加上2元;或者21-3=18,加上3元;或者21-5=16,加上5元得来的。 f(21)= f(21-2)+f(21-3)+f(21-5) 即: f(n)= f(n-2) +f(n-3) +f(n-5) .

第二:上面递归的出口

即只有当最终n传入是2,3,5时,才是能正常兑换的最初始状态。也就是说,你不管想兑换出多少钱,首先拿出的硬币肯定是这三个【2,3,5】之一吧。任何一个兑换值,最后一个找到的是这三个之一,说明就是找到兑换了。


上面问题都差点才会涉及动归。将兑换钱改成:不看有多少种,而是哪种所需硬币个数最少。这就是动归了。因为涉及了从子问题中,选择最优解,即f(n)和f(n-d)+1到底谁更少(d就是硬币的种类,任取一个)。选取最小的就是动归了,我不是都要。

相应变形公式不再是相加,而是找出最小值:f(21)=MIN( min(  f(21-2), f(21-3), f(21-5) )+1,f(21) ) ; f(0)=0 清爽吧

dp公式:f(n)=min(f(n), f(n-d)+1)  其中n是总钱数,d是所有的硬币面值。

Teaching Kids Programming – Dynamic Programming Algorithm to Compute Minimum Number of Coins | Algorithms, Blockchain and Cloud (helloacm.com) 类似一个完全背包(硬币不受限),可以用 DFS(深度优先),DP(自顶向下/自底向上)。贪心不适用。

2.1 递推:f(0)->f(1)...... 想想楼梯,自底向上,从第0个台阶,到第一个台阶,到第二个......

class Solution:
    def minCoinsRequired(self, denominations, amount):
        if not denominations:
            return 0
        dp = [0] + [math.inf] * amount
        for a in range(amount + 1):
            for d in denominations:
                if a >= d and dp[a - d] != math.inf:
                    dp[a] = min(dp[a], dp[a - d] + 1)
        return dp[amount] if dp[amount] != math.inf else -1

c++的递推:

int solve(vector<int>& denominations, int amount) {
    //每个目标的兑换的最小值为初始状态
    int* optimal=new int [amount + 1];

    for (int i = 0; i <= amount; i++)
        optimal[i] = -1;

    //已有的硬币类型为最少。只有一个就够了。
    for (auto coin : denominations)
        if (coin <= amount)//排除调硬币值都比要兑换值大的。比如你拿10元要兑换1元出来,无解的
            optimal[coin] = 1;

    //从最小兑换值算起。我们要兑换1元,2元...直到amount
    //因为后面的兑换值总是需要前面的最优值再加1获取。
    //这也就是强调的,如果想要21yuan,那肯定是从 18的最优值+1(3分面值)、或者 19最优值+1(2分面值)....
    for (int i = 1; i <= amount; i++) {
        //这里每种硬币都试一遍。即 18的最优值+1(3分面值)与它本身值比较一下,取小的
        //另外就是注意这里硬币顺序是不重要的。类似你要找最大的苹果,先找第一筐里最小的,和第二筐里最小的比,取最小;
        //与先去第二筐在和第一筐里比,是没关系的。
        for (auto coin : denominations) {
            if (i <= coin || optimal[i - coin] == -1) continue;//when equal, must be 1.

            if (optimal[i] == -1 || optimal[i - coin] + 1 < optimal[i])
                optimal[i] = optimal[i - coin] + 1;
        }

    }
    int ret = optimal[amount];
    delete[] optimal;
    return ret;
}

2.2 递归加缓存,就是自顶向下。f(n)->f(n-1)... 第10个,台阶,下降到第9,第8.......

class Solution:
    def minCoinsRequired(self, denominations, amount):
        @cache
        def dp(amount):
            if amount == 0:
                return 0
            ans = math.inf
            for i in denominations:
                if i <= amount:
                    x = dp(amount - i)
                    if x != math.inf:
                        ans = min(ans, x + 1)
            return ans
        #sys.setrecursionlimit(500000)
        ans = dp(amount)
        return ans if ans != math.inf else -1

如果我们从开始考虑问题,

即从0出发,可以到达 (0+2,0+3,0+5);我们发现这就是分别到达 2,3,5的最小路径。 再从这3个生成的节点出发:

  • 从 2出发(2+2,2+3,2+5); 我们发现这就是到达比如4,7的最小路径。5也可以到达,却是第二次到的。说明有两种方式到达5.
  • 从 3出发;
  • 从5出发;

走完第一轮递推。

依次递推,首次出现的就是最小路径了。DFS

 

3.1 c++ 示例

3.1.1 cache[i] 表示时,缺乏 i 无解时的状态。验证《算法之禅》上代码有问题:

有两个问题:1,当找到出口n==coin是,直接返回了1。没有把cache[n]=1 缓存起来。

2,cache[i] 没有缓存无解的状态。

查看代码
#include <iostream>  
#include <set>
#include <vector>
#include <map>

using namespace std;
/*
cache[i]:
    -1: uninit OR no change; 
    LACK OR THIS STATUS: 0: no change; 
    >0 has cash

solve() return: 
    -1: no cash; 
    >0 has cash; 
    ==0? NP
*/

#define TEST

#ifdef TEST
set<int> hasSet;
map<int, int> amountCount;
#endif

int solve(vector<int>& denominations, int amount, vector<int>& cache) {

#ifdef TEST
    int opt = -1;
#endif
    if (amount < 0) return -1;

    if (cache[amount] != -1) return cache[amount];

    cout << " amout:" << amount << "\t";
#ifdef TEST
    auto it = hasSet.find(amount);

    if (it != hasSet.end()) {
        cout << "XXXXXXXXXXXXXX " << amount << " have been computed no combination." << endl;
    }
    amountCount[amount]++;
#endif

    for (auto i : denominations) {
        if (i == 0) continue;

        if (i == amount) return 1;

        auto count = solve(denominations, amount - i, cache);
        if (count == -1) continue;

        //if not init or have less value
        if (opt == -1 || count + 1 < opt)
            opt = count + 1;
    }

    cache[amount] = opt;// opt -1 may be no optimal or un-init.

    cout << "\n === final result: cache[" << amount << "]= " << cache[amount] << "\n";

#ifdef TEST
    hasSet.insert(amount);
#endif

    return opt;
}

int solve(vector<int>& denominations, int amount) {

    vector<int> cache(amount + 1, -1);
    //cache record amount, which starts from 1, so amount 60+1 number.


#ifdef TEST
    for (auto i = 0; i <= amount; i++) {
        amountCount[i] = 0;
    }
#endif

    auto ans = solve(denominations, amount, cache);

#ifdef TEST
    for (auto i = 0; i <= amount; i++) {
        if (amountCount[i] > 1)
            cout << i << " been computed multi times!!!!!!" << endl;
        ;
    }
#endif

    cache.clear();
    return ans;

}


int main(int argc, char** argv) {
#if 0
    vector<int> a = { 1, 5, 10, 25 };
    //{ 66, 51, 58, 43, 62, 38, 48, 44, 60, 64 };
    auto count = solve(a, 60);
    cout << "anser:" << count << endl;


    vector<int> a = { 22 };
    auto count = solve(a, 245);
    cout << "anser:" << count << endl;


#endif

    vector<int> a = { 7,62 };
    auto count = solve(a, 499);
    cout << "anser:" << count << endl;
    return 0;
}
//
//

3.3.2 正确的姿势:

查看代码
#include <iostream>  
#include <set>
#include <vector>
#include <map>

using namespace std;

/*
cache[i]: 
    -1:init; 
    0: no cash; 
    >0: has cash

solve() return: 
    -1: no cash;   
    >0 has cash; 
    ==0: invalid
*/

#define TEST

#ifdef TEST
set<int> hasSet;
map<int,int> amountCount;
#endif

int solve(vector<int>& denominations, int amount, vector<int>& cache) {  

#ifdef TEST
    if (amount >= 0 && cache[amount] == 0) {
        cout <<"\n%%%%%%% "<< amount << "  no need to compute." << endl;
    }
#endif
    //amount小于0,无意义; cache[amount]等于0,说明对于amount,无法兑换,无解。
    if (amount <= 0 || cache[amount] == 0) return -1;

    // cache[amount]大于0,可以直接从cache中取
    if (cache[amount] > 0) return cache[amount];

    //这里才开始真正计算
    cout << " amout:" << amount << "\t";
#ifdef TEST
    auto it = hasSet.find(amount);

    if (it != hasSet.end()) {
        cout << "XXXXXXXXXXXXXX " << amount << " have been computed no combination." << endl;
    }
    amountCount[amount]++;
#endif

    for (auto i : denominations) {
        if (i == 0) continue;

        //if (i>amount) continue; //第35行有了防御,这句就不用了

        //这里是递归的出口,只有等于有的硬币时,才找到。
        if (i == amount) {
            cout << "\n\t\t***   update: cache[" << amount << "]= " << 1 ;
            cache[i] = 1;
            return 1;
        }

        auto count = solve(denominations, amount - i, cache);

        //-1是没找到
        if (count == -1) continue;

        //找到时,如果cache[amount]是初始状态,要被赋值count + 1;或者 count + 1比缓存里面的要小,也要做。
        if (cache[amount] == -1 || count + 1 < cache[amount])
            cache[amount] = count + 1;
    }

    //在上面所有硬币都被轮询,都没找到。那就是真没有了。无法兑换的状态为0.
    if (cache[amount] == -1) { 
        cache[amount] = 0; 
    }
    cout << "\n === final result: cache[" << amount << "]= " << cache[amount] << "\n";

#ifdef TEST
    hasSet.insert(amount);
#endif

    //返回值只有-1没找到;或者>0表示找到了
    return cache[amount] == 0 ? -1 : cache[amount];

}

int solve(vector<int>& denominations, int amount) {

    static vector<int> cache(amount + 1, -1);
    //cache record amount from 1, so need 60+1 number.
 //   cache[0] no meaning.

#ifdef TEST
    for (auto i = 0; i <= amount;i++) {
        amountCount[i]=0;
    }
#endif

    auto ans = solve(denominations, amount, cache);

#ifdef TEST
    for (auto i = 0; i <= amount; i++) {
        if (amountCount[i] > 1)
            cout << i << " been computed multi times!!!!!!" << endl;
        ;
    }
#endif

    cache.clear();
    return ans;

}


int main(int argc, char** argv) {
#if 0
    vector<int> a = { 1, 5, 10, 25 };
    //{ 66, 51, 58, 43, 62, 38, 48, 44, 60, 64 };
    auto count = solve(a, 60);
    cout << "anser:" << count << endl; //3


    vector<int> a = { 22 };
    auto count = solve(a, 245);
    cout << "anser:" << count << endl; //-1


#endif

    vector<int> a = { 7,62 };
    auto count = solve(a, 499);
    cout << "anser:" << count << endl;//应该是32
    return 0;
}
//
//

3.3.3 进一步的防御性编程,替换递归的出口

查看代码
 #include <iostream>  
#include <set>
#include <vector>
#include <map>

using namespace std;

/*
cache[i]: 
    -1:init; 
    0: no cash; 
    >0: has cash

solve() return: 
    -1: no cash;   
    >0 has cash; 
    ==0: invalid
*/

#define TEST

#ifdef TEST
set<int> hasSet;
map<int,int> amountCount;
#endif

#if 1//相等也进入递归

int solve(vector<int>& denominations, int amount, vector<int>& cache) {

#ifdef TEST
    if (amount >= 0 && cache[amount] == 0) {
        cout << "\n%%%%%%% " << amount << "  no need to compute." << endl;
    }
#endif
    //amount小于0,无意义; cache[amount]等于0,说明对于amount,无法兑换,无解。
    if (amount < 0 || cache[amount] == 0) return -1;

    //防御编程, 替换递归出口到这里
    if (amount == 0) return 0;

    // cache[amount]大于0,可以直接从cache中取
    if (cache[amount] > 0) return cache[amount];

    //这里才开始真正计算
    cout << " amout:" << amount << "\t";
#ifdef TEST
    auto it = hasSet.find(amount);

    if (it != hasSet.end()) {
        cout << "XXXXXXXXXXXXXX " << amount << " have been computed no combination." << endl;
    }
    amountCount[amount]++;
#endif

    for (auto i : denominations) {
        if (i == 0) continue;

        //if (i>amount) continue; //第35行有了防御,这句就不用了

        //关闭这里出口://这里是递归的出口,只有等于有的硬币时,才找到。
        //if (i == amount) {
        //    cout << "\n\t\t***   update: cache[" << amount << "]= " << 1;
        //    cache[i] = 1;
        //    return 1;
        //}

        auto count = solve(denominations, amount - i, cache);

        //-1是没找到
        if (count == -1) continue;

        //找到时,如果cache[amount]是初始状态,要被赋值count + 1;或者 count + 1比缓存里面的要小,也要做。
        if (cache[amount] == -1 || count + 1 < cache[amount])
            cache[amount] = count + 1;
    }

    //在上面所有硬币都被轮询,都没找到。那就是真没有了。无法兑换的状态为0.
    if (cache[amount] == -1) {
        cache[amount] = 0;
    }
    cout << "\n === final result: cache[" << amount << "]= " << cache[amount] << "\n";

#ifdef TEST
    hasSet.insert(amount);
#endif

    //返回值只有-1没找到;或者>0表示找到了
    return cache[amount] == 0 ? -1 : cache[amount];

}

#else

int solve(vector<int>& denominations, int amount, vector<int>& cache) {

#ifdef TEST
    if (amount >= 0 && cache[amount] == 0) {
        cout << "\n%%%%%%% " << amount << "  no need to compute." << endl;
    }
#endif
    //amount小于0,无意义; cache[amount]等于0,说明对于amount,无法兑换,无解。
    if (amount <= 0 || cache[amount] == 0) return -1;

    // cache[amount]大于0,可以直接从cache中取
    if (cache[amount] > 0) return cache[amount];

    //这里才开始真正计算
    cout << " amout:" << amount << "\t";
#ifdef TEST
    auto it = hasSet.find(amount);

    if (it != hasSet.end()) {
        cout << "XXXXXXXXXXXXXX " << amount << " have been computed no combination." << endl;
    }
    amountCount[amount]++;
#endif

    for (auto i : denominations) {
        if (i == 0) continue;

        //if (i>amount) continue; //第35行有了防御,这句就不用了

        //这里是递归的出口,只有等于有的硬币时,才找到。
        if (i == amount) {
            cout << "\n\t\t***   update: cache[" << amount << "]= " << 1;
            cache[i] = 1;
            return 1;
        }

        auto count = solve(denominations, amount - i, cache);

        //-1是没找到
        if (count == -1) continue;

        //找到时,如果cache[amount]是初始状态,要被赋值count + 1;或者 count + 1比缓存里面的要小,也要做。
        if (cache[amount] == -1 || count + 1 < cache[amount])
            cache[amount] = count + 1;
    }

    //在上面所有硬币都被轮询,都没找到。那就是真没有了。无法兑换的状态为0.
    if (cache[amount] == -1) {
        cache[amount] = 0;
    }
    cout << "\n === final result: cache[" << amount << "]= " << cache[amount] << "\n";

#ifdef TEST
    hasSet.insert(amount);
#endif

    //返回值只有-1没找到;或者>0表示找到了
    return cache[amount] == 0 ? -1 : cache[amount];

}
#endif

int solve(vector<int>& denominations, int amount) {

    static vector<int> cache(amount + 1, -1);
    //cache record amount from 1, so need 60+1 number.
 //   cache[0] no meaning.

#ifdef TEST
    for (auto i = 0; i <= amount;i++) {
        amountCount[i]=0;
    }
#endif

    auto ans = solve(denominations, amount, cache);

#ifdef TEST
    for (auto i = 0; i <= amount; i++) {
        if (amountCount[i] > 1)
            cout << i << " been computed multi times!!!!!!" << endl;
        ;
    }
#endif

    cache.clear();
    return ans;

}


int main(int argc, char** argv) {
#if 0
    vector<int> a = { 1, 5, 10, 25 };
    //{ 66, 51, 58, 43, 62, 38, 48, 44, 60, 64 };
    auto count = solve(a, 60);
    cout << "anser:" << count << endl; //3


    vector<int> a = { 22 };
    auto count = solve(a, 245);
    cout << "anser:" << count << endl; //-1


#endif

    vector<int> a = { 7,62 };
    auto count = solve(a, 499);
    cout << "anser:" << count << endl;//应该是32
    return 0;
}
//
//

 

最终总结:

看兑换钱的递归代码,其实和上面的递推代码实现起来是一样的。比如算 f(5) ,面值是【1,2】,递归首先会走 f(4),f(3),f(2), f(1); f(1)=1后依次回归出f(2),f(3),f(4)。 即先算出了兑换值是 1的最优解。3个面值都先试过,是列优先的。

 

右面:兑换值

下面:纸币面值

1元 2 3 4 5
面值1元 1 2 3 4 5
面值1元,面值2元 1 1 2    

面值1元,面值2元,

面值3元

1 1 1    

递推与递归都是一样的。

这里用了一维的缓存。是个全包背包问题。如果是01背包,一维是不能从前往后遍历的。

 


3, 组合出给定的整数值。排列不一样也算。

Teaching Kids Programming – Combination Sum Up to Target (Unique Numbers) by Dynamic Programming Algorithms | Algorithms, Blockchain and Cloud (helloacm.com)

 an array of distinct integers nums and a target integer target, return the number of possible combinations that add up to target. The answer is guaranteed to fit in a 32-bit integer.

转成爬楼梯问题就是:一次可以爬1,2,3级台阶。爬到4层,有多少种。

Example 1:
Input: nums = [1,2,3], target = 4
Output: 7
Explanation:
The possible combination ways are:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
Note that different sequences are counted as different combinations.

Example 2:
Input: nums = [9], target = 3
Output: 0

Constraints:
1 <= nums.length <= 200
1 <= nums[i] <= 1000
All the elements of nums are unique.
1 <= target <= 1000

Follow up: What if negative numbers are allowed in the given array? How does it change the problem? What limitation we need to add to the question to allow negative numbers?

3.1, 暴力回溯法:

class Solution:
    def combinationSumToTarget(self, nums: List[int], target: int) -> int:
        ans = 0
        
        def pick(left, T):
            nonlocal ans
            if T == 0:
                ans += 1
                return
            for i in range(len(nums)):
                if T >= nums[i]:
                    pick(i, T - nums[i])
                
        pick(0, target)
        return ans

3.2 动归自底向上

与爬楼梯问题一样,从0开始往出爬,一共多少种。

如果f(t)表示到达t有多少种。那么它就是 f(t)=∑ f(t-i) , 其中i就是所有项。

class Solution:
    def combinationSumToTarget(self, nums: List[int], target: int) -> int:
        n = len(nums)
        dp = [0] * (target + 1)
        dp[0] = 1
        for i in range(1, target + 1):
            for j in nums:
                if i >= j:
                    dp[i] += dp[i - j]
        return dp[-1]

3.3 top-down 递归

class Solution:
    def combinationSumToTarget(self, nums: List[int], target: int) -> int:
        @cache
        def f(n):
            if n == 0:
                return 1
            if n < 0:
                return 0
            ans = 0
            for i in nums:
                ans += f(n - i)
            return ans
        return f(target)