動態規劃專題 1 簡單線性DP

2022-09-18 22:36:14 字數 3407 閱讀 1555

本專題文章建立在本人多年寫動態規劃**的經驗上,用以自己回顧總結,也幫助朋友初步理解,部分理解可能和教科書有所出入,要參加演算法考試的同學請以教科書為準。

在現實生活中,有一類活動的過程,由於它的特殊性,可將過程分成若干個互相聯絡的階段,在它的每一階段都需要作出決策,從而使整個過程達到最好的活動效果。因此各個階段決策的選取不能任意確定,它依賴於當前面臨的狀態,又影響以後的發展。當各個階段決策確定後,就組成乙個決策序列,因而也就確定了整個過程的一條活動路線.這種把乙個問題看作是乙個前後關聯具有鏈狀結構的多階段過程就稱為多階段決策過程,這種問題稱為多階段決策問題。在多階段決策問題中,各個階段採取的決策,一般來說是與時間有關的,決策依賴於當前狀態,又隨即引起狀態的轉移,乙個決策序列就是在變化的狀態中產生出來的,故有「動態」的含義,稱這種解決多階段決策最優化的過程為動態規劃方法[1]。

雖然這段話非常抽象,但它清晰地解釋了動態規劃(dynamic programming,下文簡稱dp)為什麼「動態」。包括我在內的部分人認為dp不是一種演算法而是一種方法或者說思想。我們通常認為演算法是程式設計的方**,那麼dp這類的思想可以說是演算法的方**,比如著名的floyd多源最短路演算法的核心思想就是dp。

動態規劃首先是乙個規劃:給出乙個規劃問題,讓求最優解或最優決策,比如說乙個老闆有多條生產線,如何分配產能得到最多的剩餘價值,當然這個問題可以用高中學過的線性規劃來解決。而我們為什麼強調它是動態的,是因為它在不同步驟做決策時狀態變了,且通常做的決策不能只顧當前,還要瞻前(dp的前效性)顧後(dp的後效性)。什麼是狀態?可以這樣簡單的理解,工廠各條生產線對應商品的市場價就是狀態,我們做線性規劃的前提是市場價不發生改變,可以規劃出當下最優解。但如果市場價每天都發生變化,也就是說狀態是在變的,那麼這個規劃問題就可以說是個動態規劃問題。

如果看到這你感覺雲裡霧裡的,沒關係,可以在學了幾種dp,寫個十幾道題之後再回頭看概念。

這裡把各個術語的定義[2]丟一下,可以先不看。

具有線性狀態劃分的動歸稱為線性dp,通常其狀態轉移也是線性的。

原題鏈結

這道題非常有來頭,是2023年ioi(international olympiad in informatics)的題,當時dp還沒怎麼普及。dp普及之後,這道題在洛谷的難度被標為普及-,也就是還沒達到初中生競賽的難度。

如果不用dp,這道題該怎麼做?搜尋,把所有可以走的路列舉出來,找到所有路中的最優解。數塔有n層的,總共有幾條路呢?有 \(2^\) 條。也就是說演算法的複雜度是 \(o(2^n)\) 的。這裡放上dfs的**:

//luogu p1216 coded by jiayou

#includeusing namespace std;

int n, map[105][105];

int dfs(int floor,int pos)

int main()

這裡 \(dfs(i,j)\) 的返回值的意義是什麼呢?是走到第i層第j個的數字的時候,再往後走最多能取到幾。所以全域性的答案就是 \(dfs(1,1)\):已經走到第乙個點,再往後走能娶到的最大值。問讀者們乙個問題:在我們深度優先搜尋的時候,是不是會多次經過同乙個點?會,因為深搜的順序是按著路徑走的,而這 \(2^\) 條路徑互相之間是存在共同經過的點的。所以形如 \(dfs(4,3)\) 這樣的函式會被呼叫多次。那麼再追問讀者一下:前後兩次呼叫 \(dfs(4,3)\) 獲得的返回值相同嗎?是相同的,因為 \(dfs(4,3)\) 表示走到這個點後再往後走的最大值,這個值是定值。那麼我們是不是可以開乙個陣列 \(memory[105][105]\),第一次 \(dfs(4,3)\) 的時候,讓 \(memory[4][3]=dfs(4,3)\)。之後如果還需要呼叫 \(dfs(4,3)\),發現之前已經計算過一次了,這次就不再重複計算,直接獲取 \(memory[4][3]\) 的值。其實每個 \(dfs(i,j)\) 都是乙個子問題,它們在搜尋中可能被提出多次,這個叫做子問題重疊。對於重疊的子問題,記錄下答案,下次出現時不再去計算它,就是記憶化搜尋。記憶化搜尋和dp本質上是相同的,任何使用dp方法的**都可以改寫成記憶化搜尋,反之亦然。但使用dp可以簡潔地使用**表示出遞推關係,易於優化,且避開了遞迴,程式執行效率更高。那麼現在我們來改寫這個p1216記憶化深搜。

既然演算法變了,那我們把 \(memory\) 陣列改名成 \(dp\) 陣列(它們本質是相同的)。既 \(dp[i][j]\) 表示從 \((i,j)\) 這個點出發往下走能取到的最大值。問:\(dp[i][j]\) 的值由誰決定?由 \(dp[i+1][j]\) 和 \(dp[i+1][j+1]\) 決定,因為 \(dp[i][j]=max(dp[i+1][j],dp[i+1][j+1])+map[i][j]\) 從 \((i,j)\) 出發能獲得的最大值,自然就下一步能獲得的最大值加上 \(map[i][j]\) ,而下一步的最大值則是 \(max(dp[i+1][j],dp[i+1][j+1])\)。所以剛剛那個公式就是這麼來的。這個公式揭示了記憶化搜尋更新 \(dp\) 陣列的順序,也就是說,現在,我們不需要使用搜尋演算法,也可以算出整個 \(dp\)陣列了。需要計算 \(dp[i][j]\), 就需要先計算 \(dp[i+1][j]\) 和 \(dp[i+1][j+1]\)。而數塔最底層對應dp值是非常好知道的,因為它不能再往下走了: \(dp[i][j] = map[i][j], when\space i=n\)。那麼我們就可以先把最底層的dp算出來,然後算倒數第二層,最後算出第一層。第一層只有乙個點,那個點的dp值就是答案。

**:

//luogu p1216 coded by jiayou

#includeusing namespace std;

int n, map[1005][1005];

int dp[1005][1005];

int main()

\(dp[i][j]=max(dp[i+1][j],dp[i+1][j+1])+map[i][j]\) 這個我們叫做狀態轉移方程。這裡的狀態就是 \((i,j)\),它表示走到第i層第j個點,這是既定的條件,我們的決策也是在這個狀態下定製出來的。通俗的理解,狀態就是乙個數的集合,是你記憶化搜尋的引數列表,是你的子問題的條件。狀態轉移就是通過已知解的狀態計算出未知解的狀態,轉移方法就是狀態轉移方程。其實這道題還可以這麼寫:\(dp[i][j]\) 表示走到 \((i,j)\),已經取的值中能娶到的最大值,那麼 \(dp[i][j]=max(dp[i-1][j-1],dp[i-1][j])+map[i][j]\)。最終答案就是 \(max(dp[n][j])\),在最下層中挑出解最大的乙個,也就是反過來狀態轉移,這裡不再贅述。

[4] oi-wiki: 記憶化搜尋

動態規劃(dp)專題

航線設定 問題描述 在美麗的萊茵河畔,每邊都分布著n個城市,兩邊的城市都是唯一對應的友好城市,現需要在友好城市間開通航線以加強往來,但因為萊茵河常年大霧,如果開設的航線發生交叉就有可能出現碰船的現象。現在要求盡可能多地開通航線並且使航線不能相交。輸入有若干組測試資料,每組測試資料的第一行是乙個整數n...

簡單線性DP總結

首先 動態規劃適用的條件是該問題有最優子結構 無後效性 通過每個最優子結構狀態的遞推可以推出整體的最優解 對這些基本概念的理解既要理性也要一點點感性 dp問題解決順序是 1 確定狀態 通過最後一步將問題轉化為規模更小的子問題 2 轉移方程 3 初始條件與邊界情況 4 計算順序 其中前兩步是十分重要的...

( 動態規劃專題 ) 樹形dp

動態規劃專題 樹形dp 直接看例題 p2015 二叉蘋果樹 有一棵蘋果樹,如果樹枝有分叉,一定是分2叉 就是說沒有只有1個兒子的結點 這棵樹共有n個結點 葉子點或者樹枝分叉點 編號為1 n,樹根編號一定是1。我們用一根樹枝兩端連線的結點的編號來描述一根樹枝的位置。下面是一顆有4個樹枝的樹 2 5 3...