- 記憶化搜索
在搜索的過程中,如果搜索樹中有很多重復的結點,此時可以通過?個"備忘錄",記錄第?次搜索到的結果。當下?次搜索到這個結點時,直接在"備忘錄"??找結果。其中,搜索樹中的?個?個結點,也稱為?個?個狀態。
?如經典的斐波那契數列問題
int f[N]; // 備忘錄 int fib(int n)
{ // 搜索之前先往備忘錄??瞅瞅 if(f[n] != -1) return f[n]; if(n == 0 || n == 1) return f[n] = n; // 返回之前,把結果記錄在備忘錄中 f[n] = fib(n - 1) + fib(n - 2); return f[n];
}
- 遞歸改遞推
在?記憶化搜索解決斐波那契問題時,如果關注"備忘錄"的填寫過程,會發現它是從左往右依次填寫的。當i位置前?的格?填寫完畢之后,就可以根據格???的值計算出i位置的值。所以,整個遞歸過程,我們也可以改寫成循環的形式,也就是遞推
int f[N]; // f[i] 表?:第 i 個斐波那契數
int fib(int n)
{ // 初始化前兩個格? f[0] = 0; f[1] = 1; // 按照遞推公式計算后?的值 for(int i = 2; i <= n; i++) { f[i] = f[i - 1] + f[i - 2]; } // 返回結果 return f[n];
}
- 動態規劃
動態規劃(Dynamic Programming,簡稱DP)是?種?于解決多階段決策問題的算法思想。它通過將復雜問題分解為更?的?問題,并存儲?問題的解(通常稱為“狀態”),從?避免重復計算,提?效率。因此,動態規劃?,蘊含著分治與剪枝思想。
上述通過記憶化搜索以及遞推解決斐波那契數列的?式,其實都是動態規劃。
注意:
- 動態規劃中的相關概念其實遠不?如此,還會有:重疊?問題、最優?結構、?后效性、有向?環圖等等。
- 這些概念沒有?段時間的沉淀是不可能完全理解的。可以等學過?段時間之后,再去接觸這些概念。不過,這些概念即使不懂,也不影響做題~
在遞推形式的動態規劃中,常?下?的專有名詞來表述:
- 狀態表?:指 f 數組中,每?個格?代表的含義。其中,這個數組也會稱為 dp 數組,或者dp 表。
- 狀態轉移?程:指 f 數組中,每?個格?是如何?其余的格?推導出來的
- 初始化:在填表之前,根據題?中的默認條件或者問題的默認初始狀態,將 f 數組中若?格?先填上值。
其實遞推形式的動態規劃中的各種表述,是可以對應到遞歸形式的:
- 狀態表?<—>遞歸函數的意義;
- 狀態轉移?程<—>遞歸函數的主函數體;
- 初始化<—>遞歸函數的遞歸出?。
- 如何利?動態規劃解決問題
第?種?式當然就是記憶化搜索了:
- 先?遞歸的思想去解決問題;
- 如果有重復?問題,就改成記憶化搜索的形式。
第?種?式,直接使?遞推形式的動態規劃解決:
- 定義狀態表?:
?般情況下根據經驗+遞歸函數的意義,賦予 dp 數組相應的含義。(其實還可以去蒙?個,如果蒙的狀態表?能解決問題,說明蒙對了。如果蒙錯了,再換?個試~) - 推導狀態轉移?程:
根據狀態表?以及題意,在 dp 表中分析,當前格?如何通過其余格?推導出來。 - 初始化:
根據題意,先將顯?易?的以及邊界情況下的位置填上值。 - 確定填表順序:
根據狀態轉移?程,確定按照什么順序來填表。 - 確定最終結果:
根據題意,在表中找出最終結果
P10250 [GESP樣題 六級] 下樓梯 - 洛谷
因為上樓和下樓是?個可逆的過程,因此我們可以把下樓問題轉化成上到第n個臺階,?共有多少種?案。
解法:動態規劃
- 狀態表?:
dp[i]
表?:?到第i 個臺階的總?案數。
那最終結果就是在dp[n]
處取到。 - 狀態轉移?程:
根據最后?步劃分問題,?到第i 個臺階的?式有三種:
a. 從i - 1 臺階向上?1 個臺階,此時?到i 臺階的?案就是dp[i - 1]
;
b. 從i - 2 臺階向上?2 個臺階,此時?到i 臺階的?案就是dp[i - 2]
;
c. 從i - 3 臺階向上?3 個臺階,此時?到i 臺階的?案就是dp[i - 3]
;
綜上所述,dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3]
。 - 初始化:
填i位置的值時,?少需要前三個位置的值,因此需要初始化dp[0] = 1, dp[1] = 1, dp[2] = 2
,然后從i = 3
開始填。
或者初始化dp[1] = 1, dp[2] = 2, dp[3] = 4
,然后從i = 4 開始填。 - 填表順序:
明顯是從左往右
#include <bits/stdc++.h>
using namespace std;typedef long long LL;const int N = 65;int n;
LL f[N]; //f[i]表示有i個臺階的時候,一共有多少種方案int main()
{ios::sync_with_stdio(false);cin.tie(0);cin >> n;//初始化f[0] = 1; f[1] = 1; f[2] = 2;for (int i = 3; i <= n; i++){f[i] = f[i - 1] + f[i - 2] + f[i - 3]; }cout << f[n] << endl;return 0;
}
動態規劃的空間優化:
我們發現,在填寫dp[i]
的值時,我們僅僅需要前三個格?的值,第i-4個及其之前的格?的值已經毫??處了。因此,可以?三個變量記錄i位置之前三個格?的值,然后在填完i位置的值之
后,滾動向后更新
#include <bits/stdc++.h>
using namespace std;typedef long long LL;const int N = 65;int n;int main()
{ios::sync_with_stdio(false);cin.tie(0);cin >> n;LL a = 1, b = 1, c = 2;for (int i = 3; i <= n; i++){LL t = a + b + c;a = b;b = c;c = t;}if (n == 1) cout << b << endl;else cout << c << endl;return 0;
}
P1216 [IOI 1994] 數字三角形 Number Triangles - 洛谷
學習動態規劃最經典的??題。
解法:動態規劃
- 狀態表?:
dp[i][j]
表?:?到[i, j]
位置的最?權值。
那最終結果就是在dp 表的第n ?中,所有元素的最?值。 - 狀態轉移?程:
根據最后?步劃分問題,?到[i, j]
位置的?式有兩種:
a. 從[i - 1, j]
位置向下??格,此時?到[i, j]
位置的最?權值就是dp[i - 1][j]
;
b. 從[i - 1, j - 1]
位置向右下??格,此時?到[i, j]
位置的最?權值就是dp[i - 1][j - 1]
;
綜上所述,應該是兩種情況的最?值再加上[i,j]
位置的權值:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1]) + a[i][j]
- 初始化:
因為dp 表被0 包圍著,并不影響我們的最終結果,因此可以直接填表。
思考,如果權值出現負數的話,需不需要初始化?
? 此時可以全都初始化為 ? ∞ -\infty ?∞ ,負?窮?在取max 之后,并不影響最終結果。 - 填表順序:
從左往右填寫每??,每??從左往右
#include <bits/stdc++.h>
using namespace std;const int N = 1010;int n;
int a[N][N];
int f[N][N]; //f[i][j]表示從1,1走到i,j的時候,所有方案數的最大權值int main()
{ios::sync_with_stdio(false);cin.tie(0);cin >> n;for (int i = 1; i <= n; i++)for (int j = 1; j <= i; j++)cin >> a[i][j];for (int i = 1; i <= n; i++){for (int j = 1; j <= i; j++){f[i][j] = max(f[i-1][j], f[i-1][j-1]) + a[i][j]; }}int ret = 0;for (int j = 1; j <= n; j++){ret = max(ret, f[n][j]); }cout << ret << endl;return 0;
}
動態規劃的空間優化:
我們發現,在填寫第i ?的值時,我們僅僅需要前??的值,并不需要第i - 2 以及之前?的值。
因此,我們可以只??個?維數組來記錄上??的結果,然后在這個數組上更新當前?的值
需要注意,當?因為我們當前這個位置的值需要左上?位置的值,因此滾動數組優化的時候,要改變第?維的遍歷順序
#include <bits/stdc++.h>
using namespace std;const int N = 1010;int n;
int a[N][N];
int f[N]; //f[i][j]表示從1,1走到i,j的時候,所有方案數的最大權值int main()
{ios::sync_with_stdio(false);cin.tie(0);cin >> n;for (int i = 1; i <= n; i++)for (int j = 1; j <= i; j++)cin >> a[i][j];for (int i = 1; i <= n; i++){for (int j = i; j >= 1; j--){f[j] = max(f[j], f[j-1]) + a[i][j]; }}int ret = 0;for (int j = 1; j <= n; j++){ret = max(ret, f[j]); }cout << ret << endl;return 0;
}