一、動態規劃問題
來源:暴力搜索->記憶式搜索->經典的動態規劃->改進的動態規劃。這也是動態規劃問題的求解步驟。
本質:利用空間來換取時間。把一個問題分解爲相同的子問題,這些子問題的求解是有順序的,按順序一步一步求解,前面的步驟和決策使得問題的狀態轉移到當前狀態,當前狀態再做出最優的決策,使問題轉移到下一個狀態,當問題進入最後一個狀態時的解就是原問題的解。
二、練習題
1、有數組penny,penny中所有的值都爲正數且不重複。每個值代表一種面值的貨幣,每種面值的貨幣可以使用任意張,再給定一個整數aim(小於等於1000)代表要找的錢數,求換錢有多少種方法。
解法(1):按照經典的動態規劃步驟進行,空間複雜度爲O(n*aim)
class
Exchange
{
public
:
int
countWays(vector<
int
>
penny,
int
n,
int
aim)
{
if
(penny.empty()||n
==
0
)
return
0
;
vector<vector<
int
>
> dp(n,vector<
int
>(aim+
1
));
for
(
int
i
=
0
;i
< n;i++) {
dp[i][
0
]
=
1
;
}
for
(
int
j
=
1
;j
< aim+
1
;j++)
{
dp[
0
][j]
= j%penny[
0
]
==
0
?
1
:
0
;
}
for
(
int
i
=
1
;i
< n;i++) {
for
(
int
j
=
1
;j
< aim+
1
;j++)
{
dp[i][j]
= (j-penny[i]) >=
0
?(dp[i-
1
][j]
+ dp[i][j-penny[i]]):dp[i-
1
][j];
}
}
return
dp[n-
1
][aim];
}
};
解法(2):步驟與經典的動態規劃問題一樣,但是空間複雜度僅爲O(aim)。其實在求dp矩陣時,都是根據上一行的值迭代出當前行的值,所以完全可以只用一維矩陣來存儲,不斷地更新一維矩陣即可。
class
Exchange
{
public
:
int
countWays(vector<
int
>
penny,
int
n,
int
aim)
{
vector<
int
>
dp(aim +
1
);
for
(
int
i
=
0
;
i <= aim; i++)
if
(i
% penny[
0
]
==
0
)
dp[i]
=
1
;
for
(
int
i
=
1
;
i < n; i++)
for
(
int
j
=
1
;
j <= aim; j++)
if
(
j >= penny[i])
dp[j]
+= dp[j - penny[i]];
return
dp[aim];
}
};
解法:f(n)=f(n-1)+f(n-2)。如果直接用遞歸式求解,中間有很多重複的計算,f(n-1)分支計算過的還得在f(n-2)分支計算一次。然後狀態之間的依賴關係是很容易找出了,用動態規劃法,一步一步記錄相鄰兩個狀態即可,下一個狀態等於這兩個狀態之和。
class
GoUpstairs
{
public
:
int
countWays(
int
n)
{
vector<
int
>
dp(
2
,
0
);
dp[
0
]
=
1
;
dp[
1
]
=
2
;
int
temp;
for
(
int
i
=
3
;i
<= n;i++) {
temp
= dp[
0
];
dp[
0
]
= dp[
1
];
dp[
1
]
= (dp[
1
]+temp)%
1000000007
;
}
return
dp[
1
]%
1000000007
;
}
};
3、有一個矩陣map,它每個格子有一個權值。從左上角的格子開始每次只能向右或者向下走,最後到達右下角的位置,路徑上所有的數字累加起來就是路徑和,返回所有的路徑中最小的路徑和。
解法:f(n,m)=min(f(n-1,m),f(n,m-1))+map[n][m]。遞歸式同樣包含很多重複計算,可以根據狀態之間的依賴關係一步一步計算出來。走到第一行每個格子的最小路徑和很容易求出。根據第一行可以依次求出第二行,依次進行直到計算到最後一行。
class
MinimumPath
{
public
:
int
getMin(vector<vector<
int
>
> map,
int
n,
int
m)
{
vector<
int
>
dp(m,
0
);
dp[
0
]
= map[
0
][
0
];
for
(
int
i
=
1
,j
=
0
;i
< m;i++,j++) {
dp[i]
= map[
0
][i]+dp[j];
}
for
(
int
i
=
1
;i
< n;i++) {
dp[
0
]
+= map[i][
0
];
for
(
int
j
=
1
;j
< m;j++) {
dp[j]
= min(dp[j],dp[j-
1
])+map[i][j];
}
}
return
dp[m-
1
];
}
};
解法:用dp數組的dp[i]記錄下以A[i]結尾的遞增子序列中最長的長度,計算dp[i+1]時,遍歷A[0~i]找到比A[i+1]小的元素,再比較與這些元素對應的dp數組中的值,找到最大的一個再加1,賦值給dp[i+1]。
class LongestIncreasingSubsequence {public:
int getLIS(vector<int> A, int n) {
if (A.empty()||n == 0)
return 0;
vector<int> dp(n,0);
dp[0] = 1;
int resMax = 0;
for (int i = 1;i < n;i++) {
int tempMax = 0;
for (int j = 0;j < i;j++) {
if (A[i] > A[j])
tempMax = max(tempMax,dp[j]);
}
dp[i] = ++tempMax;
resMax = max(resMax,dp[i]);
}
return resMax;
}
};
解法:經典的動態規劃題目(LCS)。利用動態規劃表求解。dp[i][j]表示A[0~i]和B[0~j]的最長公共子序列長度。如果A[i]=B[j], 則dp[i][j]一定是dp[i-1][j-1]+1,若A[i]!=B[j],則dp[i][j]要麼是dp[i-1][j],要麼是dp[i][j-1]。
(1)常規解法:對第一行和第一列的處理不夠巧妙。
class
LCS
{
public
:
int
findLCS(string
A,
int
n,
string B,
int
m)
{
if
(A.empty()||n==
0
||B.empty()||m==
0
)
return
0
;
vector<vector<
int
>
> dp(n,vector<
int
>(m));
for
(
int
i
=
0
;i
< m;i++) {
if
(A[
0
]
== B[i]) {
for
(
int
j
= i;j < m;j++)
dp[
0
][j]
=
1
;
break
;
}
}
for
(
int
i
=
0
;i
< n;i++) {
if
(B[
0
]
== A[i]) {
for
(
int
j
= i;j < n;j++)
dp[j][
0
]
=
1
;
break
;
}
}
for
(
int
i
=
1
;i
< n;i++) {
for
(
int
j
=
1
;j
< m;j++) {
if
(A[i]
== B[j])
dp[i][j]
= dp[i-
1
][j-
1
]+
1
;
else
dp[i][j]
= max(dp[i-
1
][j],dp[i][j-
1
]);
}
}
return
dp[n-
1
][m-
1
];
}
};
class
LCS
{
public
:
int
findLCS(string
A,
int
n,
string B,
int
m)
{
vector<vector<
int
>
> dp(n+
1
,vector<
int
>(m+
1
,
0
));
for
(
int
i
=
1
;i<=n
;++i){
for
(
int
j=
1
;
j<=m; ++j){
if
(A[i-
1
]
== B[j-
1
]){
dp[i][j]
= dp[i-
1
][j-
1
]+
1
;
}
else
{
dp[i][j]
= max( dp[i-
1
][j]
,dp[i][j-
1
]);
}
}
}
return
dp[n][m];
}
};
解法:經典的01揹包問題。同樣利用動態規劃表來求解。
(1)常規解法:用常規的二維矩陣dp作爲動態規劃表,第一行第一列單獨提前處理。空間複雜度略高。
class
Backpack
{
public
:
int
maxValue(vector<
int
>
w, vector<
int
>
v,
int
n,
int
cap)
{
if
(w.empty()||v.empty()||n==
0
||cap==
0
)
return
0
;
vector<vector<
int
>
> dp(n,vector<
int
>(cap+
1
));
for
(
int
j
=
1
;j
< cap+
1
;j++)
{
dp[
0
][j]
= w[
0
]
<= j?v[
0
]:
0
;
}
for
(
int
i
=
0
;i
< n;i++) {
dp[i][
0
]
=
0
;
}
for
(
int
i
=
1
;i
< n;i++) {
for
(
int
j
=
1
;j
< cap+
1
;j++)
{
if
(w[i]
> j)
dp[i][j]
= dp[i-
1
][j];
else
dp[i][j]
= max(dp[i-
1
][j],v[i]+dp[i-
1
][j-w[i]]);
}
}
return
dp[n-
1
][cap];
}
};
(2)更優的解法:用一維矩陣dp作爲動態規劃表。每次用複製構造函數記錄上一行的求解結果,根據上一行的求解結果求出當前行的結果後再記錄到dp矩陣中。空間複雜度略好。
class Backpack {public:
int maxValue(vector<int> w, vector<int> v, int n, int cap) {
if (w.empty()||v.empty()||n==0||cap==0)
return 0;
vector<int> dp(cap+1,0);
for (int i = 0;i < n;i++) {
vector<int> last(dp);
for (int j = 1;j < cap+1;j++) {
dp[j] = j < w[i]?last[j]:max(last[j],v[i]+last[j-w[i]]);
}
}
return dp[cap];
}
};