天天看點

動态規劃入門

作者:閃念基因

1.前言

對于軟體工程師的同學來講,肯定都熟悉《算法與資料結構》的領域,圖靈獎獲得者的Pascal之父—Nicklaus Wirth提出的著名公式“算法+資料結構=程式”,可見算法的重要性。

在攀登算法這個領域山峰的時候,我們從排序、搜尋、貪心等算法一路披荊斬棘,最終來到山頂,即将發出“一覽衆山小”的感慨時,突然發現還有一座更高的絕壁——動态規劃(俗稱DP)擋在我們的前面。

同時我們在刷leetcode、面試大廠的時候,經常會遇到需要使用動态規劃才能解決的題目和場景;并且在我們的日常複雜業務開發中,也可以見到動态規劃的應用場景。

動态規劃問題非常非常經典,也很有技巧性,但絕非是一個遙不可及的概念。

今天跟大家一起來學習動态規劃的“套路”,主要從如下主題進行介紹:

1、神秘的動态規劃,主要是引出動态規劃這個概念,也就是大家平常看到的學術性的角度,如果有了一定的認識,這裡大家可以跳過。2、走進動态規劃,這個章節主要講解我自己了解的動态規劃是什麼,以及在學習過程中提煉的核心思想,以及推導過程。3、動态規劃實戰和應用,用一個例子來推導出實作的全過程,如何寫出暴力遞歸版本,記憶化搜尋版本,和最終的狀态轉移方程版本;最後還有動态規劃在售後業務中的應用介紹。

動态規劃入門

文章如果有不正确的地方,歡迎大家指出哈,感謝感謝!

2.神秘的動态規劃

2.1 什麼是動态規劃

動态規劃(英語:Dynamic programming,簡稱 DP),是一種在數學、管理科學、計算機科學、經濟學和生物資訊學中使用的,通過把原問題分解為相對簡單的子問題的方式求解複雜問題的方法。動态規劃常常适用于有重疊子問題和最優子結構性質的問題。

dynamic programming is a method for solving a complex problem by breaking it down into a collection of simpler subproblems.

以上定義來自維基百科,看定義感覺還是有點抽象。簡單來說,動态規劃其實就是,給定一個問題,我們把它拆成一個個子問題,直到子問題可以直接解決。然後呢,把子問題答案儲存起來,以減少重複計算。再根據子問題答案反推,得出原問題解的一種方法。

一般這些子問題很相似,可以通過函數關系式遞推出來。然後呢,動态規劃就緻力于解決每個子問題一次,減少重複計算,比如斐波那契數列就可以看做入門級的經典動态規劃問題。

2.2 動态規劃的應用場景

2.2.1 最優解問題

這樣的問題一般都會讓你求最大子數組、求最長遞增子數組、求最長遞增子序列或求最長公共子串、子序列等等;還有著名的背包問題,以及優惠券最合理使用問題。

2.2.2 求可行性

讓你判斷是否存在一條總和為 x 的路徑(如果找到了,就是 True;如果找不到,自然就是 False),或者讓你判斷能否找到一條符合某種條件的路徑,那麼這類問題都可以歸納為求可行性問題,并且可以使用動态規劃來解。

2.2.3 求總數

除了求最值與可行性之外,求方案總數也是比較常見的一類動态規劃問題。比如給定一個資料結構和限定條件,讓你計算出一個方案的所有可能的路徑,那麼這種問題就屬于求方案總數的問題。

3.走進動态規劃

這個章節主要講解我自己了解的動态規劃是什麼,以及在學習過程中提煉的核心思想,以及推導過程。

3.1 動态規劃核心思想

動态規劃的核心思想是:動态規劃是一種将問題執行個體分解為更小的、相似的子問題,并存儲子問題的解而避免計算重複的子問題,以解決最優化問題的算法政策。 動态規劃的實質是分治思想和解決備援。

如果使用通俗易懂的一句話,那麼應該是:拆分子問題,記住過往,減少重複計算。我們舉一個簡單日常的例子:

