三个重要概念:最优子结构、边界、状态转移方程
动态规划特征:
(1)最优子结构:如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。意思就是,总问题包含很多个子问题,而这些子问题的解也是最优的。
(2)重叠子问题:子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率。
1、台阶问题:有一座高度是10级台阶的楼梯,从下往上走,每跨一步只能向上1级或2级台阶,要求用程序来求出一共有多少种走法?
解法:设f(10)为到第10阶台阶的走法数,那么f(10)=f(8)+f(9), f(8)和f(9)即为f(10)的最优子结构, 可以看出是一个斐波那契数列,更一般地,f(i) = f(i-2)+f(i-1),边界值f(1)=1,f(2)=2,那么状态转移方程如下:
f(1) = 1,
f(2) = 2,
f(n) = f(n-1) + f(n-2) (n>=3).
分析:用递归方法求解时,在递归树上有很多结点是重复的,而且重复的结点数会随着n的增大而急剧增大,计算量也会随之增大,所以用递归方法的时间复杂度是以n的指数的方式递增的。所以不能使用递归求解,优化的方向是怎样避免计算重复值,所以更好的方法是自底向上计算,这样前面计算过的就不用再重复计算了。
代码实现:
1
2
3
4
5
6
7
8
9
10
11
12
public
int
Fibonacci(
int
n){
if(
n
==
1)
return
1;
if(
n
==
2)
return
2;
int
fibOne
=
1,
fibTwo
=
2;
int
fibN
=
0;
for(
int
i
=
3;
i
<=
n;
i
++){
fibN
=
fibOne
+
fibTwo;
fibOne
=
fibTwo;
fibTwo
=
fibN;
}
return
fibN;
}
时间复杂度:O(n)
2、变态台阶问题:有n阶台阶,一只青蛙可以跳上1级台阶,也可以跳上2级台阶...也可以跳上n级,此时青蛙跳上一个n级的台阶总共有多少种跳法?
解法:设f(n)为青蛙跳上n阶台阶的跳法数,f(4-1)为4阶台阶下有一次跳1阶台阶的跳法数,可以知道四级台阶下有一次选择跳1阶的跳法数f(4-1)等于3级台阶的所有跳法数f(3),所以f(4-1)=f(3),那么
f(0) = 1;
f(1) = 1;
f(2) = f(2-1)+f(2-2) = f(1)+f(0);
f(3) = f(3-1)+f(3-2)+f(3-3) = f(2)+f(1)+f(0)
.....
f(n) = f(n-1)+ f(n-2)+...+f(2)+f(1) +f(0) (1) (n>=0)
f(n-1) = f(n-2) +f(n-1)+...+f(2)+f(1) +f(0) (2) (n>=1)
(1)-(2)得到:f(n) = 2f(n-1)=4f(n-2)=..=2^(n-1)f(1)=2^(n-1) (n>=1)
代码实现:
public
int
JumpFloorII(
int
target) {
int
sum
=
1;
for(
int
i
=
1;
i
<
n;
i
++)
sum
*
=
2;
return
sum;
}
时间复杂度:O(n)
3、最长递增子序列问题:给定一个长度为N的数组,找出一个最长的单调自增子序列(不一定连续,但是顺序不能乱)。例如:给定一个长度为6的数组A{5, 6, 7, 1, 2, 8},则其最长的单调递增子序列为{5,6,7,8},长度为4.
解法1:设数组为array,以array[j]结尾的最长递增子序列为L[j],那么L[j] = max(L[i])+1, 其中 0=<i<j,array[j]>array[i],L[0]=1,这样求得所有L[j](0<=j<=array.length-1)后,比较所有的L[j]可获得最长单调递增子序列的长度,如代码实现1所示。如果还需要求得最长递增子序列,需要用一个数组保存以每个元素i结尾的递增子序列,如果有多个最长递增子序列,暂时只能求得一个子序列,如代码实现2所示。
代码实现1:求最长单调递增子序列的长度。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public
int
LIS(
int[]
array){
//定义以array[j]结尾的最长递增子序列L[j]
int[]
L
=
new
int[
array.
length];
//获得L[j]
L[
0]
=
1;
//边界值
for(
int
j
=
1;
j
<
array.
length;
j
++){
int
maxL
=
0;
for(
int
i
=
0;
i
<
j;
i
++){
if(
array[
j]
>
array[
i]
&&
L[
i]
>
maxL)
maxL
=
L[
i];
}
L[
j]
=
maxL
+1;
}
//比较所有的L[j],获得最长单调递增子序列的长度
int
max
=
L[
0];
for(
int
j
=
1;
j
<
array.
length;
j
++){
if(
L[
j]
>
max)
max
=
L[
j];
}
return
max;
}
时间复杂度:O(n^2)
代码实现2:求最长单调递增子序列的长度和一个最长递增子序列。
public
void
longestIncreSubSeq(
int[]
array) {
int[]
L
=
new
int[
array.
length];
// 求得所有的L[j],并比较所有的L[j],返回最大的值
int
max
=
L[
0];
int
maxj
=
-1;
int[][]
lensArr
=
new
int[
array.
length][
array.
length];
// 保存以每个元素i结尾的递增子序列
for (
int
i
=
0;
i
<
array.
length;
i
++) {
L[
i]
=
1;
lensArr[
i][
0]
=
array[
i];
}
for (
int
j
=
1;
j
<
array.
length;
j
++) {
int
maxL
=
0;
int
maxi
=
-1;
// 记录max(L[i])的下标值i
for (
int
i
=
0;
i
<
j;
i
++) {
if (
array[
j]
>
array[
i]) {
if (
L[
i]
>
maxL) {
maxL
=
L[
i];
maxi
=
i;
}
}
}
L[
j]
=
maxL
+
1;
if (
maxi
!=
-1) {
for (
int
k
=
0;
k
<
L[
maxi];
k
++)
lensArr[
j][
k]
=
lensArr[
maxi][
k];
// 来自L[maxi]中的数组添加到L[j]的数组中
lensArr[
j][
L[
j]
-
1]
=
array[
j];
// 补充最后一个元素为当前结尾元素
}
if (
L[
j]
>
max) {
max
=
L[
j];
maxj
=
j;
}
}
System.
out.
println(
"最长递增子序列长度:"
+
max);
System.
out.
print(
"最长递增子序列为:");
for (
int
i
=
0;
i
<
max;
i
++)
System.
out.
print(
lensArr[
maxj][
i]
+
" ");
}
4、最长公共子序列问题:求两个或多个已知数列最长的子序列。
解法:运用自底向上的填表格法编码,设x[1...m]和y[1...n],c[i,j]为x[1...i]和y[1..j]的最长公共子序列的长度,那么
c[i,j] = c[i-1,j-1] if x[i] = x[j]
c[i,j] = max{c[i-1,j],c[i,j-1]} otherwise
最后用在表格中用标记回溯法返回所有的最长公共子序列。
表格例子:
|
| A | B | C | B | D | A | B |
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
B | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 |
D | 0 | 0 | 1 | 1 | 1 | 2 | 2 | 2 |
C | 0 | 0 | 1 | 2 | 2 | 2 | 2 | 2 |
A | 0 | 1 | 1 | 2 | 2 | 2 | 3 | 3 |
B | 0 | 1 | 2 | 2 | 3 | 3 | 3 | 4 |
A | 0 | 1 | 2 | 2 | 3 | 3 | 4 | 4 |
代码实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//返回标记b
public
int[][]
LCS(
String
x,
String
y){
int
xLen
=
x.
length();
int
yLen
=
y.
length();
int[][]
b
=
new
int[
xLen
+1][
yLen
+1];
int[][]
c
=
new
int[
xLen
+1][
yLen
+1];
for(
int
i
=
0;
i
<=
xLen;
i
++)
c[
i][
0]
=
0;
for(
int
j
=
0;
j
<=
yLen;
j
++)
c[
0][
j]
=
0;
for(
int
i
=
1;
i
<=
xLen;
i
++){
for(
int
j
=
1;
j
<=
yLen;
j
++)
if(
x.
charAt(
i
-1)
==
y.
charAt(
j
-1)) {
c[
i][
j]
=
c[
i
-1][
j
-1]
+1;
b[
i][
j]
=
1;
}
else
if(
c[
i
-1][
j]
>
c[
i][
j
-1]){
c[
i][
j]
=
c[
i
-1][
j];
b[
i][
j]
=
0;
}
else{
c[
i][
j]
=
c[
i][
j
-1];
b[
i][
j]
=
-1;
}
}
return
b;
}
//打印路径
public
void
Display(
int[][]
b,
String
x,
int
i,
int
j){
if(
i
==
0
||
j
==
0)
return;
if(
b[
i][
j]
==
1){
Display(
b,
x,
i
-1,
j
-1);
System.
out.
print(
x.
charAt(
i
-1)
+
" ");
}
else
if(
b[
i][
j]
==
0)
Display(
b,
x,
i
-1,
j);
else
if(
b[
i][
j]
==
-1)
Display(
b,
x,
i,
j
-1);
}
时间复杂度:O(m*n)
5、连续子数组和最大问题:一个整数数组中的元素有正有负,在该数组中找出一个连续子数组,要求该连续子数组中各元素的和最大,这个连续子数组便被称作最大连续子数组。比如数组{2,4,-7,5,2,-1,2,-4,3}的最大连续子数组为{5,2,-1,2},最大连续子数组的和为5+2-1+2=8。
解法:设数组为array,以array[i]结尾的最大连续子数组和为dp[i],那么dp[i]=dp[i-1]>0?dp[i-1]:0+array[i],其中0<=i<=array.length-1,dp[0]=array[0],最后比较所有的dp[i],其中最大的一个即为最大连续子数组和。
代码实现:
public
int
maxSubArray(
int[]
array){
int[]
dp
=
new
int[
array.
length];
dp[
0]
=
array[
0];
//边界值
int
maxSum
=
dp[
0];
for(
int
i
=
1;
i
<
array.
length;
i
++){
dp[
i]
=
dp[
i
-1]
>
0?
dp[
i
-1]:
0
+
array[
i];
//间接最优子结构
maxSum
=
Math.
max(
dp[
i],
maxSum);
}
return
maxSum;
}
时间复杂度:O(n)
6、背包问题:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。
7、股票问题:3种变形
8、编辑距离问题:求两个字串之间,由一个转成另一个所需的最少编辑操作次数。编辑操作包括替换、插入、删除一个字符。一般来说,编辑距离越小,两个串的相似度越大。
9、字符串对齐问题:两个字符串中相同的字符串彼此对应的最小花费?
注:代码均为手写输入,可能有错,其他动态规划问题未完待续