遞歸和動態規劃

2021-02-20 腦洞前端
動態規劃可以理解為是查表的遞歸。那麼什麼是遞歸?
遞歸

定義:遞歸算法是一種直接或者間接調用自身函數或者方法的算法。

算法中使用遞歸可以很簡單地完成一些用循環實現的功能,比如二叉樹的左中右序遍歷。遞歸在算法中有非常廣泛的使用,包括現在日趨流行的函數式編程。

純粹的函數式編程中沒有循環,只有遞歸。

接下來我們來講解一下遞歸。通俗來說,遞歸算法的實質是把問題分解成規模縮小的同類問題的子問題,然後遞歸調用方法來表示問題的解

遞歸的三個要素

一個問題的解可以分解為幾個子問題的解

子問題的求解思路除了規模之外,沒有任何區別

有遞歸終止條件

我這裡列舉了幾道算法題目,這幾道算法題目都可以用遞歸輕鬆寫出來:

動態規劃

如果說遞歸是從問題的結果倒推,直到問題的規模縮小到尋常。那麼動態規劃就是從尋常入手, 逐步擴大規模到最優子結構。 這句話需要一定的時間來消化,如果不理解,可以過一段時間再來看。

遞歸的解決問題非常符合人的直覺,代碼寫起來比較簡單。但是我們通過分析(可以嘗試畫一個遞歸樹),可以看出遞歸在縮小問題規模的同時可能會重複計算。 中 我通過遞歸的方式來解決這個問題,同時內部維護了一個緩存來存儲計算過的運算,那麼我們可以減少很多運算。這其實和動態規劃有著異曲同工的地方。

我們結合求和問題來講解一下, 題目是給定一個數組,求出數組中所有項的和,要求使用遞歸實現。

代碼:

function sum(nums) {
if (nums.length === 0) return 0;
if (nums.length === 1) return nums[0];

return nums[0] + sum(nums.slice(1));
}

我們用遞歸樹來直觀地看一下。

這種做法本身沒有問題,但是每次執行一個函數都有一定的開銷,拿 JS 引擎執行 JS 來說,每次函數執行都會進行入棧操作,並進行預處理和執行過程,所以對於內存來說是一個挑戰。很容易造成爆棧。

瀏覽器中的 JS 引擎對於代碼執行棧的長度是有限制的,超過會爆棧,拋出異常。

我們再舉一個更加明顯的例子,問題描述:

一個人爬樓梯,每次只能爬 1 個或 2 個臺階,假設有 n 個臺階,那麼這個人有多少種不同的爬樓梯方法?

代碼:

function climbStairs(n) {
if (n === 1) return 1;
if (n === 2) return 2;
return climbStairs(n - 1) + climbStairs(n - 2);
}

這道題和 fibnacci 數列一模一樣,我們繼續用一個遞歸樹來直觀感受一下:

可以看出這裡面有很多重複計算,我們可以使用一個 hashtable 去緩存中間計算結果,從而省去不必要的計算。那麼動態規劃是怎麼解決這個問題呢?答案就是「查表」。

剛才我們說了遞歸是從問題的結果倒推,直到問題的規模縮小到尋常。動態規劃是從尋常入手, 逐步擴大規模到最優子結構。

從剛才的兩個例子,我想大家可能對前半句話有了一定的理解,我們接下來講解下後半句。

如果爬樓梯的問題,使用動態規劃,代碼是這樣的:

function climbStairs(n) {
if (n === 1) return 1;
if (n === 2) return 2;

let a = 1;
let b = 2;
let temp;

for (let i = 3; i <= n; i++) {
temp = a + b;
a = b;
b = temp;
}

return temp;
}

動態規劃的查表過程如果畫成圖,就是這樣的:

虛線代表的是查表過程

這道題目是動態規劃中最簡單的問題了,因為設計到單個因素的變化,如果涉及到多個因素,就比較複雜了,比如著名的背包問題,挖金礦問題等。

對於單個因素的,我們最多只需要一個一維數組即可,對於如背包問題我們需要二維數組等更高緯度。

爬樓梯我們並沒有使用一維數組,而是藉助兩個變量來實現的,空間複雜度是 O(1).之所以能這麼做,是因為爬樓梯問題的狀態轉移方程只和前兩個有關,因此只需要存儲這兩個即可。動態規劃問題有時候有很多這種討巧的方式,但並不是所有的

動態規劃的兩個要素

狀態轉移方程

臨界條件

在上面講解的爬樓梯問題中

f(1) 與 f(2) 就是【邊界】
f(n) = f(n-1) + f(n-2) 就是【狀態轉移公式】

動態規劃為什麼要畫表格