A :21+12+3+64+45+6+70+37 , 這個等式的值是多少? B :嗯,花了3分鐘算出來答案是 258A : 在上面等式的右邊寫上 "+2" 呢,此時等式的值為多少? B : 很快得出答案 "260" A : 你怎麼這麼快就知道答案了 B : 隻要在258的基礎上加2就行了啊

3.2 動态規劃解題套路

下面我們分幾個步驟介紹動态規劃的解題套路,分别是:

1)嘗試設計暴力遞歸模型:重點!這是基礎2)分析有沒有重複解:(列出調用過程,可以隻列出前幾層)3)用記憶化搜尋法 :空間換時間,減少重複計算4)狀态轉移方程:精細化分析DP表結構和遞歸模型,實作動态規劃的狀态轉移方程

3.2.1 嘗試暴力遞歸模型

這是實作動态規劃算法的第一步,也是最基礎最重要的前置條件;我們在這個步驟中,需要根據實際場景和問題去嘗試設計遞歸模型,首先我們需要把問題轉化為規模縮小了的同類問題的子問題,并且還需要确定明确的不需要繼續進行遞歸的條件,我們俗稱為 Base Case,不能讓遞歸進入死循環哈。

暴力遞歸模型有一個緻命的缺陷,即 僅得到了子問題的結果,但不會記錄每個子過程的解;這樣就導緻産生重複計算,進而提高了算法的時間複雜度;比如在Leetcode上面的一些題目,如果僅僅使用暴力遞歸實作算法,送出的時候會報“超出時間限制”。

動态規劃入門

3.2.2 分析重複解

也叫「重複子問題」,從「斐波拉契數列」求解的問題中,我們知道,如果遞歸地去這個問題,會遇到很多「重複子問題」。

斐波那契數列規定第一項是1,第二項是1,以後的每一項F(n) = F(n-1) + F(n-2)。我們展開求解第7項的過程如下:

動态規劃入門

我們隻需要将求第7項的值在白闆展開後會發現,整個遞歸過程會有很多重複的過程,這時候我們就可以使用動态規劃進行優化求解。

3.2.3 記憶化搜尋法

對于重複出現的子問題,隻在第一次遇到時加以求解,并把答案儲存起來,讓以後再遇到時直接引用,不必重新求解。

動态規劃法所針對的問題有一個顯著的特征,即它所對應的子問題樹中的子問題呈現大量的重複。

比如上面的斐波那契數列,在我們求解過一次F(5)後,将其值存起來,下次再遇到需要求F(5)時直接取就可以了,減少了重複求解的過程。同理,對于每一個求解過的值都這樣做,這就是動态規劃。

并且根據算法中可變參數的個數,以此來分析并定義幾維的緩存DP數組。

3.2.4 狀态轉移方程

我們不妨把狀态轉移方程了解為遞歸關系,就是如何從已知求得未知的表達式。

最終的方程式完全脫離了原題本意,是根據暴力遞歸模型和題目中給出的已知條件,比如:

Base Case是什麼?

可變參數範圍多大?

普遍位置如何依賴?

最終求的位置是哪個?

其他...

結合上面的已知條件提煉出一個算法(就是狀态轉移方程),對緩存DP數組進行填充指派,最終傳回DP數組中的某個位置即為我們最終求解的值。

是以,設計動态規劃的步驟和解題套路流程如下圖所示:

動态規劃入門

4.動态規劃實戰和應用

本章節我們會通過1個動态規劃的例子 和1個發票系統中實際應用動态規劃的場景來介紹和講解。

我們實戰動态規劃算法的過程,會按照上面講述的動态規劃解題套路中介紹的流程展開,分别是:

1)嘗試設計暴力遞歸模型:重點!這是基礎2)分析有沒有重複解:(列出調用過程,可以隻列出前幾層)3)用記憶化搜尋法 :空間換時間,減少重複計算4)狀态轉移方程:精細化分析DP表結構和遞歸模型,實作動态規劃的狀态轉移方程

4.1 機器人走位問題

題目如下:

給定1~N個位置,有一個機器人在其中行走,每次隻能往左、右走一步。

