一、动态规划问题
来源:
暴力搜索->记忆式搜索->经典的动态规划->改进的动态规划。这也是动态规划问题的求解步骤。
本质:
利用空间来换取时间。把一个问题分解为相同的子问题,这些子问题的求解是有顺序的,按顺序一步一步求解,前面的步骤和决策使得问题的状态转移到当前状态,当前状态再做出最优的决策,使问题转移到下一个状态,当问题进入最后一个状态时的解就是原问题的解。
二、练习题
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];
}
};