1. 程式人生 > 其它 >君君演算法課堂-動態規劃基礎及線性動態規劃

君君演算法課堂-動態規劃基礎及線性動態規劃

目錄

動態規劃基礎及線性動態規劃

動態規劃的狀態及記憶化搜尋

動態規劃\((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).\)