機器人從cur位置出發最終到達目标位置aim, 所走過的總步數必須等于restSteps,計算滿足條件的走法有多少種?

比如:給定4個位置,機器人從2号位置發出,走過6步,最終到達4号位置, 一共有多少種走法?

注意:如果目前機器人走到了最左邊1的位置,則隻能走到2的位置;如果走到了N的位置,則隻能走到N-1位置。

第一步:嘗試設計暴力遞歸模型

這個題目是一個很經典的動态規劃入門題,有些同學見到後可能有點蒙圈,不知道怎麼解決。其實可以作如下分析:

退出遞歸的base case是什麼:當剩餘步數(restSteps)等于0,即 restSteps == 0

如何定義為一種滿足條件的走法:當剩餘步數(restSteps)等于0 且 機器人剛好停在aim位置,即 restSteps == 0 && cur==aim

遞歸場景:

如果目前機器人走到了最左邊1的位置,則隻能走到2的位置,

如果走到了N的位置,則隻能走到N-1位置

否則,機器人可以往左或者 往右走

這個時候,我們可以得出最終的遞歸模型如下:

動态規劃入門

據上分析,我們可以寫出暴力遞歸版本的代碼如下:

動态規劃入門

到目前為止,我們已經設計出了暴力遞歸的代碼,完成了動态規劃解題套路的第一步;我們接下來看看是否有重複解。

第二步:分析是否存在重複解

我們給定如下資料做分析:

int cur = 2; //開始位置

int restSteps = 4; //剩餘步數

int aim = 4; //目标位置

int N =4; //位置總數

一共有4個位置,需要從2号位置出發,機器人走過4步,最終能夠停在4号位置的所有走法。(注意!這裡為了友善我們可以在紙上展開所有的走法,我們特意講走步數設定為4)

第一種走法:機器人從2 号位置出發,走到3号位置(剩餘3步),再走到4号位置(剩餘2步);再走到3号位置(剩餘1步);最終走到4号位置(剩餘0步)

第二種走法:機器人從2 号位置出發,走到3号位置(剩餘3步),再走到2号位置(剩餘2步);再走到3号位置(剩餘1步);最終走到4号位置(剩餘0步)

第三種走法:機器人從2 号位置出發,走到1号位置(剩餘3步),再走到2号位置(剩餘2步);再走到3号位置(剩餘1步);最終走到4号位置(剩餘0步)

移動過程詳見如下圖:

動态規劃入門

分析第一種走法和第二種走法的第一步(2→3)的這個子過程,我們發現,都是目前位置(cur)從2走到3,剩餘步數(restSteps)還剩餘3步,這是一個重複計算的過程。

分析第二種走法和第三種走法的第三步(2→3)的這個子過程,我們同樣可以發,都是目前位置(cur)從2走到3,剩餘步數(restSteps)還剩餘1步,同樣,這也是一個重複計算的過程。

如果機器人可以走的步數越多,能統計出來的走法就越多;同時産生的重複計算的子過程也越多。

是以,我們展開前面3種走法,就知道了的确存在重複解;是以,我們可以進一步優化。

第三步:記憶化搜尋法

首先,我們要分析整個遞歸調用過程中的可變參數有幾個,根據上面的遞歸代碼版本,我們得知函數process一共有4個入參,固定不變的入參有2個,分别是數組長度N,目标位置aim;可變的入參有2個,分别是機器人所在的目前位置cur,剩餘的步數restSteps。

這裡我們隻關注可變參數,由此可知,我們需要準備一個2維數組來囊括這兩個參數所有的變化範圍,而cur的範圍最大是N。即:dp[N+1][restSteps+1],這裡+1 是為了友善了解能夠包含全部範圍和計算。

dp數組的第一維(行)表示機器人可走的所有位置,即範圍從1~N。dp數組的第二位(列)表示機器人還剩餘的步數,即範圍從0~restSteps。

動态規劃入門

PS:感興趣的同學可以列印這兩種算法的運作時間,随着restSteps的值越大,算法二的優勢越明顯,我這裡則不再示範。

第四步:狀态轉移方程

