樹形遞歸

Tree Recursion

遞歸:函數體中直接或間接調用函數自身,則稱其是遞歸的。html

樹形遞歸:函數體中調用函數自身屢次git

示例1

斐波那契數列github

def fib(n):
    """
    效率還比較糟糕,如計算fib(35)
    0 1 1 2 3 5 8 13 21
    >>> fib(0)
    0
    >>> fib(1)
    1
    >>> fib(8)
    21
    """
    if n == 0 or n == 1:
        return n
    else:
        return fib(n-1) + fib(n-2)

示例2

計算一個正整數n在上限爲n時的最大分割可能數express

以下圖表示計算組成6的全部正整數組合,且全部組合中的數要小於4數組

下圖調用將返回9函數

 

思路

遞歸的思想就是縮減問題規模,直至到一個不可再拆分的基底狀況。不一樣於斐波那契數列,遞歸中須要控制的變量只有表示是第幾個斐波那契數的n,所以只須要將n的基底狀況0和1的返回值肯定,以後就是遞歸調用了。spa

在當前問題中,有兩個變量,一個是待拆分數n,另外一個是組合可能最大數m,所以在考慮如何寫遞歸函時,應當將兩個變量的可能交叉組合,考慮全部的基底狀況。code

這時繪製圖表能夠幫助咱們整理本身的思路,不至於混淆或遺漏。htm

 

設若n小於0,則不符合函數要求,返回0,即沒有可能;blog

設若n爲0,則無論m等於多少,都有一種可能,即本身自己什麼都不加;

設若m等於0,則只要n不爲0,都返回0,即沒有可能要求的組合。

以上都是最基礎的狀況,剩下的一個就是要着重考慮的了,也就是要進行遞歸調用的狀況。

 假設調用count_partitions(6, 4),削減問題規模的話,能夠知道它的值就是count_partitions(6, 3)加上一端固定爲4時的全部可能。

這時,問題就變爲如何用程序語言表述後者。若是隻看向另外一端,在當前例子中,就是可能組合的數不大於4時的2的全部組合,也就是count_partitions(6-4, 4)。

這樣最後一種狀況也就表示出來了。圖示以下:

代碼

def count_partitions(n, m):
    """
    the number of partitions of a positive integer n, using parts up to size m, is the
    number of ways in which n can be expressed as the sum of positive integer parts up
    to m in an increasing order
    對n而言上限爲m-1的可拆分數 + 對n而言一側爲m的可拆分數(即對n-m而言上限爲m的可拆分數)
    >>> count_partitions(6, 4)
    9
    >>> count_partitions(5, 3)
    5
    """
    if n == 0:
        return 1
    elif n < 0:
        return 0
    elif m == 0:
        return 0
    else:
        with_m = count_partitions(n-m, m)
        without_m = count_partitions(n, m-1)
        return with_m + without_m

示例3

在一個width * height的柵格長方形中,有一隻毛蟲蜷縮在左下角,另有一出口在右上角。若是它只能向上或向右移動,那麼要達到出口,總共有多少種可能?

思路

在這個問題中,到達出口的可能數不禁毛蟲決定,而是由長方形的寬和高決定。也就是此次的遞歸中要控制的兩個變量,一個是height,另外一個是width。

仍是從最基礎的狀況開始思考:

設若寬和高都是1,那可能就只有一種,也就是待着不懂;

設若寬是1,那麼高不管是多少,也都只有一種可能,就是沿着惟一的一條路向上走,別無他法;

設若高是1,那麼寬不管是多少,也都只有一種可能,就是沿着惟一的一條路向右走,逃出生天;

有圖示以下

接下來考慮剩下的最後一種狀況:寬和高都大於1時,毛蟲能夠怎麼走?

那咱們就將毛蟲放在一個6 * 6(width * height)的長方形中去考慮。

已知毛蟲只能向右走或向上走,不可反方向行動,那毛蟲第一步開始就只有兩種可能,每一種可能均可以依此爲基礎向下延申出其它可能。那麼全部可能數就可表述爲第一步向上走後全部可能數之和加上第一步向右走後的全部可能數之和。代入到當前例子中,毛蟲第一步向右走後,就可視做其此時處在一個5 * 6的長方形中;毛蟲第一步向上走後,就可視做其此時處在一個6 * 5的長方形中。以下圖:

這就是分割當前問題的方法了,用程序也能夠很容易地表示了。有圖以下:

代碼

def count_paths(width, height):
    """
    In a rectangle grid of certain width and height, a caterpillar who can only
    move right or up is on the bottom left grid. In order to reach the exit at right
    top grid, How many different paths can the caterpillar take? 
    >>> count_paths(1,1)
    1
    >>> count_paths(1,9)
    1
    >>> count_paths(10,1)
    1
    >>> count_paths(3,3)
    6
    """
    if width == 1 and height == 1:
        return 1
    elif width == 1 and height > 1:
        return 1
    elif width > 1 and height == 1:
        return 1
    else:
        return count_paths(width-1, height) + count_paths(width, height-1)

爲了更清晰表達思路,也可簡化以下:

def count_paths_simpler(width, height):
    """
    >>> count_paths(1,1)
    1
    >>> count_paths(1,9)
    1
    >>> count_paths(10,1)
    1
    >>> count_paths(3,3)
    6
    """
    if width == 1 and height == 1:
        return 1
    else:
        caterprie_goes_up = count_paths(width-1, height)
        caterprie_goes_right = count_paths(width, height-1)
        return caterprie_goes_right + caterprie_goes_up

 

 示例3

簡化的揹包問題

