sql表中只有子节点的递归_动态规划与静态规划、递归、分治、回溯
動態規劃算是運籌學或者算法中的硬骨頭了。不是說算法本身有多難,而是學完用完之后還是感覺到對其領會的不夠深入,一種能用其術,不知其道的感覺。在很多教材或者回答中,經常看多將動態規劃放在遞歸這一部分中。當初學的時候也會有困擾,弄不清楚動態規劃和其他算法最本質的區別,希望接下來的內容可以幫到大家。
對于一些不太了解動態規劃的同學,建議先從后面看起,確定了動態規劃的原理再看一遍,興許就觸及了動態規劃的靈魂。溫故而知新嘛。
動態規劃與線性規劃
動態規劃提出于20世紀50年代。
之所以叫做規劃,其目的還是用于求解最優。名曰“動態”,說明與運籌學里面的靜態規劃(線性規劃)有所區別,但區別在哪呢?
我們來舉個線性規劃例子:
小明的媽媽給了小明10塊零花錢,小明拿著零花錢去買糖。糖果店有巧克力、奶糖和水果糖,分別是3塊、2塊、1塊。小明每種糖都能嘗嘗的條件下,怎么賣數量最多的糖。
這個問題實在是很容易,先挨個買一顆,然后all in 最便宜的水果糖,得到結果一共是7顆(1,1,5)。
但是我們還是仔細看一下這個問題,通過確定規劃目標和約束得到了以下不等式組:
但是我們突然想到,買糖不是可以一顆一顆的賣么。如果是一顆一顆賣,階段其實就是產生了。花10塊錢買到最多的糖的階段來源于上一個階段,在上一個狀態的時候,小明需要決定花多少錢買一顆糖從而進化到最終的最佳狀態。在這個過程里,小明手上的糖的數量就代表著階段的數量,而原本的線性規劃的問題就轉化成了一個動態規劃的問題。
我們回想一下線性規劃和動態規劃的求解過程。線性規劃最常用的求解方法就是單純形法。在可行域中選擇一個初始點,然后通過在單純型上更新最優找到更好的點:(1,1,1) -> ... -> (1,1,5)。而動態規劃是通過遍歷得到每個階段的最佳狀態作為下一個階段的基礎。如果對于一些連續數值進行動態規劃,計算量可能會相當大,畢竟遍歷是一種不怎么聰明的辦法。因此從方法上,線性規劃的問題都可以通過動態規劃去解決,但是很多時候通過動態規劃解決的效率有些太低了。動態規劃本質還是一種非線性規劃(求解目標函數或約束條件中有一個或幾個非線性函數),通過非線性來解決線性問題是可行的,只不過沒有必要。但是反過來就沒那么可行了,因為當存在順序之后,可能會有一些約束或目標函數轉變成非線性。
總結一下,我個人認為線性規劃是一類問題,而動態規劃是一種算法或是一種分析手段,很多線性規劃的問題可以通過動態規劃解決,有些動態規劃解決的問題其本質還是線性規劃。舉一個例子:如果說線性規劃是計算1累加到100的值得話,動態規劃就是首尾相加法。
動態規劃
什么樣的問題可以用動態規劃來解決:
最優子結構和重疊子結構
我們用一個例子來看這個問題:還是小明,小明現在有1元,5元,10元的硬幣來找錢,如何找回硬幣數量最少(請忽略貪婪算法)。
- 最優子結構:當一個問題的最優解來源于其子問題的最優解時。當找錢數量為6元的時候,子問題是找錢為1元和找錢為5元(因為只有找錢1元或5元時能通過再找一枚硬幣變成找錢6元。6元問題的最優解必然是以1元問題和5元問題為基礎的,它的最優解包含了子問題的最優解。如果要找6元的時候,買方說:“找兩塊就行了,剩下的我再拿瓶可樂”。這個時候找6元問題的最優解就不包含子問題的最優解了。
- 重復子問題:這個問題的解決方式與子問題的解決方式是一樣的。也就是說獲取最優解的方式是一樣的。找6元的標準是最少硬幣數,而找5元的標準同樣是最少硬幣數,計算過程是相同的。
表中是拆解了例子的過程。每一個階段都能找到一個上一階段,直至初始階段。
num_coin = min(1+num_coin(change-1), 1+num_coin(change-5), 1+num_coin(change-10)) ?有的講解或教材會將實現動態規劃的方式分為兩種:
- 遞歸法
- 迭代法
兩者的核心原理都是一樣的,兩種方法的實現如下所示:
coin_list = [1,5,10]#遞歸 def recmoney(coin_list, change):min_coins = changeif change in coin_list:return 1else:for i in [c for c in coin_list if c <= change]:num_coins = 1 + recmoney(coin_list, change - i)if num_coins < min_coins:min_coins = num_coinsreturn min_coins ? #迭代 def recmoney(coin_list, change):min_coins = changeif change in coin_list:return 1else:for i in range(len(coin_list)):for j in range(change, coin_list[i]-1, -1):for k in range(1, min_coins+1):if j >= k*coin_list[i]:f[j] = min(f[j], f[j - k*coin_list[i]] + k)return f[m]因此我們可以看到遞歸其實是實現動態規劃的一種方式而并不約等于動態規劃,很多教科書上說動態規劃是附加記憶卡的遞歸的說法,我覺得可能不太恰當。
那動態規劃問題的分析框架是啥,或者說怎么樣能夠更快的理清楚這個動態規劃問題?
我認為有三個點:
當我們確定了這三個點之后,整個問題就非常清楚了。這里可以用一個簡單地例子檢驗是否領會了:https://leetcode-cn.com/problems/ones-and-zeroes/
零一問題中:階段的單位是一個數組、變化的單位是一個0或1、每個階段的變化量就是多識別出一個數組所包含的0和1的數量。整個問題就變成了
動態規劃與備忘錄
在計算子節點中會存在很多重復的計算。比如小明要找7元零錢,7元零錢的最優來源于6元和2元的最優解(因為只存在1元和5元的硬幣),6元依次減少會再見到2元的場景一次。所以對找2元的情景就會計算幾次,如果我們把第一次的計算結果記錄下來,之后再遇到這樣的情況就能夠直接使用,不用再往下算了。這就是備忘錄的作用,其本質就是對原問題行程的樹結構進行剪枝,把重復計算的樹枝減掉。
coin_list = [1,5,10]#遞歸 def recmoney(coin_list, change, note):min_coins = changeif change in coin_list:note[change] = 1return 1elif note[change] > 0:return note[change]else:for i in [c for c in coin_list if c <= change]:num_coins = 1 + recmoney(coin_list, change - i, note)if num_coins < min_coins:min_coins = num_coinsnote[change] = min_coinsreturn min_coins動態規劃與遞歸、分治
動態規劃與遞歸其實很多在之前已經涉及了。既然動態規劃問題的前提是重復的子問題以及問題的最優解是建立在一系列子問題的最優解上的,這個特性很符合遞歸的應用要求,所以基本都是使用遞歸的方法來解決動態規劃問題的。但是這并不說明動態規劃與遞歸能畫上等號,也不是說動態規劃就是一個有記憶能力的遞歸。動態規劃更重要的是對階段和變化量的識別思想,而遞歸是解決這一問題的手段。
分治法是將一個很大很復雜的問題拆分成為一個一個可解決的相同的子問題,然后返回子問題的解,形成原問題解。這個思路和動態規劃很相似,只是動態規劃更專注于“規劃”也就是求最優解。遞歸算法從廣義上就是分治的思想。
一個被說爛了的例子:求斐波拉切數列
無論是循環迭代還是遞歸,原理都是把原來的一模一樣的公式再來一遍:
def fib(N: int) -> int:def rcfib(N): #定義遞歸函數if N in [0, 1]:return Nreturn rcfib(N-1) + rcfib(N-2)def fib(N: int) -> int:if N in [0, 1]:return Na, b = 0, 1while N > 1: #通過迭代a, b = b, a + bN -= 1return b動態規劃與回溯
回溯是一種搜索算法,通過從某節點開始選擇子節點,再以當前的子節點向下選擇子節點的子節點,直到沒有子節點沒有被遍歷過,再返回該點的父節點。
又是遞歸(深度優先搜索DFS)。
總結一下
動態規劃、分治和回溯我更傾向將其定義為解決問題的思路,而遞歸作為一種方法或計算方式。
解決動態規劃問題的思路是找到階段的單位、變化的單位、每個階段變化的量就很容易理清整棵樹的關系了。
更重要的是:請千萬學好遞歸。
總結
以上是生活随笔為你收集整理的sql表中只有子节点的递归_动态规划与静态规划、递归、分治、回溯的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ad取消覆铜_【学院推荐】PCB工程师不
- 下一篇: android 自定义view实现拖动放