[算法系列] 深入递归本质+经典例题解析——如何逐步生成, 以此类推,步步为营
[算法系列] 深入遞歸本質(zhì)+經(jīng)典例題解析——如何逐步生成, 以此類推,步步為營(yíng)
本文是遞歸系列的第三篇, 第一篇介紹了遞歸的形式以及遞歸設(shè)計(jì)方法(迭代改遞歸),;第二篇以遞歸為引子, 詳細(xì)介紹了快排和歸排以及堆排的核心思想; 本篇主要通過(guò)幾個(gè)題, 從遞推, 歸納法的角度, 深入了介紹了遞歸的本質(zhì)和具體應(yīng)用.
往期回顧:
回顧我們?cè)诘谝黄恼掠懻摰倪f歸中, 下面是我們能夠看到現(xiàn)象形式:
f(n) -> f(n - 1) -> f(n-2) -> ... -> f(1)但實(shí)際本質(zhì)是: 為了解決/完成 f(n), 必先完成f(n- 1); 為解決f(n-1),必先解決f(n-2) … 那么最先要解決f(1)
f(1) -> f(2) -> f(3) -> ... ->f(n)回顧以前學(xué)過(guò)的數(shù)學(xué)歸納法:
1. 證明當(dāng)k=1時(shí),條件成立 2. 假設(shè)k=n(n>=1)時(shí),條件成立 3. 證明k=n+1,條件成立 得到結(jié)論: k取任意正整數(shù)時(shí),條件成立如果沒(méi)記錯(cuò)的話這叫第一數(shù)學(xué)歸納法, 往往我們用來(lái)證明構(gòu)造的某些式子在給定自然數(shù)集合(全體或局部)的正確性. 而數(shù)學(xué)歸納法本質(zhì)是什么呢? 通俗來(lái)看, 就是首先證明了k=1時(shí)的正確性, 然后證明k = n 成立可以推導(dǎo)出k=n+1成立. 根據(jù)上述兩個(gè)條件可以得出k=2也就成立了… 然后k=3也就成立… 本質(zhì)是遞推.
- 遞歸解決的本質(zhì)是先從f(1)->f(2)->…->f(n), 小問(wèn)題解決了,再解決大問(wèn)題
- 數(shù)學(xué)歸納法式從k = 1 逐層證明, 或者說(shuō)證明k=n和k=n+1的關(guān)系,然后遞推
- 遞推, 就是按照前一個(gè)(或幾個(gè))的關(guān)系推理出下一個(gè) …
recursion一詞既可以翻譯為遞推,也可以翻譯為遞歸, 這里的歸應(yīng)該是是規(guī)約的意思. 注意這里的遞歸和編程形式中的 遞歸調(diào)用 是有點(diǎn)區(qū)別的, 編程中談到的形式化更多一些, 而數(shù)學(xué)本質(zhì)還是和遞歸遞推沒(méi)有區(qū)別.
遞歸, 遞推, 數(shù)學(xué)歸納法本質(zhì)正是同一種東西.
好了,現(xiàn)在看來(lái)知道了這些似乎作用不大. 我們還是舉個(gè)例子, 搞懂遞歸, 看這篇就夠了 !! 遞歸設(shè)計(jì)思路 + 經(jīng)典例題層層遞進(jìn) 中的青蛙上樓梯問(wèn)題.
1. 再談青蛙上樓梯
樓梯有n個(gè)臺(tái)階, 一個(gè)青蛙一次可以上1 , 2 或3 階 , 實(shí)現(xiàn)一個(gè)方法, 計(jì)算該青蛙有多少種上完樓梯的方法文中給出了遞歸的解法:
(回憶找重復(fù),找變化,找出口)
假如青蛙上10 階, 那么其實(shí)相當(dāng)于要么 站在第9 階向上走1步,要么 站在第8 階向上走兩步, 要么在第7階向上走3步. 每個(gè)大于3階樓梯的問(wèn)題都可以看成幾個(gè)子問(wèn)題的堆疊
變化:令f(n) 為 青蛙上n階的方法數(shù). 則f(n) = f(n -1) +f(n - 2) + f(n -3) , 當(dāng)n >= 3
出口: 當(dāng)n = 0 時(shí) ,青蛙不動(dòng) , f(0) = 0; n = 1時(shí) ,有1種方法 , n = 2 時(shí) 有2 種方法
def f(n):if n == 0 :return 1 #站著不動(dòng)也得返回1的, 因?yàn)閷?shí)際上0種方法的是沒(méi)意義if n == 1:return 1if n == 2:return 2return f(n - 1) +f(n - 2) +f(n -3)顯然這樣的遞歸方法不是很直觀的, 其實(shí)一開(kāi)始拿到這題 , 普通地想, 應(yīng)該是拿出張白紙來(lái), 左邊起名一列: 階數(shù) , 右邊起名一列: 走法
階數(shù) 走法 1 1 0->1 2 2 0->1->2 0->2 3 4 0->1->2->3 0->1->3 0->2->3 0->3 4 7 ... ... ...詳細(xì)康康階數(shù)為4時(shí)的走法:
注意我分成了三列寫, 如果不看紅色部分的話, 三列分別代表了上第1,2,3階的方法. 現(xiàn)在帶著紅色的 ->4 一起看:
- 第一列: 相當(dāng)于先上到第1階再一次上到4 (因?yàn)樽畲罂梢钥?階嘛)
- 第二列: 相當(dāng)于先上到第2階再一次上到4 (相當(dāng)于最后一次跨2階嘛)
- 第三列:相當(dāng)于先上到第3階再一次上到4(最后一次跨1階即可)
顯然上到第四階的方法剛好就是這三列的和了 …
到這里, 有興趣的同學(xué)可以在寫出階數(shù)為5的走法. 但其實(shí)也會(huì)得到下面的結(jié)論:
- 第一列: 相當(dāng)于先上到第2階再一次上到5 (因?yàn)樽畲罂梢钥?階嘛)
- 第二列: 相當(dāng)于先上到第3階再一次上到5 (相當(dāng)于最后一次跨2階嘛)
- 第三列:相當(dāng)于先上到第4階再一次上到5(最后一次跨1階即可)
顯然上到第五階的方法剛好就是這三列的和了 …
…想一想, 規(guī)律也就可以得出了
階數(shù)為n的走法. 但其實(shí)也會(huì)得到下面的結(jié)論:
- 或者先上到第n-3階再一次上到n (因?yàn)樽畲罂梢钥?階嘛)
- 或者先上到第n-2階再一次上到n (相當(dāng)于最后一次跨2階嘛)
- 或者于先上到第n-1階再一次上到n(最后一次跨1階即可)
所以 f(n) = f(n -1) +f(n - 2) + f(n -3) 不是憑空產(chǎn)生, 而真是一步一步的像上面一樣推出來(lái) – 遞歸表達(dá)也是如此
下面就可以自然的得到遞歸法實(shí)現(xiàn)了
def go_stairs(n):if n <= 1:return 1if n == 2 :return 2if n == 3 :return 4return go_stairs(n - 1) + go_stairs(n - 2) + go_stairs(n - 3)寫出了出口條件, 寫出了遞推式, 計(jì)算機(jī)不就幫我們像上面一樣, 一步一步地推下去了么…
同樣的, 我們也可以按照我們的推理演算的順序, 用一個(gè)長(zhǎng)度為3的數(shù)組, 保存每次得到的f(n -1) ,f(n - 2) ,f(n -3), 下一輪再更新…這就是我們遞推的迭代法實(shí)現(xiàn) :
def go_stairs_ite(n):#聲明一個(gè)長(zhǎng)度為4的數(shù)組保存每次計(jì)算得到值, 用于存儲(chǔ)每次計(jì)算所需的三個(gè)值和一個(gè)結(jié)果值arr =[]if n <= 1:return 1if n == 2 :return 2if n == 3 :return 4arr[0] = 1arr[1] = 2arr[2] = 4 #1 2 4 ()for i in range(4, n+1):arr[3] = arr[0] #1 2 4 1 不斷地空出來(lái)一個(gè)固定位置,存結(jié)果 arr[0] = arr[1] #2 2 4 1 arr[1] = arr[2] #2 3 4 1arr[2] = arr[3] + arr[0] + arr[1] #2 4 7 1return arr[2]2. 機(jī)器人走方格 cc150 9.2
有一個(gè) X*Y 的方格, 一個(gè)機(jī)器人只能走格點(diǎn)且只能向右或者向右走, 要從左上角走到左下角 請(qǐng)?jiān)O(shè)計(jì)一個(gè)算法, 計(jì)算機(jī)器人有多少種走法 給定兩個(gè)個(gè)正整數(shù)X , Y, 返回機(jī)器人走法的數(shù)目.分析如下:
得到遞推公式和出口條件就可以寫出遞歸形式代碼:
''' 遞歸形式 ''' def robot_go_grim(x, y):if (x == 1 or y == 1):return 1return robot_go_grim(x - 1 , y ) +robot_go_grim(x , y - 1 )想清楚了, 代碼看上去是不是異常簡(jiǎn)潔呢?
現(xiàn)在考慮迭代形式: 我們知道,
- 如果只有一個(gè)格子, 那么終點(diǎn)即為起點(diǎn), 結(jié)果為1
- n * 1 或 1 * m 的情況, 總是只有一種走法
在對(duì)應(yīng)格子中填上從此處到右下角的走法, 目前可得到:
然后就可以填格子, 根據(jù)就是f(n,m) = f(n - 1,m) +f(n, m -1) .
這其實(shí)也就相當(dāng)于:當(dāng)前的方法數(shù) = 自己下方格子處的方法數(shù) + 右邊格子處的方法數(shù)
- 填到圖中值為6 的格子處, 也就得到了f(3,3)的解
- 填到圖中值為5 的格子處, 也就得到了f(2,5)的解
3.輸出合法括號(hào)cc9.6
編寫一個(gè)方法,打印n對(duì)括號(hào)的全部有效組合(即左右括號(hào)正確匹配) 示例 輸入:3 輸出:()()(),((())),(())(),()(()),(()())按照前兩道的思路, 我們依然從最初開(kāi)始逐步遞推: 尋找每次大規(guī)模問(wèn)題和其小一號(hào)問(wèn)題的關(guān)系. 同時(shí)出口條件又是已知的
def proper_bracket(n):''':param n: 輸入括號(hào)對(duì)數(shù):return:'''#聲明一個(gè)set用于存放結(jié)果sn = set()#出口條件if n == 1 :sn.add("()")return snsn_1 = proper_bracket(n-1) #聲明小一號(hào)規(guī)模的子問(wèn)題,上一次求得的sn作為下一次的sn_1for e in sn_1: #以下全是歸回來(lái)的副作用, 闡明子問(wèn)題與父問(wèn)題的關(guān)系sn.add("()"+e) sn.add(e+"()")sn.add("("+e+")")return snprint(proper_bracket(3))稍微解釋下上述代碼,
- n=1時(shí)為出口條件,答案明確
- n>1時(shí)依次調(diào)用n-1, 因此首先求得的是n=2時(shí), sn_1="()",針對(duì)它的每一項(xiàng)進(jìn)行加左,加右,加外三個(gè)操作得到sn, 再逐次返回
下面的迭代形式正是遞推過(guò)程的正向體現(xiàn)
''' 迭代形式 ''' def proper_bracket_ite(n):sn = set()sn.add("()")if n ==1 :return snfor i in range(2 , n+1 ):sn_new = set() #從n=2開(kāi)始每次創(chuàng)建一個(gè)新集合set_new, 從sn推出set_newfor e in sn:sn_new.add("()" +e)sn_new.add(e + "()")sn_new.add("(" + e + ")")sn = sn_new #set_new變sn,周而復(fù)始return sn4.集合的所有子集cc9.4
編寫一個(gè)方法,返回int集合的所有子集 # 例如: # 輸入: [1,2,3] # 輸出: [],[1],[1,2],[1,2,3],[2,3],[3],[1,3],[2]此題我們同樣按照小規(guī)模往大規(guī)模進(jìn)行推理,
-
當(dāng)只有一個(gè)元素時(shí), 只用考慮有這個(gè)元素(子集1),或者沒(méi)有這個(gè)元素(子集2),
-
當(dāng)有兩個(gè)元素時(shí),可以這樣考慮:
- 加入第一個(gè)元素 =>形成子集1
- 加入第二個(gè)元素=>形成子集2
- 彈出第二個(gè)元素
- 彈出第一個(gè)元素
- 加入第二個(gè)元素=>形成子集3
-
當(dāng)有多個(gè)元素時(shí), 對(duì)于每個(gè)元素,都有試探放入或者不放人集合中兩個(gè)選擇:
-
選擇該元素放入,遞歸地進(jìn)行后續(xù)元素的選擇,完成放入該元素后續(xù)所有元素的試探;
-
之后將其拿出
-
再進(jìn)行一次選擇不放入該元素,遞歸地進(jìn)行后續(xù)元素的選擇,完成不放入該元素時(shí)對(duì)后續(xù)元素的試探
-
設(shè)arr傳入的數(shù)組, item為每一個(gè)子集, res為最終的結(jié)果集, i 表示當(dāng)前arr的下標(biāo)
下圖演示遞歸求解的調(diào)用思路:
代碼如下
對(duì)于這種放或不放的01事件,還可以用二進(jìn)制表示的方法。。具體來(lái)看,就是就是是和否可能性的組合問(wèn)題.以原始集合{1,2,3}為例,下圖可以很好的表示子集的所有可能性:
因此,我們可以用當(dāng)前位置上1或0表示選或不選當(dāng)前位置上的元素, 數(shù)組長(zhǎng)度即為二進(jìn)制數(shù)的位數(shù), 即可用一個(gè)3位二進(jìn)制數(shù)保存{A,B.C}的所有可能性.
而在尋找這種可能時(shí), 可從0遍歷到2^(len(arr))-1, 其中的每一個(gè)二進(jìn)制數(shù),剛好表達(dá)的是一種可能性. 比如:110,即為{A,B}.
def get_subset_ite(arr):res= list() #最終結(jié)果集for i in range(2**len(arr) - 1 ,-1 , - 1):item = list() #d當(dāng)前子集for j in range(len(arr) - 1 , -1 ,- 1): #j是遍歷每一位,當(dāng)該為為1,則對(duì)應(yīng)的元素加入if (i>>j) & 1 == 1: #若該二進(jìn)制位為1,則加入itemitem.append(arr[j])res.append(item) return(res)5.全排列cc9.5
寫一個(gè)方法,返回一個(gè)字符串?dāng)?shù)組的全排列 例如 輸入:"ABC" 返回:"ABC","ACB","BAC","BCA","CAB","CBA"這個(gè)問(wèn)題和剛剛的那個(gè)子集問(wèn)題結(jié)合起來(lái)看
- 子集問(wèn)題是:針對(duì)某一位上的元素,選還是不選這個(gè)元素的問(wèn)題(0或1). 對(duì)每一位來(lái)說(shuō)均有兩種可能, 總計(jì)為2^n個(gè)情況(子集)
- 全排列問(wèn)題是: 每個(gè)位置都要選,但是是選n個(gè)當(dāng)中哪一個(gè)的問(wèn)題. 其次,當(dāng)前選定一個(gè)了,下一個(gè)可選情況就少1了.因此情況個(gè)數(shù)為n!
那么如何用遞歸思考方式著手解決呢? 還從小規(guī)模逐漸推吧
- “b"可以放在"a"的前面形成"ab”
- 也可以放在"a"后面形成"ba"
- 對(duì)于"ab",有a左,ab中間,b右三個(gè)位置可加入, 分別形成三個(gè)串
- 對(duì)于"ba",同樣有三個(gè)為加如c,同樣形成三個(gè)新串
由此推而廣之到n時(shí):
令 S(n-1) = {前n-1的子串全排列集合}, 則S(n)與S(n-1)關(guān)系為:for each item in S(n-1):for each empty between str[i] and str[i+1]:item.append(str[n])res.append(item)上面的方法, 其實(shí)并不是我們平時(shí)所一下想到的, 那么我們平時(shí)是怎么想的呢?
- 先以a開(kāi)頭, b開(kāi)頭 , c開(kāi)頭寫…abcd
- 調(diào)換最后兩位順序…
- 逐漸從后面向前面調(diào)換順序, 寫完所有a打頭的item
- 接下來(lái)交換a和b, 以b打頭, a第二個(gè)寫… 寫完為止
- 然后依然b打頭, c第二個(gè)寫…
- 接下來(lái)交換a和c,c打頭,a第二個(gè)…
看文字感覺(jué)不好表述, 那么還是看圖好了:
藍(lán)色數(shù)字為調(diào)用回溯順序
代碼:
res = list() #全局: 最終結(jié)果list def get_all_array(str):arr = list(str)arr.sort() #先排好序generate(arr, 0)return resdef generate(arr ,k ):#遞歸走到底了 表示排好了if k == len(arr):item = "".join(arr)res.append(item)#從第k位開(kāi)始的每個(gè)字符都嘗試放在新排列的第k個(gè)位置for i in range(k , len(arr)):swap(arr , k ,i) #交換, 形成新的順序, 比如 bc=>cbgenerate(arr , k+1) #遞歸調(diào)用swap(arr , k ,i) #這是返回時(shí)的副作用, 再次交換, 復(fù)原 == >回溯# 輔助函數(shù)swap def swap(arr , i ,j):if i <0 or j < 0 or i > len(arr) or j > len(arr):return "i or j is out of indedx"tem = arr[i]arr[i] = arr[j]arr[j] = tem ['abc', 'acb', 'bac', 'bca', 'cba', 'cab']上面這個(gè)交換-回溯法很簡(jiǎn)潔, 但是并不能按照字典序打印, 下面這個(gè)方法就可以將其字典序打印了
偽代碼如下:
res = list() #存放最終結(jié)果 generate("" , str) #初始時(shí)前綴為空generate(prefix, str) :if prefix.length == str.length:res.add(prefix) #結(jié)果集中放入prefixreturnfor each ch in str:# 這個(gè)字符可用: 在pre中出現(xiàn)的次數(shù) < 在字符集中出現(xiàn)的次數(shù) (這是關(guān)鍵)if prefix.count(ch) < str.count(ch)generate(prefix + ch ,str) #將ch加入prefix中,繼續(xù)遞歸調(diào)用好了, 本次介紹就到這里, 下面來(lái)小結(jié)一下:
-
本文是遞歸系列的第三篇, 第一篇介紹了遞歸的形式以及遞歸設(shè)計(jì)方法(迭代改遞歸),;第二篇以遞歸為引子, 詳細(xì)介紹了快排和歸排以及堆排的核心思想; 本篇主要通過(guò)幾個(gè)題, 從遞推, 歸納法的角度, 深入了介紹了遞歸的本質(zhì)和具體應(yīng)用.
-
本文所談遞歸的"本質(zhì)",是數(shù)學(xué)角度上的,且并未繼續(xù)深入(比如所謂的封閉式計(jì)算方法,直接求通項(xiàng)等). 同時(shí),關(guān)于計(jì)算機(jī)中的遞歸(比如棧開(kāi)辟,函數(shù)存儲(chǔ)等問(wèn)題)并未涉及, 待以后補(bǔ)充學(xué)習(xí)后一定補(bǔ)上.
-
前兩個(gè)題是數(shù)值類問(wèn)題, 后三個(gè)題為非數(shù)值型問(wèn)題. 他們的核心在這里都是: 逐步生成, 以此類推 .
-
遞歸設(shè)計(jì)的方法依然還是 搞懂遞歸, 看這篇就夠了 !! 遞歸設(shè)計(jì)思路 + 經(jīng)典例題層層遞進(jìn) 中詳細(xì)介紹的:
- 找出口條件 ==> 邊界, 最小規(guī)模的問(wèn)題 ==> 初始情況
- 找不變 ==> 解決問(wèn)題的方法不應(yīng)變化, f(n) 與 f(n-m) 才能表述成父問(wèn)題與子問(wèn)題的關(guān)系(回憶前面的漢諾塔問(wèn)題)
- 找變化 ==> n規(guī)模的問(wèn)題與n-m規(guī)模問(wèn)題之間的關(guān)系(考慮走格子, 全排列問(wèn)題) ==> 遞推公式中的n
-
本篇介紹的一些東西將在后續(xù)對(duì)回溯, dfs ,動(dòng)態(tài)規(guī)劃的介紹中有所體現(xiàn). 這里主要強(qiáng)調(diào)的是:
如何觀察問(wèn)題 ==> 從小規(guī)模開(kāi)始遞推 ==> 找出本質(zhì)(遞推公式) ==> 按照方法,設(shè)計(jì)算法(遞歸, 迭代)
接下來(lái)的文章將對(duì)遞歸的一些應(yīng)用: dfs, dp等進(jìn)行介紹
下一篇:[算法系列] 搞懂DFS——設(shè)計(jì)思路+經(jīng)典例題(數(shù)獨(dú)游戲, 部分和, 水洼數(shù)目)圖文詳解
總結(jié)
以上是生活随笔為你收集整理的[算法系列] 深入递归本质+经典例题解析——如何逐步生成, 以此类推,步步为营的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: SpringBoot 错误页面和异常处
- 下一篇: nRF51822 入门必备教程(一篇搞定