05-动态规划
核心思想:拆分子问题,记住过往,减少重复计算
例子:青蛙跳阶问题(参考力扣70)
一个青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上10级台阶总共有多少种跳法?
- 到10
- 先到9,再跳1;
- 先到8,再跳2;
以此类推:定义f(n) = 跳到第n级台阶的跳法
有:
f(10) = f(9) + f(8);
f(9) = f(8) + f(7);
…
f(3) = f(2) + f(1)
而f(2) = 2,f(1) = 1很容易得出。
f(n) = f(n - 1) + f(n - 2)
1
2
3
4
5
6
7
8
9
class Solution { //直接递归出现超时
public int climbStairs(int n) {
if(n == 1)
return 1;
else if(n == 2)
return 2;
return climbStairs(n-1) + climbStairs(n - 2);
}
}
!!递归树中,一个子问题的事件复杂度为O(1),共有2的n次幂-1个节点,因此总时间复杂度为O(2的次幂)!!
带备忘录的递归解法(自顶向下)
子问题数=树结点数=n,时间复杂度为O(n)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
//使用HashMap充当备忘录
Map<Integer,Integer> tempMap = new HashMap();
public int climbStairs(int n) {
if(n == 0) return 1;
if(n <= 2) return n;
if(tempMap.containsKey(n)){
return tempMap.get(n);
}else{
tempMap.put(n,climbStairs(n-1)+climbStairs(n-2));
return tempMap.get(n);
}
}
}
自底向上的动态规划
一些概念:以本题为例
- f(n-1)和f(n-2)称为f(n)的最优子结构;
- f(n) = f(n - 1) + f(n - 2)称为状态转移方程;
- f(1) = 1,f(2) = 2为边界;
- f(10) = f(9) + f(8),f(9) = f(8) + f(7)就是重叠子问题。
在本题中 ,f(n)只依赖与前面的两个数,因此只需要两个变量a和b存储;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public int climbStairs(int n) {
if(n <= 2)
return n;
int a = 1;
int b = 2;
int temp = 0;
for(int i = 3;i<=n;i++){
temp = a + b;
a = b;
b = temp;
}
return temp;
}
}
最长公共子序列问题: 洛谷P1439
dp[i][j]:处理完第一个序列的前i个字符,第二个序列的前j个字符,现有的最长公共子序列;dp[0][?],dp[?][0] = 0;状态转移方程:
1 2
dp[i][j] = dp[i-1][j-1] + 1, if a[i] = b[j] max{dp[i-1][j],dp[i][j-1]}, else
容易得到下面代码:
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
package luogu;
import java.util.Scanner;
public class P1439 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] a = new int[n + 1];
int[] b = new int[n + 1];
for (int i = 1; i <= n; i++) {
a[i] = sc.nextInt();
}
for (int i = 1; i <= n; i++) {
b[i] = sc.nextInt();
}
int[][] dp = new int[n + 1][n + 1];
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= n; j++) {
if (i == 0 || j == 0)
dp[i][j] = 0;
else if (a[i] == b[j])
dp[i][j] = dp[i - 1][j - 1] + 1;
else
dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]);
}
}
System.out.println(dp[n][n]);
}
}
部分正确,但存在MLE,TLE
初步改进,使用滚动数组:
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
40
41
package luogu;
import java.util.Scanner;
public class P1439 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] a = new int[n + 1];
int[] b = new int[n + 1];
for (int i = 1; i <= n; i++) {
a[i] = sc.nextInt();
}
for (int i = 1; i <= n; i++) {
b[i] = sc.nextInt();
}
// int[][] dp = new int[n + 1][n + 1];
int[] cur = new int[n + 1];
int[] pre = new int[n + 1];
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= n; j++) {
if (i == 0 || j == 0)
cur[j] = pre[j] = 0;
else if (a[i] == b[j])
// dp[i][j] = dp[i - 1][j - 1] + 1;
cur[j] = pre[j - 1] + 1;
else
// dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]);
cur[j] = Math.max(cur[j - 1], pre[j]);
}
int[] temp = new int[n + 1];
pre = cur;
cur = temp;
}
System.out.println(pre[n]);
}
}
- 分析直接使用状态方程的代码可知,每一个dp的更新,只与本行和上一行的dp表有关;
- 因此只需要存储当前行的cur[j],以及上一行的pre[j],代替二维数组即可;
- 每一行更新完后,令pre = cur, cur置0,滚动起来。
滚动数组只减少了内存,在时间复杂度方面并没有改进,引出最终解法:LIS(最长上升子序列)
原本先求两个序列的最长公共子序列,但b是a的一个重排;
找LCS等价于找b在a中索引序列的LIS
使用贪心+二分查找:
- 用数组lis存储当前构造的LIS;
- 贪心策略:
- 如果x比lis末尾大,x直接加入lis,即扩展lis;
- 否则,用二分查找替换lis中第一个>=x的数,保证lis尽可能小,才能找到更长的上升序列;
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
40
41
42
package luogu;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
public class P1439 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
Map<Integer,Integer> pos = new HashMap<>();
// a[i]在原数组中的索引
for(int i = 1;i<=n;i++) {
int num = sc.nextInt();
pos.put(num, i);
}
int[] indexSeq = new int[n];
// 将b[i]映射至a的索引
for(int i = 0;i<n;i++) {
int num = sc.nextInt();
indexSeq[i] = pos.get(num);
}
//indexSeq中为b在a中对应的索引序列
int[] lis = new int[n];
int length = 0;
for(int x : indexSeq) {
int posIdx = Arrays.binarySearch(lis, 0,length,x); //在lis数组中,从0到length寻找x
if(posIdx<0) posIdx = -posIdx - 1; //若没找到,计算插入下标
lis[posIdx] = x;
if(posIdx == length) length++; // 表示是当前lis中最大的数字,因此可以扩展序列
//如果不是最大的,直接插入,挤掉了一个比x大的数,让lis变得更小
}
System.out.println(length);
}
}
This post is licensed under CC BY 4.0 by the author.


