递归解决换零钱问题--问题分析

如下是咱们的问题:数组

把100元用50元,20元,10元,5元,1元去换,总共有几种换法?spa

形式化(formalize)

 

为了简化叙述,把问题记为“coc(100, [50, 20, 10, 5, 1])”。.net

coc: case of change或者count of change(change在英文中有零钱的意思)code

为使问题更具备通常性,参数化具体值,获得:orm

caseOfChange(amount, cashList)blog

其中amount为金额,cashList为零钱列表。递归

递归分析

前面说到递归分两种情形,一是基本情形(base case),二是递归情形(recursive case)图片

基本情形(base case)

显然,问题难在零钱种类太多,若是只有一种零钱,问题就简单了:get

好比coc(100, [50])=1,即有一种换法,也就是换成2张50的。it

又如coc(50, [20])=0,即无法换,2张20不够,3张又太多。

显然,结果不是0就是1,就看可否整除。根据以上分析,得出:

caseOfChange(amount, cashList) { 
	// base case
    if (cashList.containsOnlyOneCash()) { // 只有一种零钱时 
        if (amount.canBeChangedWith(cashList.getTheOnlyOneCash())) { // 能换时,即能整除 
            return 1;
        } else { 
            return 0; 
        } 
    } else {
    	// recursive case, TODO
    }
}

 

注:暂时请只专一于描述咱们的问题,如今还不急着考虑具体的实现。

递归情形(recursive case)

由于递归的模式并非那么明显,咱们仍是先回到传统的思路上考虑。

拆分问题

问题难在零钱种类太多,咱们首先考虑可否拆解它。

把大问题分红几个小问题,或者说把一个复杂问题分红几个简单问题,这是咱们经常使用的伎俩。

那么怎么分解呢?咱们常常能想到的就是一分为二。

若是咱们能找到一种方法,比方说,能把5种零钱拆成“1+4”,那么“1”能够在base case中解决了,更进一步,4又可拆成1+3,循环往复,问题就可解决了。

具体事例

为方便思考,咱们仍是用更具象的例子来分析。好比,怎么把coc(100, [50, 20, 10, 5, 1])中的50拆分出来?

在纸上写下一些具体的换法,经观察能够有两种换法:

image

一种如红色部分,分红“用了50的”和“没用50的”。

一种如蓝色部分,分红“彻底只用50的”和“不彻底用50的”(这等于说仍是没把50拆分出来)。

因此,红色那种更好一点,它的另一部分已经完全与50无关了。

不过,与咱们想像的把5拆成1+4不一样,这里是把5拆成5+4。

它意味着把[50, 20, 10, 5, 1]分红了[50, 20, 10, 5, 1]和[20, 10, 5, 1]。由于前者除了50以外依然还用了其它。

考虑一下集合中的”并“操做。[50, 20, 10, 5, 1] U [20, 10, 5, 1] = [50, 20, 10, 5, 1],对吧,但愿你还能记得。因此反之你确实能够把它视做某种分解。

如今能够把结果写成两部分之和,形式化的表达以下:

coc(100, [50, 20, 10, 5, 1])=cocWith50(100, [50, 20, 10, 5, 1]) + cocWithout50(100, [50, 20, 10, 5, 1])

其中:cocWith50表示“用了50的”,cocWithout50则为“没用50的”。

找到一个递归!

显然,如今已经不是递归了,coc已经变成了cocWith50和cocWithout50,星星已经不是那个星星了。

不过你要是再仔细看看上面图中红色部分的下半身:

image

也便是前面等式中右边第二部分的cocWithout50(100, [50, 20, 10, 5, 1])),它不就是coc(100, [20, 10, 5, 1])吗?

观察cocWithout50(100, [50, 20, 10, 5, 1]),既然方法声称不使用50,参数[50, 20, 10, 5, 1]里还带了50不就没用了吗?

因此cocWithout50(100, [50, 20, 10, 5, 1])=cocWithout50(100, [20, 10, 5, 1]),

而后,既然参数里已经不带50,方法不就不必强调without50了吗?

因此cocWithout50(100, [20, 10, 5, 1])=coc(100, [20, 10, 5, 1])!

至此能够获得如下等式:

coc(100, [50, 20, 10, 5, 1])=cocWith50(100, [50, 20, 10, 5, 1]) + coc(100, [20, 10, 5, 1])