動態規劃問題要畫表格,但是有的人不知道為什麼要畫,就覺得這個是必然的,必要要畫表格才是動態規劃。

其實動態規劃本質上是將大問題轉化為小問題,然後大問題的解是和小問題有關聯的,換句話說大問題可以由小問題進行計算得到。

這一點是和遞歸一樣的, 但是動態規劃是一種類似查表的方法來縮短時間複雜度和空間複雜度。

畫表格的目的就是去不斷推導,完成狀態轉移, 表格中的每一個cell都是一個小問題, 我們填表的過程其實就是在解決問題的過程,我們先解決規模為尋常的情況,然後根據這個結果逐步推導,通常情況下,表格的右下角是問題的最大的規模,也就是我們想要求解的規模。

比如我們用動態規劃解決背包問題, 其實就是在不斷根據之前的小問題A[i - 1][j] A[i -1][w - wj]來詢問:

我是應該選擇它

還是不選擇它

至於判斷的標準很簡單,就是價值最大,因此我們要做的就是對於選擇和不選擇兩種情況分別求價值,然後取最大,最後更新cell即可。

相關問題

太多了,沒有逐一列舉

總結

本篇文章總結了算法中比較常用的兩個方法 - 遞歸和動態規劃。

如果你只能藉助一句話,那麼請記住:遞歸是從問題的結果倒推,直到問題的規模縮小到尋常。動態規劃是從尋常入手, 逐步擴大規模到最優子結構。

