遞歸的邏輯(2)——特征方程和遞歸算法


遞歸關系的基本解法

  無論是fabo_2還是fabo_3,在計算時都需要遵守遞歸表達式,即求f(n)的值時必須先求得n之前的所有序列數。這就讓我們有了一個設想,能否將斐波那契數列的遞歸表達轉換成普通的函數映射?這樣就可以在常數時間內求得f(n)。

特征方程和特征根

  首先要明確的是,沒有一個通用的方法能夠解所有的遞歸關系,但是一些解法對於某些規則的遞歸相當有效,其中的特征方程法就可以用來求得斐波那契數列的顯示表達。

  當一個遞歸關系滿足:

  則稱遞歸關系為k階的線性(所有F的次數都是1)常系數(a1,a2,...,ak都是常數)齊次(每一項F次數都相等,沒有常數項)遞歸關系。

  我們的目標是尋找遞歸關系的顯示表達,這就相當於尋找一個能夠表達F(n)函數,令這個函數是:

  根據遞歸表達式:

  這就使問題變成了解方程問題,這個方程稱為遞歸關系的特征方程。方程會產生k個跟x1,x2,…,xk,這些跟稱為特征根或特征解,當特征解互不相同時,則遞歸關系的通解是:

  其中c1,c2,…,ck是常數,只要求得這些常系數就得到了通解的固定形態。

  注:這種方法僅適用於沒有重跟的常系數線性齊次遞歸關系。

斐波那契的顯示表達式

  斐波那契數列正好是常系數線性齊次遞歸關系,因此可以轉換為下面的特征方程:

  接下來是求解特征根:

  由於已經知道在斐波那契數列中x > 0,所以xn-2≠0,結果只能是x2-x-1=0,這就可以計算出兩個特征根:

  兩個特征根互不相同,f(n)的通解滿足:

  由於已知f(0)=f(1)=1,因此可以將通解轉換為方程組:

  終於可以求得斐波那契數列的顯示表達了:

  這是個很神奇的表達,用無理數表示了有理數。這樣斐波那契數列的代碼就更直接了:

 1 import math
 2
 3 # 直接利用顯示公式計算斐波那契數列
 4 def fabo_2(n):
 5     f = (((math.sqrt(5) + 1) / 2) ** (n + 1) - ((math.sqrt(5) - 1) / 2) ** (n + 1)) / math.sqrt(5)
 6     f = round(f, 1)
 7     if f > int(f):
 8         return int(f) + 1
 9     else:
10         return int(f)
11
12 if __name__ == '__main__':
13     for i in range(0, 14):
14         print('f({0}) = {1}'.format(i, fabo_2(i)))

  Python直接使用顯示公式將會得到一個浮點數,因此必須額外加上5~9行的處理。

黃金分割

  現在對斐波那契數列的f(n)求極限:

  這就是著名的黃金分割,一個兔子繁殖的故事最終居然和黃金分割點聯系到一起,是不是有些不可思議?

遞歸算法

  我們已經見識過不少遞歸程序,它提供了一種解決復雜問題的直觀途徑,這一節我們將看到更多關於遞歸的算法。