子问题中的一个已经向着原问题递归了,并且第二个参数cashList在减少,这意味着递归有收敛的可能!

临门一脚

如今激动地畅想一下,若是咱们能把等式右边第一部分的cocWith50也能向coc靠拢,那不就大功告成了吗?

怎么往coc这条大腿上靠呢?咱们不妨从结果出发,所谓向它靠拢,也便是要寻求一种等价关系,使得:

cocWith50(100, [50, 20, 10, 5, 1])=coc(?, [?..?])

观察一下,等式左边有三个元素:

1. cocWith50,

2. 100,

3. [50, 20, 10, 5, 1]

从结果出发,cocWith50要变成coc,这个是咱们的目标。

但这样就会丢掉一个特性,那就是“with50”(它强调了每种换法必定要用到50),coc方法显然是没这种强调的;

因此其它两个元素的变化必须体现这个特性或者说至少作出某种呼应。

那么,咱们还有两个元素能够变。并且从要让递归收敛的角度考虑,它们应该是一种递减的变化。

如今看第二个元素,100能怎么变?或者说100能变吗?毕竟你是要拿100去换呀!不太明朗,咱们先看看第三个。

第三个元素,cashList,能怎么变呢?既然要递减,咱们无非但愿能去掉list中的一些,让咱们看看:

1. cocWith50点名了要50,不能少,

2. 但也没说其它的如20,10,5,1之类的就不要呀,因此也不能少!(从图中也能直观看出,这些零钱均可能会用到)

另外,也不清楚如何去呼应“with50”这一丢失的特性。看来咱们只好把目光再次投向第二个元素“100”了:

毕竟,你能依靠的也就这哥俩了,100会是咱们的救命稻草吗?

让咱们大胆猜想一下,让100变成“100-50”,也便是认定:

cocWith50(100, [50, 20, 10, 5, 1])=coc(100-50, [50, 20, 10, 5, 1]) =coc(50, [50, 20, 10, 5, 1])

注:最后的50是由100-50获得的,刚好也是50,但与红色标出的50是不一样的。

咱们固然不是胡乱猜想:

理由之一:cocWith50变到coc,去掉了“with50”,咱们也在100里减去50,正好呼应了这一变化。

理由之二:这使得参数在减少,符合递归要求参数递减的要求。

固然,咱们想要的实际上是cashList的递减,amount的减少不能让咱们递归到base case上。

但从另外一角度看,若是amount能不断减少,将致使cashList中的一些零钱比amount还大,这将间接致使cashList的递减,也即递归将会收敛。

好比100递减到了20,那么coc(20, [50, 20, 10, 5, 1])=coc(20, [20, 10, 5, 1]),由于50比20还大,因此去掉也没影响了。

天然,咱们还须要寻求更加明显的理由来支持咱们的猜想。紧扣“with50”这一特性,从图片观察:

image

显然,既然每种不一样换法都带了50,那么去掉它以后,不一样的换法彼此间仍是保持不一样。因此:

image

那么,100-50的意义在哪,如今就很明确了,由于每种换法都带了50,因此减去它不影响等价性。因此:

cocWith50(100, [50, 20, 10, 5, 1])=coc(100-50, [50, 20, 10, 5, 1])=coc(50, [50, 20, 10, 5, 1]) ,猜想被证明!

因为100-50刚好为50,两个50容易误解,咱们举另一个例子,好比把50用[20, 10, 5, 1]去换,则以下图所示:

综合之有:

coc(100, [50, 20, 10, 5, 1])=coc(100-50, [50, 20, 10, 5, 1]) + coc(100, [20, 10, 5, 1])

递归模式已经被概括出来。参数化,抽象化后:

coc(A, [C1, C2…Cn])=coc(A-C1, [C1, C2…Cn]) + coc(A, [C2, C3…Cn]);

即:

caseOfChange(amount, cashList) { 
	// base case
    if (cashList.containsOnlyOneCash()) { // 只有一种零钱时 
        if (amount.canBeChangedWith(cashList.getTheOnlyOneCash())) { // 能换时,即能整除 
            return 1;
        } else { 
            return 0; 
        } 
    } else {
    	// recursive case
    	return caseOfChange(amount.subtractBy(cashList.getFirstCash()), cashList)
        	+ caseOfChange(amount, cashList.newCashListWithoutFirstCash());
    }
}

 