相關焦點

  • 關於動態規劃,你想知道的都在這裡了
    動態規划動態規劃是一種解決最優化、搜索和計數問題的通用技術,這些問題都可以被分解為多個子問題。這就引出了遞歸和動態規劃之間的關係。遞歸和動態規劃從概念上講,動態規劃涉及到遞歸問題。我們希望通過同一個問題的較小實例來解決原始問題,而遞歸是在代碼中實現這一點的最佳選擇。與純遞歸函數的不同之處在於,我們將用空間來換取時間:我們將存儲各子問題的最優解,進而高效地找到原始問題的最優解。
  • 程式設計師必備的基本算法:遞歸詳解
    遞歸的特點實際上,遞歸有兩個顯著的特徵,終止條件和自身調用:自身調用:原問題可以分解為子問題,子問題和原問題的求解方法是一致的,即都是調用自身的同一個函數。因為節點1和3都是「葉子節點」了,所以就返回啦。這也是遞歸的「遞」的過程~
  • Java動態規划算法策略
    Dynamic Programming是五大常用算法策略之一,簡稱DP,譯作中文是「動態規劃」,可就是這個聽起來高大上的翻譯坑苦了無數人,因為看完這個算法你可能會覺得和動態規劃根本沒太大關係,它對「動態」和「規劃」都沒有太深的體現。
  • 算法思想簡介(分制),貪心(DJS)動態分配(dp)回溯(萬能)
    算法思想簡介(分制(分開在遞歸),貪心(DJS),動態分配(dp,解決多變化條件),回溯(萬能,深度優先))不管是動態規劃,還是回溯都是在可選擇 條件固定時,進行選擇 ,都會用到遞歸調用。不同的是:貪心最好理解,從頭開始找最優結果一直到最後。
  • 詳解:遞歸神經網絡和LSTM網絡那些事兒
    【IT168 編譯】遞歸神經網絡是最先進的順序數據算法之一,在蘋果Siri和Google語音搜索中都使用到的算法。這是因為它是第一個記憶它的輸入的算法,由於內部存儲器,這使得它非常適合涉及順序數據的機器學習問題。它是過去幾年Deep Learning的驚人成就背後的算法之一。在這篇文章中,你將學習遞歸神經網絡如何工作的基本概念,最大的問題是什麼以及如何解決它們。
  • 什麼是遞歸?什麼是迭代?
    這個故事永遠也講不完,因為沒有遞歸結束條件。老師講遞歸時總是說,遞歸很簡單,一個遞歸結束條件,一個自己調用自己。如果遞歸沒有結束條件,那麼就會無限遞歸下去。在編程的時候,沒有遞歸結束條件或者遞歸過深,一般會造成棧溢出。遞歸算法一般用於解決三類問題:(1)數據的定義是按遞歸定義的。(Fibonacci函數)(2)問題解法按遞歸算法實現。
  • 動態規劃課件
    最近給別人補習算法課程時製作的動態規劃課件,分享給大家自學。
  • 算法一看就懂之「 遞歸 」
    (給算法愛好者加星標,修煉編程內功)作者:不止思考 微信公號 / 王奎之前的文章咱們已經聊過了「 數組和鍊表 」、「 堆棧」和
  • C 語言,你真的懂遞歸了嗎?
    要說到遞歸如果不說棧的話,我覺得有點不合適,遞歸特點就是不斷的調用同一個函數,如果這個函數沒有一個遞歸界限,那麼就是死循環了,所以討論遞歸,就必須要討論遞歸的界限,就是限定這個遞歸調用多少次。所以說遞歸次數肯定是有限定的了。遞歸求階乘使用遞歸求階乘是很經典的方法,我們看一下代碼。
  • 學習編程的人,怎麼能不知道什麼叫遞歸?
    話不多說,開始今天的學習:遞歸:不要看這個名字好像挺高大上的樣子,其實理解起來還是蠻容易的。在學習遞歸之前,我們先學習下目錄的遍歷,遞歸的主要使用途徑就需要它。一、目錄的遍歷目錄,自然也就是指我們常說的文件夾了,一個文件夾裡面是可以有很多個子文件夾和子文件的。如果遍歷目錄?
  • 如何優雅地使用javascript遞歸畫一棵結構樹
    本文轉載自【微信公眾號:趣談前端,ID:beautifulFront】經微信公眾號授權轉載,如需轉載與原文作者聯繫遞歸和尾遞歸簡單的說,遞歸就是函數自己調用自己,它作為一種算法在程序設計語言中廣泛應用。
  • 動態規劃:整數拆分,你要怎麼拆?
    動態規划動規五部曲,分析如下:1.確定dp數組(dp table)以及下標的含義dp[i]:分拆數字i,可以得到的最大乘積為dp[i]。dp[i]的定義講貫徹整個解題過程,下面哪一步想不懂了,就想想dp[i]究竟表示的是啥!
  • 二叉樹的中序非遞歸遍歷
    3 代碼此處給出二叉樹中序非遞歸遍歷的Java樣例代碼:4 總結本文總結了二叉樹中序非遞歸遍歷的算法,其中使用了棧做輔助數據結構,並詳細的繪圖並描述了遍歷過程,對於使用的輔助數據結構棧的操作與中序遍歷的特點進行結合達到效果
  • Java實現輸出九九乘法表—for循環和遞歸算法
    ;10;i++) { for(int j=1;j<=i;j++) { System.out.printf("%d*%d=%d\t",j,i,j*i); } System.out.println(""); }//https://blog.zeruns.tech }}遞歸
  • 新冠之年的「內捲風」——談內卷的失速、遞歸與巴洛克
    但是,尼採和博爾赫斯同時都談到了巴洛克的另一面,尼採認為巴洛克雖然是「一種過於豐富的形式衝動」,但卻一樣使人激動和受益。(參見:《人性的,太人性的》,楊恆達譯,人民大學出版社,2005年,第144條)博爾赫斯在《世界歷史醜聞》的序裡把巴洛克定義為「那種刻意耗盡(或者試圖耗盡)其自身可能性的風格」;同時,他還指出了巴洛克的「智力屬性」,也即巴洛克更關注對藝術的形式的創造和技術的革新。
  • 二叉樹的所有遍歷非遞歸實現
    一般分為前序遍歷(根 左 右),中序遍歷(左 根 右),後序遍歷(左 右 根)二:前序遍歷(根、左、右)前序遍歷是指按照根左右的順序依次遍歷,使用非遞歸遍歷,一般會用到棧,利用先進後出的特性來達到訪問二叉樹節點目的。
  • Processing教程 - 類和動態數組
    這一期來講一講Processing中的總在一起搞事情的一對CP:class 類 和 Arraylist 動態數組圖文比較簡略
  • 杭州探索形成動態維護的 國土空間總體規劃「一張圖」
    日前,杭州市國土空間規劃「一張圖」實施監督信息系統總體設計方案專家評審會召開,記者從市規劃資源局了解到:與杭州國土空間總體規劃編制同步,全市國土空間規劃「一張圖」實施監督信息系統正在抓緊建設中,該系統將支撐國土空間規劃編制、審批、實施和監測評估預警全過程,為建立健全國土空間規划動態監測評估預警和實施監管機制提供信息化支撐;與此同時,全市規劃和自然資源數據將接入「城市大腦」,構建城市大腦規劃資源駕駛艙
  • 如何實現後臺管理系統的權限路由和權限菜單
    希望通過這3篇文章的復盤和實戰, 可以讓大家開發企業應用的時候更加遊刃有餘.本文主要涉及的技術點如下:如何使用遞歸算法動態渲染不定層級的菜單如何基於權限來控制菜單展現基於nodejs的權限服務設計正文動態菜單和權限路由是後臺管理系統設計中必不可少的環節, 作為複雜後臺管理系統來說, 導航菜單往往不是簡單的一級菜單, 往往都會有3級,4級菜單, 如下: 所以我們首要解決的問題就是面對未知層級菜單時的前端解決方案.