動態規劃(2):動態規劃的三種形式
阿新 • • 發佈:2019-01-23
例:數字三角形(POJ 1163)
Language:DefaultThe TriangleTime Limit: 1000MS | Memory Limit: 10000K |
Total Submissions: 45053 | Accepted: 27208 |
Description
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
(Figure 1)
Input
Output
Sample Input
5 7 3 8 8 1 0 2 7 4 4 4 5 2 6 5
Sample Output
30
Source
基本思路
用二維陣列存放數字三角形。
D( r, j) : 第r行第 j 個數字(r,j從1開始算)
MaxSum(r, j) : 從D(r,j)到底邊的各條路徑中,
最佳路徑的數字之和。
問題:求 MaxSum(1,1)
典型的遞迴問題。
D(r, j)出發,下一步只能走D(r+1,j)或者D(r+1, j+1)。故對於N行的三角形:
if ( r == N)
MaxSum(r,j) = D(r,j)
else
MaxSum( r, j) = Max{ MaxSum(r+1,j), MaxSum(r+1,j+1) } + D(r,j)
然後你會發現:
為什麼超時
重複計算
如果採用遞規的方法,深度遍歷每條路徑,存在大量重複計算。則時間複雜度為 2n,對於 n = 100 行,肯定超時。
改進
如果每算出一個MaxSum(r,j)就儲存起來,下次用到其值的時候直接取用,則可免去重複計算。那麼可以用O(n2)時間完成計算。因為三角形的數字總數是 n(n+1)/2
改進程式碼
#include <iostream>
#include <algorithm>
#define MAX 101
using namespace std;
int D[MAX][MAX]; int n;
int maxSum[MAX][MAX];
int MaxSum(int i, int j)
{
if (maxSum[i][j] != -1)
return maxSum[i][j];
if (i == n) maxSum[i][j] = D[i][j];
else {
int x = MaxSum(i + 1, j);
int y = MaxSum(i + 1, j + 1);
maxSum[i][j] = max(x, y) + D[i][j];
}
return maxSum[i][j];
}
int main()
{
int i, j;
cin >> n;
for (i = 1; i <= n; i++)
for (j = 1; j <= i; j++)
{
cin >> D[i][j];
maxSum[i][j] = -1;
}
cout << MaxSum(1, 1) << endl;
}
轉化為遞推
“人人為我”型遞推
#include <iostream>
#include <algorithm>
#define MAX 101
using namespace std;
int D[MAX][MAX]; int n;
int maxSum[MAX][MAX];
int main()
{
int i, j;
cin >> n;
for (i = 1; i <= n; i++)
for (j = 1; j <= i; j++)
cin >> D[i][j];
for (int i = 1; i <= n; ++i)
maxSum[n][i] = D[n][i];
for (int i = n - 1; i >= 1; --i)
for (int j = 1; j <= i; ++j)
maxSum[i][j] = max(maxSum[i + 1][j], maxSum[i + 1][j + 1]) + D[i][j];
cout << maxSum[1][1] << endl;
}
這裡說的人人為我型遞迴就是在求DP[i][j]的過程中, 使用DP[i-1][1]→DP[i - 1][i - 1]來推到出DP[i][j];
“我為人人”型遞迴
#include <iostream>
#include <algorithm>
#define MAX 101
using namespace std;
int D[MAX][MAX]; int n;
int maxSum[MAX][MAX];
int main()
{
int i, j;
cin >> n;
for (i = 1; i <= n; i++)
for (j = 1; j <= i; j++)
cin >> maxSum[i][j];
for (int i = 1; i < n; i++)
{
for (int j = 1; j <= i; j++)
{
maxSum[i + 1][j] = max(maxSum[i + 1][j], maxSum[i + 1][j] + maxSum[i][j]);
maxSum[i + 1][j + 1] = max(maxSum[i + 1][j + 1], maxSum[i + 1][j + 1] + maxSum[i][j]);
}
}
int maxx = -9999999999;
for (int i = 1; i <= n; i++)
maxx = max(maxx, maxSum[n][i]);
cout << maxx << endl;
}
這裡說的人人為我型遞迴就是在求DP[i][j]的過程中, 求出DP[i + 1][j]和DP[i + 1][j +1]的其中一個解。
空間優化
沒必要用二維maxSum陣列儲存每一個MaxSum(r,j),只要從底層一行行向上遞推,那麼只要一維陣列maxSum[100]即可,即只要儲存一行的MaxSum值就可以。
進一步考慮,連maxSum陣列都可以不要,直接用D的第n行替代maxSum即可。
節省空間,時間複雜度不變
#include <iostream>
#include <algorithm>
#define MAX 101
using namespace std;
int D[MAX][MAX];
int n; int * maxSum;
int main()
{
int i, j;
cin >> n;
for (i = 1; i <= n; i++)
for (j = 1; j <= i; j++)
cin >> D[i][j];
maxSum = D[n]; //maxSum指向第n行
for (int i = n - 1; i >= 1; --i)
for (int j = 1; j <= i; ++j)
maxSum[j] = max(maxSum[j], maxSum[j + 1]) + D[i][j];
cout << maxSum[1] << endl;
}
·
·
·
總結三種動態規劃的形式
人人為我
狀態i的值Fi由若干個值已知的狀態值Fk,Fm,..Fy推出,如求和,取最大值
我為人人
狀態i的值Fi在被更新(不一定是最終求出)的時候,依據Fi去更新(不一定是最終求出)和狀態i相關的其他一些狀態的值Fk,Fm,..Fy
注意:在選取最優備選狀態的值Fm,Fn,…Fy時,有可能有好的演算法或資料結構可以用來顯著降低時間複雜度。
記憶型遞迴
優點:只經過有用的狀態,沒有浪費。遞推型會檢視一些沒用的狀態,有浪費
缺點:可能會因遞迴層數太深導致爆棧,函式呼叫帶來額外時間開銷。無法使用滾動陣列節省空間。總體來說,比遞推型慢。