其中:

  1. amount.subtractBy(cashList.getFirstCash()),正如方法名字所暗示那样,表示“金额减去第一个零钱”;

  2. cashList.newCashListWithoutFirstCash()表示“去掉第一个零钱后的新零钱列表”。

至此,主体的结构已经基本OK了,不过,还遗留一个问题,递归情形中的第一部分还没法收敛,还须要增长一些额外判断,即当金额比零钱列表中的第一个还小时,就减少零钱列表的元素,这样就能向着base case归约了。

或者换种思路,让金额一直递减,直到它为0甚至为负时咱们再做出决定。咱们来看看这意味着什么。

手动演算

根据递归模式,咱们也能够手动地演算一下。以简单的“把10元换成5元与1元”为例,有coc(10, [5, 1])=3。

具体为5+5,5+1+1+1+1+1,1+1+1+1+1+1+1+1+1+1三种。

公式推演以下:

coc(10, [5, 1])

=coc(10-5, [5, 1]) + coc(10, [1])

=coc(5, [5, 1]) + 1

=coc(5-5, [5, 1]) + coc(5, [1]) + 1

=coc(0, [5, 1]) + 1 + 1

那么如今问题来了,coc(0, [5, 1])=?

显然,根据最终结果为3,因此coc(0, [5, 1])=1,也即用0元去换,有一种换法。

这可能会让人以为有些很差理解,现实中没人去拿0元去换。

若是观察上述的推算过程,还出现了coc(5, [5, 1]),即5元又拿5元与1元去换,显然,现实中也不会发生这样的换法。

其实前面也出现了coc(50, [50, 20, 10, 5, 1],50元又用了50元去换。

但咱们知道,之因此出现这种状况,是等价替换形成的,coc(5, [5, 1])是通过一次约减后的中间结果,而coc(0, [5, 1])则是通过两次约减后得来的,它实际就是原来的“5+5”这种换法。

金额为0的状况

能够用另外一种角度来看待这个问题,以coc(10, [5, 1])=3为例,这里实质是什么呢?

10=2*5+0*1,至关于5+5,也即10元能换成2张5元加0张1元,只不过咱们一般省略后者。

10=1*5+5*1,至关于5+1+1+1+1+1,也即10元能换成1张5元加5张1元。

10=0*5+10*1,至关于1+1+1+1+1+1+1+1+1+1,也即10元能换成0张5元加10张1元,只不过咱们一般省略前者。

因此=3实质就是有三种系数的组合,(2, 0),(1, 5),(0, 10)。

咱们不考虑系数为负的状况,不然就有无数种可能了。如10=3*5+(-5)*1=4*5+(-10)*1=…

那么0=0*5+0*1,至关于有一种且惟一一种系数组合(0, 0),也即0元能换成0张5元加0张1元。

不考虑系数为负的状况,那么就不可能有其它组合了,由于两个零钱都是正数。

实际上,coc(0, [50, 20, 10, 5, 1])也是1种。

系数组合为(0, 0, 0, 0, 0)。无论有多少零钱种类,只要用0元去换,都有一种且惟一一种换法。

显然,只要能整除,反复约减之下就能获得0.

同理:coc(5, [5, 1])有两种系数组合(1, 0), (0, 5)。

金额为负数的状况

反之,若是不能整除,反复约减之下就会获得负数,如coc(50, [20, …]),那么,三次约减以后就会出现形如“coc(-10, [20, …]”,那么,这种状况就认为有0种换法,即不可换。

最终结果

综上,增长两种base case的判断,程序就能收敛了,最终形式化的结果以下:

caseOfChange(amount, cashList) { 
	// base case
	if (amount.isNegative()) { // 负数 
	    return 0; 
	} 
	if (amount.isZero()) { // 0元 
	    return 1; 
	}
	if (cashList.containsOnlyOneCash()) { // 只有一种零钱时 
        if (amount.canBeChangedWith(cashList.getTheOnlyOneCash())) { // 能换时,即能整除 
            return 1;
        } else { 
            return 0; 
        } 
    }
	
	// recursive case
	return caseOfChange(amount.subtractBy(cashList.getFirstCash()), cashList)
    	+ caseOfChange(amount, cashList.newCashListWithoutFirstCash());
}

 

因为篇幅太长,将在后面的篇章中给出具体的程序实现并作一些回顾与总结。

下一篇见递归解决换零钱问题--代码实现

相关文章
相关标签/搜索