可疑的遞歸

  在程序設計中遞歸的簡單定義是一個能調用自身的程序,然而程序不能總是調用自身,否則就沒有辦法停止,所以遞歸還必須有一個明確的終止條件;另一個指導原則是——每次遞歸調用的參數值必須更小。

  看看下面的程序是否是合理的遞歸?

 1 # 可疑的遞歸
 2 def suspicious(n, k):
 3     print('\t' * k, 'suspicious({0})'.format(n))
 4     if n == 1:
 5         return 1
 6     elif n & 1 == 1:
 7         return suspicious(n * 3 + 1, k + 1)
 8     else:
 9         return suspicious(n // 2, k + 1)
10
11 if __name__ == '__main__':
12     suspicious(3, 0)

  如果參數n是奇數,suspicious使用3n+1來調用自身;如果n是偶數,使用n/2來調用自身。顯然並不是每次都使用更小的參數調用自身,這不符合遞歸的原則,我們也沒法使用歸納法來證明整個程序是否會終止。

  代碼的運行結果:

  雖然對於suspicious(3)來說,它最后終止了,但我們無法證明它是否會對某個參數有任意深度的嵌套。為了避免不必要的麻煩,還是讓遞歸程序更明確一點——每次遞歸調用的參數值都更小。

動態編程

  斐波那契的遞歸代碼很優美,它能夠讓人以順序的方式思考,然而這段代碼只能作為遞歸程序的演示樣品,並不能應用於實踐。在運行時便會發現,當輸入大於30時速度會明顯變慢,普通家用機甚至無法計算出f(50)。可以通過運行下面的代碼清晰的看到變慢的原因:

1 def fabo_test(n):
2     if n < 2:
3         return 1
4     else:
5         print('\t' * (6 - n), 'f({0})'.format(n))
6         return fabo_test(n - 1) + fabo_test(n - 2)
7 if __name__ == '__main__':
8     fabo_test(6)

  第二次遞歸會忽略上一次所做的所有計算,所以很不給面子的出現了大量重復,這將導致相當大的開銷。這段代碼說明的問題是,寫一個簡單而低效的遞歸方法是相當容易的,我們需要小心這種陷阱。

  知道問題的所在就可以對症下葯,只要把計算過的數據存儲起來就好了,這種方法稱為動態編程,它消除了重復計算,適用於任何遞歸計算,能夠把算法的運行時間從指數級改進到線性級,其代價是我們可以負擔得起緩存造成的空間開銷,是典型的空間換時間。

 1 # 存儲所有計算過的斐波那契數
 2 fabo_list = [1, 1]
 3 # 用遞歸計算斐波那契序列
 4 def fabo_4(n):
 5     if n < len(fabo_list):
 6         return fabo_list[n]
 7     else:
 8         fabo_n = fabo_4(n - 1) + fabo_4(n - 2)
 9         fabo_list.append(fabo_n)
10
11         return fabo_n
12
13 if __name__ == '__main__':
14     for i in range(40, 51):
15         print('f({0}) = {1}'.format(i, fabo_4(i)))

  運行結果:

小偷的背包

  一個小偷撬開了一個保險箱,發現里面有N個大小和價值不同的東西,但自己只有一個容量是M的背包,小偷怎樣選擇才能使偷走的物品總價值最大?

  假設有5個物品A,B,C,D,E,它們的體積分別是3,4,7,8,9,價值分別是4,5,10,11,13,可以用矩形表示體積,將矩形旋轉90°后表示價值:

  下圖展示一個容量為17的背包的4中填充方式,其中有兩種方式的總價都是24:

  背包問題有很多重要的實應用,比如長途運輸時,需要知道卡車裝載物品的最好方式。我們基於這樣的思路去解決背包問題:在取完一個物品后,找到填充背包剩余部分的最佳方法。對於一個容量為M的背包,需要對每一種類型的物品都推測一下,如果把它裝入背包的話總價值是多少,依次遞歸下去就能找到最佳方案。這個方案的原理是,一旦做出了最佳選擇就無需更改,也就是說一旦知道了如何填充較小容量的背包,則無論下一個物品是什么,都無需再次檢驗已經放入背包中的物品(已經放入背包中的物品一定是最佳方案)。

  可以根據這種方案編寫代碼:

1 class Goods:
2     ''' 物品的數據結構  '''
3     def __init__(self, size, value):
4         '''
5         :param size: 物品的體積
6         :param value: 物品的價值
7         '''
8         self.size = size
9         self.value = value
 1 def fill_into_bag(M, goods_list):
 2     '''
 3     填充一個容量是 M 的背包
 4     :param M: 背包的容量
 5     :param goods_list: 物品清單,包括每種物品的體積和價值,物品互不相同
 6     :return: (最大價值,最佳填充方案)
 7     '''
 8     space = 0       # 背包的剩余容量
 9     max = 0         # 背包中物品的最大價值
10     plan = []       # 最佳填充方案
11
12     for goods in goods_list:
13         space = M - goods.size
14         if space >= 0:
15             # 在取完一個物品(goods)后,填充背包剩余部分的最佳方法
16             space_plan = fill_into_bag(space, goods_list)
17             if space_plan[0] + goods.value > max:
18                 max = space_plan[0] + goods.value
19                 plan = [goods] + space_plan[1]
20
21     return max, plan
22
23 def paint(plan):
24     print('最大價值:' + str(plan[0]))
25     print('最佳方案:')
26     for goods in plan[1]:
27         print('\t大小:{0}\t價值:{1}'.format(goods.size, goods.value))
28
29 if __name__ == '__main__':
30     goods_list = [Goods(3, 4), Goods(4, 5), Goods(7, 10), Goods(8, 11), Goods(9, 13)]
31     plan = fill_into_bag(17, goods_list)
32     paint(plan)

  運行結果:

  遺憾的是,fill_into_bag方法同樣只能作為一個簡單的試驗樣品,它犯了和fabo_test同樣的錯誤,第二次遞歸會忽略上一次所做的所有計算,要花指數級的時間才能計算出結果!為了把時間降為線性,需要使用動態編程技術對其進行改進,把計算過的值都緩存起來,由此得到了背包問題的2.0版,當然,我們並不想把這個算法告訴小偷:

 1 # 字典緩存,space:(max,plan)
 2 sd = {}
 3 def fill_into_bag_2(M, goods_list):
 4     '''
 5     填充一個容量是 M 的背包
 6     :param M: 背包的容量
 7     :param goods_list: 物品清單,包括每種物品的體積和價值,物品互不相同
 8     :return: (最大價值,最佳填充方案)
 9     '''
10     space = 0       # 背包的剩余容量
11     max = 0         # 背包中物品的最大價值
12     plan = []       # 最佳填充方案
13
14     if M in sd:
15         return sd[M]
16
17     for goods in goods_list:
18         space = M - goods.size
19         if space >= 0:
20             # 在取完一個物品(goods)后,填充背包剩余部分的最佳方法
21             print(goods.size, space)
22             space_plan = fill_into_bag_2(space, goods_list)
23             if space_plan[0] + goods.value > max:
24                 max = space_plan[0] + goods.value
25                 plan = [goods] + space_plan[1]
26     # 設置緩存,M空間的最佳方案
27     sd[M] = max, plan
28
29     return max, plan

  


   作者:我是8位的

  出處:http://www.cnblogs.com/bigmonkey

  本文以學習、研究和分享為主,如需轉載,請聯系本人,標明作者和出處,非商業用途! 

  掃描二維碼關注公眾號“我是8位的”


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM