君君演算法課堂-動態規劃基礎及線性動態規劃
動態規劃基礎及線性動態規劃
動態規劃的狀態及記憶化搜尋
動態規劃\((Dynamic Programming,DP)\)就是通過對問題的拆分,
定義問題的狀態和狀態之間的關係。
使得問題能夠通過遞推(或是分治)的方式去解決。
狀態
狀態的本質是某個問題的子問題。
和搜尋演算法類似,只有當確定狀態後,才能找到狀態之間的聯絡。
由於問題形式不一,所以狀態也各種各樣,
對於問題要具體分析,才能找到合適的狀態。
一般情況下,可以通過所求的東西和一些影響結果的量來確定狀態。
狀態的特點
-
無後效性
某階段的狀態一旦確定,
則此後過程的演變 不再受此前各種狀態及決策的影響。
簡單的說,就是 “未來與過去無關”,
當前的狀態是此前歷史的一個完整總結,
此前的歷史只能通過當前的狀態去影響過程未來的演變。
-
最優子結構
子問題的區域性最優將導致整個問題的全域性最優,
即問題具有最優子結構的性質,
也就是說一個問題的最優解只取決於其子問題的最優解,
非最優解對問題的求解沒有影響。
狀態的轉移
狀態的轉移即為狀態之間的聯絡。
通過狀態之間的的聯絡,能夠對問題進行解決。
一般來說,狀態轉移方程可以寫成一個遞推式的形式。
對於同一問題的不同狀態定義來說,狀態的轉移也可能是不同的。
實現
動態規劃的實現就是自底向上列舉每一種狀態,
之後通過狀態轉移方程進行求解。
由於一種狀態至多會被計算一次,
所以動態規劃的複雜度一般與狀態數有關,為多項式級別。
記憶化搜尋
記憶化搜尋 屬於 搜尋的一種剪枝方式,
即通過遞迴的方法對狀態進行求解。
在一般情況下,記憶化搜尋仍採用自頂向下的順序,
但每當求解出一個狀態,就將它的解儲存下來。
當以後再次遇見就這個狀態的時候,便不需要重新求解。
與動態規劃相同,每種狀態只會被計算一次,複雜度一般也被多項式級別。
在某些情況下,動態規劃可以與記憶化搜尋相互替代。
例題:擺花
有 \(n\) 種花,按照順序排成一排 \(m\) 朵,
第 \(i\)
種花最多可以 放 \(a_i\) 個,最少放\(1\)個。要求所有花必須按照種類遞增擺放,且同種花完全一樣.
現在問有多少種不同的擺放方案。
資料範圍 \(n, m \leq 200\)
題解:可以用動態規劃或者記憶化搜尋解決。
狀態為 \(f[i][j]\) 表示只考慮前 \(i\) 種花,一共擺放了 \(j\) 朵的方案數。
轉移只要列舉當前這種花放了幾盆即可。
\[f[i][j]=f[i-1][j-k](1\leq k\leq a_i) \]例題:滑雪
滑雪場是一個 \(n * m\) 的網格圖,其中每個點的權值代表該點的高度。
滑雪的時候只能從相鄰兩格中較高的那格滑到較低的那格。
現在問這個滑雪場中最長可以滑行的距離。
資料範圍 \(n, m \leq 1000\)
題解:定義狀態 \(f[x][y]\) 表示從點 \((x, y)\) 開始所能走的最長路。
如果我們要用動態規劃解決這個問題,需要先將所有點按照高度排序。
但如果我們用記憶化搜尋則不需要。
小結
- 記憶化搜尋相比動態規劃更好理解,程式碼實現更為容易。
- 對於一些狀態順序不明確的題目,用記憶化搜尋更為方 便。
- 記憶化搜尋可以剪去一些無效狀態。
- 記憶化搜尋由於使用遞迴,常數較大。
- 記憶化搜尋優化起來相較動態規劃更加困難。
線性動態規劃
在所有動態規劃中,線性動態規劃最常見,最基礎, 也最重要。
學好線性動態規劃有助於我們更快更好地設計狀態與轉 移方程,
並對動態規劃有更深的理解。
例題:假期
小 \(L\) 的暑假有 \(n\) 天,他想要在暑假過的充實。
每一天,小 \(L\) 可以選擇去游泳、學程式設計或在家休息。
小 \(L\) 不想連續兩天參加相同的活動(休息不算活動)。
現在給出 \(n\) 天中游泳池和機房的開放情況,
問小 \(L\) 在 \(n\) 天中最少休息幾天。
資料範圍 \(n \leq 10^5\)
題解:容易想到可以用每天做的事情以及天數來表示狀態
\(f[i]\)表示第 \(i\) 天在家休息前 \(i\) 天最少休息幾天。
\(g[i]\) 表示第 \(i\) 天去游泳前 \(i\) 天最少休息幾天。
\(h[i]\) 表示第 \(i\) 天去程式設計前 \(i\) 天最少休息幾天。
\(f[i] = min(f[i−1], g[i−1], h[i−1]) + 1\)
\(g[i] = min(f[i−1], h[i−1])\) (如果第 \(i\) 天游泳池開放)
\(h[i] = min(f[i−1], g[i−1])\) (如果第 \(i\) 天機房開放)
時間複雜度 \(O(n)\),空間複雜度 \(O(n).\)
例題:小奇挖礦
現在有 \(m + 1\) 個星球,從左到右標號為 \(0\) 到 \(m\),小奇最初在 \(0\) 號星球。
有 \(n\) 處礦體,第 \(i\) 處礦體有 \(a_i\) 單位原礦,在第 \(b_i\) 個星球上。
由於飛船使用的是老式的跳躍引擎,
每次它只能從第 \(x\) 號星球移動到第 \(x + 4\) 號星球或 \(x + 7\) 號星球。
每到一個星球,小奇會採走該星球上所有的原礦,
求小奇能採到的最大原礦數量。
注意,小奇不必最終到達 \(m\) 號星球。
資料範圍 \(n \leq 10^5, m \leq 10^9.\)
題解:一個最直接的想法是用 \(f[i]\) 表示走到第 \(i\) 個星球最多可以收集多少礦物。
但是由於 \(m\) 非常大,而這個演算法複雜度是 \(O(m)\),所以不可行。
發現如果相鄰兩處礦脈相差 \(> 17\),那麼他們之間一定能互相到達。
所以可以將所有相鄰距離 \(> 17\) 的礦脈之間的距離都改為 \(18\)。
那麼可以將 \(m\) 縮小到 \(18*n\) 的範圍內,即可用 \(O(m)\) 的演算法 通過。
時間複雜度 \(O(n*18)\),空間複雜度 \(O(n*18).\)
例題:大搬家
現在有一個長度為 \(n\) 的搬家指示,
其中 \(a_i\) 表示住在第 \(i\) 棟房子的人需要搬家到第 \(a_i\) 棟房子裡。
由於政府一時腦抽,這 \(n\) 戶人家連續進行了三次搬家,
大家發現一次搬家後的結果正好和三次搬家的結果一樣。
問有多少種不同的數列 \(a_i\),答案對 \(1000000007\) 取模。
資料範圍 \(n \leq 10^6.\)
題解:由於進行兩次搬家後結果不變,所以 \(a\) 這個置換隻包含一元環與二元環。
考慮 \(f[i]\) 表示長度為 \(i\) 的數列 \(a\) 的方案數。之後分兩種情況進行轉移。
-
新加入的 \(a_i = i\),那麼從 \(f[i−1]\) 轉移過來。
-
新加入的 \(a_i = j\),那麼有 \(a_j = i\),其 中 \(i,j\) 都為新加入的,
所以從 \(f[i−2]\) 轉移過來。
綜上有 \(f[i] = f[i−1] + f[i−2] * (i − 1)\)。
時間複雜度 \(O(n)\),空間複雜度 \(O(n).\)
例題:說真話
一次考試共有 \(n\) 個人參加,
第 \(i\) 個人說:“有 \(a_i\) 個人分數 比我高,\(b_i\)個人分數比我低。”
問最少有幾個人沒有說真話(可能有相同的分數)
資料範圍 \(n \leq 100000\)
題解:最少多少人說假話話 等價於 \(n-\)最多多少人說真話。
我們發現,對於第 \(i\) 個人,如果有 \(a_i\) 個分數比他高,有 \(b_i\)個分數比他低,
那麼說明區間 \([a_i + 1, n − b_i ]\)中所有人的分數相同。
所以我們可以將每個人表示成一個區間 \([l_i ,r_i ]\)。
如果兩個人所在的區間相交卻不重合,那麼他們一定不 能同時合法。
如果有超過 \((r_i − l_i + 1)\) 個人的區間同時為 \([l_i ,r_i ]\),
那麼最多隻能有 \((r_i − l_i + 1)\) 個人合法。
所以我們可以統計出每一種區間出現的次數,並按 照 \(r_i\) 順序排序。
用 \(s_i\) 表示區間 \([l_i ,r_i ]\) 中有多少人合法。
定義狀態 \(f_i\) 表示前 \(i\) 名最多有多少人合法。
之後考慮從前向後列舉每一種區間。
容易得到轉移 \(f[r_i] = max(max(f[1], ..., f[l_i−1]) + s_i , f[r_i] )\)。
使用字首最大值即可解決問題。
時間複雜度 \(O(n)\),空間複雜度 \(O(n).\)