斐波那契數列:Fn = Fn-1 + Fn-2 ( n = 1,2 fib(1) = fib(2) = 1)html
代碼以下:python
# _*_coding:utf-8_*_ def fibnacci(n): if n == 1 or n == 2: return 1 else: return fibnacci(n - 1) + fibnacci(n - 2) # 寫這個是咱們會發現計算f(5) 要算兩邊f(4) # f(5) = f(4)+f(3) # f(4) = f(3)+f(2) # f(3) = f(2)+f(1) # f(3) = f(2)+f(1) # f(2) = 1 # 那麼同理,算f(6),咱們會計算兩次f(5),三次f(4).... # 固然不是說全部的遞歸都會重複計算, # 時間隨着數字越大,時間越長 print(fibnacci(10)) # 55
簡單來講,就是想要計算f(5),咱們須要先計算出子問題 f(4) 和 f(3),而後要計算 f(4) ,咱們須要先計算出子問題 f(3) 和 f(2),以此類推,最後遇到 f(1) 或者 f(2) 的時候,結果已知,就能直接返回結果,遞歸樹再也不向下生長了。git
遞歸算法的時間複雜度怎麼計算?子問題個數乘以解決一個子問題須要的時間。程序員
子問題個數,即遞歸樹中節點的總數。顯然二叉樹節點總數爲指數級別,因此子問題個數爲 O(2^n)。github
解決一個子問題的時間,在本算法中,沒有循環,只有 f(n - 1) + f(n - 2) 一個加法操做,時間爲 O(1)。算法
因此,這個算法的時間複雜度爲 O(2^n),指數級別,爆炸。數組
觀察遞歸樹,很明顯發現了算法低效的緣由:存在大量重複計算,好比 f(5) 被計算了兩次,並且你能夠看到,以 f(5) 爲根的這個遞歸樹體量巨大,多算一遍,會耗費巨大的時間。更況且,還不止 f(5) 這一個節點被重複計算,因此這個算法及其低效。app
這就是動態規劃問題的第一個性質:重疊子問題。下面,咱們想辦法解決這個問題。函數
明確了問題,其實就已經把問題解決了一半。即然耗時的緣由是重複計算,那麼咱們能夠造一個「備忘錄」,每次算出某個子問題的答案後別急着返回,先記到「備忘錄」裏再返回;每次遇到一個子問題先去「備忘錄」裏查一查,若是發現以前已經解決過這個問題了,直接把答案拿出來用,不要再耗時去計算了。學習
通常使用一個數組充當這個「備忘錄」,固然你也可使用哈希表(字典),思想都是同樣的。
def fibnacci_n_recurision(n): f = [0, 1, 1] if n > 2: for i in range(n - 2): num = f[-1] + f[-2] f.append(num) return f[n] print(fibnacci_n_recurision(10))
實際上,帶「備忘錄」的遞歸算法,把一棵存在巨量冗餘的遞歸樹經過「剪枝」,改形成了一幅不存在冗餘的遞歸圖,極大減小了子問題(即遞歸圖中節點)的個數。
遞歸算法的時間複雜度怎麼算?子問題個數乘以解決一個子問題須要的時間。
子問題個數,即圖中節點的總數,因爲本算法不存在冗餘計算,子問題就是 f(1), f(2), f(3) ... f(20),數量和輸入規模 n = 20 成正比,因此子問題個數爲 O(n)。
解決一個子問題的時間,同上,沒有什麼循環,時間爲 O(1)。
因此,本算法的時間複雜度是 O(n)。比起暴力算法,是降維打擊。
至此,帶備忘錄的遞歸解法的效率已經和動態規劃同樣了。實際上,這種解法和動態規劃的思想已經差很少了,只不過這種方法叫作「自頂向下」,動態規劃叫作「自底向上」。
啥叫「自頂向下」? 就是從上向下延伸,都是從一個規模較大的原問題好比說 f(5),向下逐漸分解規模,直到 f(1) 和 f(2) 觸底,而後逐層返回答案,這就叫「自頂向下」。
啥叫「自底向上」?反過來,咱們直接從最底下,最簡單,問題規模最小的 f(1) 和 f(2) 開始往上推,直到推到咱們想要的答案 f(5),這就是動態規劃的思路,這也是爲何動態規劃通常都脫離了遞歸,而是由循環迭代完成計算。
爲了讓咱們的說服更有理一些,這裏寫了一個裝飾器,咱們經過運行時間看。一樣對於上面兩個函數,一個遞歸,一個非遞歸,咱們輸入 n=15
# cal_time.py 函數代碼以下: import time def cal_time(func): def wrapper(*args, **kwargs): t1 = time.time() result = func(*arg, **kwargs) t2 = time.time() print("%s running time: %s secs." % (func.__name__, t2 - t1)) return result return wrapper 運行結果: fibnacci running time: 0.01000070571899414 secs. fibnacci_n_recurision running time: 0.0 secs.
總結來講,就是遞歸很是很是的慢,那非遞歸相對來講就比較快了。那爲何呢?就是爲何遞歸的效率低。咱們上面代碼也說過了,就是對子問題進行重複計算了。那第二個函數爲何快呢,咱們將每次的計算結果存在了函數裏,直接調用,避免了重複計算(固然不是說全部的遞歸都會重複計算子問題),第二個函數咱們其實能夠看作是動態規劃的思想,從上面的代碼來看:
動態規劃的思想==遞推式+重複子問題
怎麼理解呢,就是說動態規劃遵循一套固定的流程:遞歸的暴力解法 ——> 帶備忘錄的遞歸解法 ——> 非遞歸的動態規劃解法 這個過程是層次遞進的解決問題的過程,你若是沒有前面的鋪墊,直接看最終的非遞歸動態規劃解法,固然以爲難。
動態規劃(dynamic programming)是運籌學的一個分支,是求解決策過程(decision process)最優化的數學方法。把多階段過程轉化爲一系列單階段問題,利用各階段之間的關係,逐個求解,創立了解決這類過程優化問題的新方法——動態規劃。
若要解一個給定問題,咱們須要解其不一樣部分(即子問題),再合併子問題的解以得出原問題的解。一般許多子問題很是類似,爲此動態規劃法試圖僅僅解決每一個子問題一次,從而減小計算量:一旦某個給定子問題的解已經算出,則將其記憶化存儲,以便下次須要同一個子問題解之時直接查表。這種作法在重複子問題的數目關於輸入的規模呈指數增加時特別有效。
動態規劃最重要的有三個概念:一、最優子結構 二、邊界 三、狀態轉移方程
因此咱們在學習動態規劃要明白三件事情:
1,目標問題
2,狀態的定義:opt[n]
3,狀態轉移方程:opt[n] = best_of(opt[n-1], opt[n-2])
其實狀態轉移方差直接表明着暴力解法,千萬不要看不起暴力解,動態規劃問題最難的就是寫出狀態轉移方差,即這個暴力解。
某公司出售鋼條,出售價格與鋼條長度直接的關係以下表:
問題:如今有一條長度爲 n 的鋼條和上面的價格表,求切割鋼條方案,使得總收益最大。
分析:長度爲4的鋼條的全部切割方案以下:(C方案最優)
思考:長度爲 n 的鋼條的不一樣切割方案有幾種?
下面是當長度爲n的時候,最優價格的表格( i 表示長度爲 n ,r[i] 表示最優價格)
設長度爲 n 的鋼條切割後最優收益值爲 Rn,能夠獲得遞推式:
第一個參數Pn 表示不切割
其餘 n-1個參數分別表示另外 n-1種不一樣切割方案,對方案 i=1,2,...n-1 將鋼條切割爲長度爲 i 和 n-i 兩段
方案 i 的收益爲切割兩段的最優收益之和,考察全部的 i,選擇其中收益最大的方案
能夠將求解規模爲 n 的原問題,劃分爲規模更小的子問題:完成一次切割後,能夠將產生的兩段鋼條當作兩個獨立的鋼條切割問題。
組合兩個子問題的最優解,並在全部可能的兩段切割方案中選取組合收益最大的,構成原問題的最優解。
鋼條切割知足最優子結構:問題的最優解由相關子問題的最優解組合而成,這些子問題能夠獨立求解。
鋼條切割問題還存在更簡單的遞歸求解方法:
代碼以下:
def _cut_rod(p, n): if n == 0: return 0 q = 0 for i in range(1, n+1): q = max(q, p[i] + _cut_rod(p, n-i)) return q
以下圖所示,當鋼條的長度增長時候,切割的方案次數隨着指數增長。當n=1的時候切割1次,n=2的時候切割2次,n=3的時候切割4次,n=4的時候切割8次。。。。
因此:自頂向下遞歸實現的時間複雜度爲 O(2n)
代碼以下:
# _*_coding:utf-8_*_ import time # 給遞歸函數一個裝飾器,它就遞歸的裝飾!! 因此爲了防止這樣咱們再套一層便可 def cal_time(func): def wrapper(*args, **kwargs): t1 = time.time() result = func(*args, **kwargs) t2 = time.time() print('%s running time : %s secs' % (func.__name__, t2 - t1)) return result return wrapper # p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 21, 23, 24, 26, 27, 28, 30, 33, 36, 39, 40] p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30] def cut_rod_recurision_1(p, n): if n == 0: return 0 else: res = p[n] for i in range(1, n): res = max(res, cut_rod_recurision_1(p, i) + cut_rod_recurision_1(p, n - i)) return res # print(cut_rod_recurision_1(p, 9)) def cut_rod_recurision_2(p, n): if n == 0: return 0 else: res = 0 for i in range(1, n + 1): res = max(res, p[i] + cut_rod_recurision_2(p, n - i)) return res # print(cut_rod_recurision_2(p, 9)) @cal_time def c1(p, n): return cut_rod_recurision_1(p, n) @cal_time def c2(p, n): return cut_rod_recurision_2(p, n) print(c1(p, 10)) print(c2(p, 10)) ''' c1 running time : 0.02000117301940918 secs 30 c2 running time : 0.0010001659393310547 secs 30 '''
咱們經過計算時間,發現第二個遞歸方法明顯比第一個遞歸方法快不少。那麼是否還有更簡單的方法呢?確定有,下面學習動態規劃。
遞歸算法因爲重複求解相同子問題,效率極低。即便優化事後的遞歸也效率不高。那這裏使用動態規劃。
動態規劃的思想:
動態規劃解法代碼:
def cut_rod_dp(p, n): r = [0 for _ in range(n+1)] for j in range(1, n+1): q = 0 for i in range(1, j+1): q = max(q, p[i]+r[j-i]) r[j] = q return r[n]
或者便於理解這樣:
def cut_rod_dp(p, n): r = [0] for i in range(1, n+1): res = 0 for j in range(1, i+1): res = max(res, p[j]+r[i-j]) r.append(res) return r[n]
時間複雜度: O(n2)
如何修改動態規劃算法,使其不只輸出最優解,還輸出最優切割方案?
對於每一個子問題,保存切割一次時左邊切下的長度
下圖爲r[i] 表示最優切割的價格,s[i]表示切割左邊的長度。
代碼以下:
def cut_rod_extend(p, n): r = [0] s = [0] # 這個循環的意思是從底向上計算 for i in range(1, n+1): res_r = 0 # 用來記錄價格的最優值 res_s = 0 # 用來記錄切割左邊的最優長度 for j in range(1, i+1): if p[j] + r[i-j] > res_r: res_r = p[j] + r[i-j] res_s = j r.append(res_r) s.append(res_s) return r[n], s def cut_rod_solution(p, n): r, s = cut_rod_extend(p, n) ans = [] while n>0: ans.append(s[n]) n-= s[n] return ans print(cut_rod_extend(p, 10)) # (30, [0, 1, 2, 3, 2, 2, 6, 1, 2, 3, 10]) print(cut_rod_solution(p, 9)) # [3, 6]
1,最優子結構
2,重疊子問題
一個序列的子序列是在該序列中刪去若干元素後獲得的序列。例如:ABCD 和 BDF 都是 ABCDEFG 的子序列。
在一個序列中,子串是連續的,子序列能夠不連續。
最常公共子序列(LCS)問題:給定兩個序列 X 和 Y,求 X 和 Y 長度最大的公共子序列。例如 X = ABBCBDE, Y = DBBCDB , LCS(X, Y) = BBCD 。
應用場景:字符串類似度比對。
當X的長度爲m,Y的長度爲n,則時間複雜度爲: 2^(m+n)
雖然咱們最早想到的時暴力窮舉法,可是很顯然,由其時間複雜度可知,這是不可取的。
例如:要求 a = ABCBDAB 與 b = BDCABA 的LCS:
因爲最後一位 B!= A
所以LCS(a, b)應該來源於 LCS(a[: -1], b)與 LCS(a, b[: -1]) 中更大的哪個。
最優解的遞推式以下:
c[i,j] 表示 Xi 和 Yj 的LCS 長度。
代碼以下:
def lcs_length(x, y): m = len(x) n = len(y) c = [[0 for _ in range(n + 1)] for _ in range(m + 1)] for i in range(1, m + 1): for j in range(1, n + 1): if x[i - 1] == y[j - 1]: # i,j位置上的字符匹配的時候,來自於左上方 c[i][j] = c[i - 1][j - 1] + 1 else: c[i][j] = max(c[i - 1][j], c[i][j - 1]) # 逐行打印 for _ in c: print(_) return c[m][n] # 最優值出來了,可是過程沒有出來,也就是隻有最長,不知道公共子序列 # print(lcs_length("ABCBDAB", "BDCABA")) ''' [0, 0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 1, 1, 1] [0, 1, 1, 1, 1, 2, 2] [0, 1, 1, 2, 2, 2, 2] [0, 1, 1, 2, 2, 3, 3] [0, 1, 2, 2, 2, 3, 3] [0, 1, 2, 2, 3, 3, 4] [0, 1, 2, 2, 3, 4, 4] 4 ''' def lcs(x, y): m = len(x) n = len(y) c = [[0 for _ in range(n + 1)] for _ in range(m + 1)] # 1左上方 2上方 3 左方 b = [[0 for _ in range(n + 1)] for _ in range(m + 1)] for i in range(1, m + 1): for j in range(1, n + 1): if x[i - 1] == y[j - 1]: # i,j位置上的字符匹配的時候,來自於左上方 c[i][j] = c[i - 1][j - 1] + 1 b[i][j] = 1 elif c[i - 1][j] > c[i][j - 1]: # 來自於上方 c[i][j] = c[i - 1][j] b[i][j] = 2 else: c[i][j] = c[i][j - 1] b[i][j] = 3 return c[m][n], b # c, b = lcs("ABCBDAB", "BDCABA") # for _ in b: # print(_) ''' [0, 0, 0, 0, 0, 0, 0] [0, 3, 3, 3, 1, 3, 1] [0, 1, 3, 3, 3, 1, 3] [0, 2, 3, 1, 3, 3, 3] [0, 1, 3, 2, 3, 1, 3] [0, 2, 1, 3, 3, 2, 3] [0, 2, 2, 3, 1, 3, 1] [0, 1, 2, 3, 2, 1, 3] ''' def lcs_trackback(x, y): c, b = lcs(x, y) i = len(x) j = len(y) res = [] while i > 0 and j > 0: if b[i][j] == 1: # 來自左上方 =》匹配 res.append(x[i - 1]) i -= 1 j -= 1 elif b[i][j] == 2: # 來自上方=》 不匹配 i -= 1 else: # ==3 來自左方 =》不匹配 j -= 1 # 由於是回溯法,因此倒着寫的,咱們最後須要reverse回來 return "".join(reversed(res)) print(lcs_trackback("ABCBDAB", "BDCABA")) # BDAB
給定一個整數數組 nums ,找到一個具備最大和的連續子數組(子數組最少包含一個元素),返回其最大值。
示例:輸入:[-2, 1, -3, 4, -1, 2, 1, -5, 4] 輸出:輸出:6
思路:咱們首先分析題目,爲何最大和的連續子數組不包括其餘的元素而是這幾個呢?若是咱們想在現有的基礎上去擴展當前連續子數組,相鄰的元素是必定要被加入的,而相鄰元素可能會減損當前的和。
算法過程:遍歷數組,用 sum 去維護當前元素加起來的和,當 sum 出現小於0的狀況時,咱們把它設爲0,而後每次都更新全局最大值。
def maxSubArray(self, nums: List[int]) -> int: sum = 0 MaxSum = nums[0] for i in range(len(nums)): sum += nums[i] MaxSum = max(sum, MaxSum) if sum <0: sum = 0 return MaxSum
那看起來這麼簡單,如何理解呢?一開始思考數組是個空的,咱們每次選一個 nums[i] 加入當前數組中新增了一個元素,也就是用動態的眼光去考慮。代碼簡單爲何就能達到效果呢?
咱們進行的加和是按照順序來的,當咱們i 選出來後,加入當前 sum,這時候有兩種狀況:
1,假設咱們當前 sum 一致都大於零,那每一次計算的 sum 都是包括開頭元素的一端子序列。
2,出現小於0的時候,說明咱們當前子序列第一次小於零,因此咱們如今造成的連續數組不能包括以前的連續子序了,因而拋棄他們,從新開始新的子序。
設sum[i]爲以第i個元素結尾的最大的連續子數組的和。假設對於元素i,全部以它前面的元素結尾的子數組的長度都已經求得,那麼以第i個元素結尾且和最大的連續子數組實際上,要麼是以第i-1個元素結尾且和最大的連續子數組加上這個元素,要麼是隻包含第i個元素,即sum[i]= max(sum[i-1] + a[i], a[i])。能夠經過判斷sum[i-1] + a[i]是否大於a[i]來作選擇,而這實際上等價於判斷sum[i-1]是否大於0。因爲每次運算只須要前一次的結果,所以並不須要像普通的動態規劃那樣保留以前全部的計算結果,只須要保留上一次的便可,所以算法的時間和空間複雜度都很小。
代碼以下:
def maxSubArray4(self, nums: List[int]) -> int: length = len(nums) for i in range(1, length): # 當前值的大小與前面的值之和比較,若當前值更大,則取當前值,捨棄前面的值之和 subMaxSum = max(nums[i]+nums[i-1], nums[i]) # 將當前和最大的賦給 nums[i], 新的nums 存儲的爲什麼值 nums[i] = subMaxSum return max(nums)
只要遍歷一遍。nums[i]表示的是以當前這第i號元素結尾(看清了必定要包含當前的這個i)的話,最大的值無非就是看以i-1結尾的最大和的子序能不能加上我這個nums[i],若是nums[i]>0的話,則加上。注意我代碼中沒有顯式地去這樣判斷,不過個人Max表達的就是這個意思,而後咱們把nums[i]肯定下來。
計算機解決問題其實沒有任何奇技淫巧,它惟一的解決辦法就是窮舉,窮舉全部可能性。算法設計沒法就是先思考「如何窮舉」,而後再追求「如何聰明的窮舉」。
列出動態轉移方差,就是在解決「如何窮舉」的問題,之因此說他難,一是由於不少窮舉須要遞歸實現,二是由於有的問題自己的解空間複雜,不難容易窮舉完整。
備忘錄,DP table 就是在追求「如何聰明地窮舉」。用空間換時間的思路,是下降時間複雜度的不二法門。
動態規劃須要和回溯法搭配着使用,動態規劃只負責求最優解,而回溯法則能夠找到最優值的路徑。
回溯法是一種選優搜索法,按選優條件向前搜索,以達到目標。但當探索到某一步時,發現原先選擇並不優或達不到目標,就退回一步從新選擇,這種走不通就退回再走的技術爲回溯法,而知足回溯條件的某個狀態的點稱爲 「回溯點」。許多複雜的,規模較大的問題均可以使用回溯法,有「通用解題方法」的美稱。回溯法有「通用的解題法」之稱,也叫試探法,它是一種系統的搜索問題的解的方法。簡單來講,回溯法採用試錯的方法解決問題,一旦發現當前步驟失敗,回溯方法就返回上一個步驟,選擇另外一種方案繼續試錯。
從一條路往前走,能進則進,不能進則退回來,換一條路再試。
1,定義一個解空間(子集樹,排序樹二選一)
2,利用適用於搜索的方法組織解空間
3,利用深度優先法搜索解空間
4,利用剪枝函數避免移動到不可能產生解的子空間
回溯算法針對的大多數問題有如下特色:問題的答案有多個元素(可向下成走迷宮是有多個決定),答案須要一些約束(好比數獨),尋找答案的方式在每個步驟相同。回溯算法逐步構建答案,並在肯定候選元素不知足約束後馬上放棄候選元素(一旦碰牆就返回),直到找到答案的全部元素。
問題描述:你玩過報紙上那種查找單詞的遊戲嗎?就是那種在一堆字母中橫向或豎向找出單詞的遊戲。小明在玩一個和那個很像的遊戲,只不過如今不只能夠上下左右鏈接字母,還能夠拐彎。如圖所示,輸入world,就會輸出「找到了」。
問題描述:小米最近有四本想讀的書:《紅色的北京》,《黃色的巴黎》,《藍色的夏威夷》,《綠色的哈薩里》,若是小明每次只能從圖書館借一本書,他一共有多少種借書的順序呢?
回溯法是一種經過探索全部可能的候選解來找出所欲的解的算法。若是候選解被確認,不是一個解的話(或者至少不是最後一個解),回溯算法會經過在上一步進行一些變換排期該解。即回溯而且再次嘗試。
這裏有一個回溯函數,使用第一個整數的索引做爲參數 backtrack(first)。
1,若是第一個整數有索引 n,意味着當前排列已完成。
2,遍歷索引 first 到索引 n-1 的全部整數 ,則:
代碼以下:
class Solution: def permute(self, nums): """ :type nums: List[int] :rtype: List[List[int]] """ def backtrack(first = 0): # if all integers are used up if first == n: output.append(nums[:]) for i in range(first, n): # place i-th integer first # in the current permutation nums[first], nums[i] = nums[i], nums[first] # use next integers to complete the permutations backtrack(first + 1) # backtrack nums[first], nums[i] = nums[i], nums[first] n = len(nums) output = [] backtrack() return output
便於理解的代碼以下:
class solution: def solvepermutation(self, array): self.helper(array, []) def helper(self, array, solution): if len(array) == 0: print(solution) return for i in range(len(array)): newarray = array[:i] + array[i + 1:] # 刪除書本 newsolution = solution + [array[i]] # 加入新書 self.helper(newarray, newsolution) # 尋找剩餘對象的排列組合 solution().solvepermutation(["紅", "黃", "藍", "綠"])
方法二:走捷徑(直接使用Python的庫函數,迭代函數itertools)
li = ['A', 'B', 'C', 'D'] def solutoin(li): import itertools res = list(itertools.permutations(li)) return len(res)
問題描述:小明想上兩門選修課,他有四種選擇:A微積分,B音樂,C烹飪,D設計,小明一共有多少種不一樣的選課組合?
固然第一個方法就是走捷徑!,直接使用python的庫函數itertools進行迭代:
li = ['A', 'B', 'C', 'D'] def solutoin(li): import itertools res = list(itertools.permutations(li, 2)) return len(res)
下面開始回溯法的學習。
class solution(): def solvecombination(self, array, n): self.helper(array, n, []) def helper(self, array, n, solution): if len(solution) == n: print(solution) return for i in range(len(array)): newarray = array[i + 1:] # 建立新的課程列表,更新列表,即選過的課程不能再選 newsolution = solution + [array[i]] # 將科目加入新的列表組合 self.helper(newarray, n, newsolution) solution().solvecombination(["A", "B", "C", "D"], 2)
問題描述:保安負責人小安面臨一個難題,他須要在一個8x8千米的區域裏修建8個保安站點,並確保每一行、每一列和每一條斜線上都只有一個保安站點。苦惱的小安試着嘗試佈置了不少遍,但每一次都不符合要求。小安求助程序員小明,沒過多久小明就把好幾個佈置方案(實際上,這個問題有92種答案)發給了小安,其中包括下面執行結果截圖,試問小明是怎麼作到的。
這是全部的算法學完後的綜合做業,固然這也是算法學習的一個總結。固然下面的問題我都有涉及,這裏不作一一解答。
1. 實現如下算法而且編寫解題報告,解題報告中須要給出題目說明、本身對 題目的理解、解題思路、對算法的說明和理解、以及算法複雜度分析等內容 2. 實現冒泡排序、插入排序、快速排序和歸併排序 3. 以儘量多的方法解決2-sum問題並分析其時間複雜度:給定一個列表和 一個整數,從列表中找到兩個數,使得兩數之和等於給定的數,返回兩個數 的下標。題目保證有且只有一組解 4. 封裝一個雙鏈表類,並實現雙鏈表的建立、查找、插入和刪除 5. 使用至少一種算法解決迷宮尋路問題 6. 使用動態規劃算法實現最長公共子序列問題
參考分治與動態規劃參考文獻:https://blog.csdn.net/weixin_41250910/article/details/94502136
https://blog.csdn.net/weixin_43482259/article/details/97996658
原文出處:https://www.cnblogs.com/wj-1314/p/11724493.html