有一揹包,和若干物品,各有其重,物品要放入揹包中,且揹包重物品的數量不可超過揹包的最大承重。

另外物品也各有其價值,但此次爲簡化問題,暫不考慮各自價值,只求共有多少種裝包方法。

思路

該問題還是一個計算可能組合數的問題。物品可表示爲一個元素爲元組的列表,形如items = [(worth, weight),...]。

仍從最特殊的基本狀況開始思考。由於基本情形中,再也不有相似1 * 1長方形的基本單位,因而就從有無開始考慮,再也不以1爲基底。

設若沒有物品,那無論揹包能不能承重,都只有一種可能,就是什麼都不放;

設若揹包承重爲0,物品也有的話,也就是有東西要放,揹包卻不能放重量大於0的物品,此時有幾種可能呢?

這時,假設物品的重量能夠爲0,那麼可能性就有不少,也就是取決於物品數量。這還能夠算做是基底狀況嗎?

所謂的基底狀況,必須足夠簡單,不會有不肯定的可能。

換個思路,若是揹包重量不以等於0爲起點,而是以小於0爲起點,那麼不管有無物品,都只有0種可能了,由於就算什麼也不放,也超過了最大承重0。

此時,將大於等於0歸於一種狀況。設若沒有物品的話,那就只有一種狀況,就是什麼也不放。有圖示以下:

又留下了須要遞歸調用的最後一種狀況。

再次只考慮最簡單的問題。目光轉向第一個物品,對其的處理只有兩種可能,要麼放入揹包,要麼不放入揹包,其它可能組合也都以此爲基礎延申。

設若放入揹包,那此時揹包的最大承重就要減去物品的重量,剩下的問題就在當前重量下如何放入其它物品;

設若不放入揹包,那揹包的最大承重不變,剩下的問題就是在原最大承重下如何放入其它物品。

這也就是咱們要進行遞歸調用的地方。有圖以下:

  

代碼

def knapsack_count(weight, items):
    """
    :param weight: maximum weight of the knapsack
    :param items: [(worth, weight), (worth, weight)]
    :return: how many ways we can fill the knapsack without going over the weight limit
    >>> knapsack_count(-1, [(10, 2)])
    0
    >>> knapsack_count(3, [])
    1
    >>> knapsack_count(10, [(1, 4), (2, 5)])
    4
    """
    if weight < 0:
        return 0
    if len(items) == 0:
        return 1
    with_first_item = knapsack_count(weight - items[0][1], items[1:])
    without_first_item = knapsack_count(weight, items[1:])
    return with_first_item + without_first_item

 

示例4

人民幣面額有100元、50元、20元、10元、5元、1元,共計6種。問任意特定數額共有多少種組合方式?

思路

該問題由SICP一書中樹形遞歸一節的問題轉換而來。原題是以美國的half-dollars, quarters, dimes, nickels, and pennies(50,25,10,5,1)爲基礎,

本土化後大概就變成了這個樣子。但究其本質,思路無二。

該問題中有兩個變量,一個是錢幣總額,另外一個是零錢種類(雖然100元對我而言不算零錢)。還從基底狀況開始考慮。

假設總額爲0,那無論有多少種零錢,都只有一種組合方式,就是什麼都不放;

假設總額小於0,則都沒有任何一種組合方式能夠知足條件;

假設種類爲0並且總額不等於0,那麼也是沒有任何一種組合方式能夠達到要求。

可繪製圖表以下:

接下來考慮須要用到遞歸的地方。

不妨這樣思考:任何對大於0的數額的組合方式,都至關於不使用第一種零錢的全部組合方式,加上去全部使用第一種零錢的組合方式。

代碼

def count_change(amount, kinds_of_coins):
    """
    >>> count_change(10, 2)
    3
    >>> count_change(100, 6)
    344
    """
    if amount == 0:
        return 1
    elif amount < 0 or kinds_of_coins == 0:
        return 0
    else:
        # 全部不使用第一種零錢的組合方式
        return (count_change(amount, kinds_of_coins - 1)
                # 全部使用第一種零錢的組合方式
                + count_change(amount - first_denomination(kinds_of_coins), kinds_of_coins))


# 爲了總額中扣除當前種類中第一種零錢的面額,要返回當前種類中第一種貨幣的面值
def first_denomination(kinds_of_coins):
    if kinds_of_coins == 6:
        return 100
    elif kinds_of_coins == 5:
        return 50
    elif kinds_of_coins == 4:
        return 20
    elif kinds_of_coins == 3:
        return 10
    elif kinds_of_coins == 2:
        return 5
    elif kinds_of_coins == 1:
        return 1

下面也順帶貼一下原書中的racket語言實現方式(即美國錢幣的例子):

(define (cc amount kinds-of-coins)
  (cond [(= amount 0) 1]
        [(or (< amount 0) (= kinds-of-coins 0)) 0]
        [else (+ (cc amount (- kinds-of-coins 1))  (cc (- amount (first-denomination kinds-of-coins)) kinds-of-coins))]))

(define (first-denomination kinds-of-coins)
  (cond ((= kinds-of-coins 1) 1)
        ((= kinds-of-coins 2) 5)
        ((= kinds-of-coins 3) 10)
        ((= kinds-of-coins 4) 25)
        ((= kinds-of-coins 5) 50)))

 

 

參考:

https://sequoia-tree.github.io/#Textbook

http://composingprograms.com/pages/17-recursive-functions.html

http://sarabander.github.io/sicp/html/1_002e2.xhtml#g_t1_002e2_002e1  

相關文章
相關標籤/搜索