狀态轉移方程可以了解為遞歸關系,就是如何從已知求得未知的表達式。

那麼,我們需要分析上面暴力遞歸版本的遞歸關系,并能夠進行如下推導:

1)遞歸調用模型中,根據base case填充dp數組

停止遞歸調用的條件(base case)為:剩餘步數(restSteps)等于0時,且分兩種情況:

  • 當機器人所在的目前位置(cur) 剛好停在目标位置(aim)時,即為1種走法;
  • 否則,即為0種走法。

是以,當剩餘步數(restSteps)等于0時,即表示dp數組的第0列;再根據機器人最終停留的位置等于目标位置,将dp[aim][0]指派為1,其他行的第一列都是0,這樣我們就填充好了dp數組的第一列。

2)根據特殊場景填充dp數組

我們看題目中給出了哪些特殊場景,分别透露出了哪些資訊呢?

  • 機器人走到最左邊1号位置時,隻能往右走到2号位置,相應的剩餘步數減去1;
  • 機器人走到最右邊N号位置時,隻能往左走到N-1位置,相應的剩餘步數減去1上面的定義得知,dp數組的第一維表示位置範圍,是以:

填充dp數組第一行的邏輯為:dp[1][restSteps] 依賴于 dp[2][restSteps - 1]

填充dp數組最後一行的邏輯為:dp[N][restSteps] 依賴于dp[N-1][restSteps - 1]

3)普遍位置依賴填充dp數組

這是狀态轉移方程中最核心的部分,因為普遍性位置的依賴邏輯關系是比較隐秘的,也占了dp數組絕大多數位置空間。

我們分析的方式還是從暴力遞歸模型中找蛛絲馬迹。

因為一個機器人如果不在最左邊也不在最右邊,那麼他下一步移動的位置即可以往左也可以往右,依賴關系如下:

dp[cur][restSteps] = dp[cur+1][restSteps -1] + dp[cur - 1][restSteps -1]

即依賴于上一行左上角的值 加上 下一行左下角的值。

4)最終傳回的位置

如果我們從已知條件中找到了未知的狀态轉移方程表達式,并填充好了dp數組,那我們就需要知道最終求的解時dp中的哪個位置。

我們還是需要從暴力遞歸模型中尋找。

在調用process遞歸方法時,最開始從的入參如下:

int cur = 2; //開始位置

int restSteps = 4; //剩餘步數

int aim = 4; //目标位置

int N =4; //位置總數

...return process2(cur,restSteps,aim,N,dp);

其中,可變參數為:cur、restSteps 即為我們需要從dp數組中傳回的最終值。

是以,cur等于2,restSteps等于4, 我們需要傳回dp[2][4]。

推導過程如下:

動态規劃入門

最終,基于上面的推導過程,最終狀态轉移方程代碼如下:

動态規劃入門

4.2 發票項目-可開票金額最優比對

這是在我們項目中的一個功能場景,财務人員在開發票的過程中,會需要給消費者開具等額的發票,是以就需要根據使用者消費的金額來比對金額最相近的商品記錄,具體流程如下:

1、從商品線擷取符合要求的商品清單,商品金額随機分布

2、财務人員輸入開票金額

3、系統自動勾選最接近開票金額的商品清單

我們分析這個問題屬于動态規劃算法的範疇,類似于背包問題的更新版本,就是将最大價值最終結果的路徑記錄,進而逆向推導出路徑元素。

具體代碼如下:

動态規劃入門

該動态規劃算法的實作過程和上述解題套路一樣,這裡由于篇幅暫不展開了。

5.結語

動态規劃是一個很具有挑戰性的話題和領域,其中變化高深莫測,同時也是我們解決複雜性問題的霹靂手段;

我們從最開始的嘗試開始,一步一步的往下推導,這就是狀态轉移的過程,想要一步直接寫出最後的動态規劃代碼來是需要不斷的練習和總結的。

寫到這裡,深感惶恐,若大家都有收獲便是善莫大焉!

作者:興盛優選技術社群

出處:https://zhuanlan.zhihu.com/p/618731459