递归和动态规划都是将原问题拆成多个子问题然后求解,他们之间最本质的区别是,动态规划保存了子问题的解,避免重复计算。
- 动态规划一般可分为4类:
- 线性动规
- 区域动规
- 树形动规
- 背包动规
动态规划的状态定义和状态转移方程
1.斐波那契数列
1)递归
public static int fib1(int n) {
if (n == 0) {
return 0;
}
if (n == 1) {
return 1;
}
return fib1(n-1) + fib1(n-2);
}
2) 自底向上(记忆化搜索)
int[] mem = null;
public int fib2(int n) {
mem = new int[n+1];
Arrays.fill(mem, -1);
fib122(n);
return mem[n];
}
int fib122(int n){
if (n==0)
return 0;
if (n==1)
return 1;
if (mem[n] == -1){
mem[n] = fib122(n-1) + fib122(n-2);
}
return mem[n];
}
3)动态规划
int fib3(int n) {
int[] mem = new int[n + 1];
mem[0] = 0;
mem[1] = 1;
for (int i = 2; i <= n; i++) {
mem[i] = mem[i - 1] + mem[i - 2];
}
return mem[n];
}
1272
1
0
267914296
267914296
267914296
2.背包问题(NPC)
背包问题(Knapsack problem)是一种组合优化的NP完全(NP-Complete,NPC)问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高
背包问题有以下几种分类:
- 01背包问题
- 完全背包问题
- 多重背包问题
1)01背包问题
在最优解中,每个物品只有两种可能的情况,即在背包中或者不在背包中(背包中的该物品数为0或1),因此称为0-1背包问题
如果采用暴力穷举的方式,每件物品都存在装入和不装入两种情况,所以总的时间复杂度是O(2^N),这是不可接受的。而使用动态规划可以将复杂度降至O(NW)。
dp[i][j]表示将前i件物品装进限重为j的背包可以获得的最大价值, 0<=i<=N, 0<=j<=W
不装入第i件物品,即dp[i−1][j];
装入第i件物品(前提是能装下),即dp[i−1][j−w[i]] + v[i]。
即状态转移方程为
dp[i][j] = max(dp[i−1][j], dp[i−1][j−w[i]]+v[i]) // j >= w[i]
i.记忆化搜索
- 时间复杂度: O(n * C) 其中n为物品个数; C为背包容积
- 空间复杂度: O(n * C)
//记忆搜索
static int[][] mem = null;
static int npc1(int[] w, int[] v, int C) {
int n = w.length;
mem = new int[n][C + 1];
for (int i = 0; i < n; i++) {
Arrays.fill(mem[i], -1);
}
//前n个限重 C的最大价值
return bestValue(w, v, n - 1, C);
}
static int bestValue(int[] w, int[] v, int i, int C) {
if (i < 0 || C <= 0) {
return 0;
}
if (mem[i][C] != -1) {
return mem[i][C];
}
// 如果装不下
int res = 0;
res = bestValue(w, v, i - 1, C);
if (C >= w[i]) {
res = Math.max(res, (bestValue(w, v, i - 1, C - w[i]) + v[i]));
}
return mem[i][C] = res;
}
public static void main(String[] args) {
int[] w = {4, 5, 6, 7, 8};
int[] v = {10, 9, 5, 4, 3};
int maxV = npc1(w, v, 10);
System.out.println("count=" + count);
System.out.println(maxV);
}
结果如下
count=23
19
ii.动态规划
- 时间复杂度: O(n * C) 其中n为物品个数; C为背包容积
- 空间复杂度: O(n * C)
int npc2(int[] w, int[] v, int C) {
int n = w.length;
int[][] mem = new int[n][C + 1];
for (int k = 0; k <= C; k++) {
mem[0][k] = (k >= w[0]) ? v[0] : 0;
}
for (int i = 1; i < n; i++) {
for (int j = 0; j <= C; j++) {
mem[i][j] = mem[i - 1][j];
if (j >= w[i]) {
mem[i][j] = Math.max(mem[i][j], mem[i - 1][j - w[i]] + v[i]);
}
}
}
return mem[n - 1][C];
}
iii.动态规划优化一
优化思路:第i行元素只依赖于第i-1行元素,理论上,只需要保持两行元素即可
/// 动态规划改进: 滚动数组
/// 时间复杂度: O(n * C) 其中n为物品个数; C为背包容积
/// 空间复杂度: O(C), 实际使用了2*C的额外空间
int npc2(int[] w, int[] v, int C) {
int n = w.length;
int[][] mem = new int[n][C + 1];
for (int k = 0; k <= C; k++) {
mem[0][k] = (k >= w[0]) ? v[0] : 0;
}
for (int i = 1; i < n; i++) {
for (int j = 0; j <= C; j++) {
mem[i % 2][j] = mem[(i - 1) % 2][j];
if (j >= w[i]) {
mem[i % 2][j] = Math.max(mem[i % 2][j], mem[(i - 1) % 2][j - w[i]] + v[i]);
}
}
}
return mem[(n - 1) % 2][C];
}
iii.动态规划优化二
/// 动态规划改进
/// 时间复杂度: O(n * C) 其中n为物品个数; C为背包容积
/// 空间复杂度: O(C), 只使用了C的额外空间
int npc3(int[] w, int[] v, int C) {
int n = w.length;
int[] mem = new int[C + 1];
for (int k = 0; k <= C; k++) {
mem[k] = (k >= w[0]) ? v[0] : 0;
}
for (int i = 1; i < n; i++) {
for (int j = C; j >= w[i]; j--) {
mem[j] = Math.max(mem[j], mem[j - w[i]] + v[i]);
}
}
return mem[C];
}
3.背包问题更多变种
- 多重背包问题:每个物品不不⽌止1个,有num(i)个
- 完全背包问题:每个物品可以⽆无限使⽤用
- 多维费⽤用背包问题:要考虑物品的体积和重量量两个维度?
- 物品间加⼊入更更多约束:物品间可以互相排斥;也可以互相依赖
4.最长上升子序列
Longest Increasing Subsequence (LIS)
【Leetcode 300】最长上升子序列
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:
可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
i.你算法的时间复杂度应该为 O(n2)
ii.进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?
int lengthOfLis2(int[] nums) {
int[] mem = new int[nums.length];
Arrays.fill(mem, 1);
for (int i = 1; i < nums.length; i++) {
for (int j = 0; j < i; j++) {
if (nums[j] < nums[i]) {
mem[i] = Math.max(mem[i], mem[j] + 1);
}
}
}
int len = 1;
for (int i = 0; i < mem.length; i++) {
len = Math.max(len, mem[i]);
}
return len;
}
iii.这里思考一个问题:在上面的代码中只求解出了上升子序列的长度,那么如何求出具体的上升子序列呢?
List<List<Integer>> listOfLis2(int[] nums) {
List<List<Integer>> resList = new ArrayList<>();
int[] mem = new int[nums.length];
Arrays.fill(mem, 1);
for (int i = 1; i < nums.length; i++) {
for (int j = 0; j < i; j++) {
count2++;
if (nums[j] < nums[i]) {
mem[i] = Math.max(mem[i], mem[j] + 1);
}
}
}
//最长的递增序列的长度
int len = 1;
for (int i = 0; i < mem.length; i++) {
len = Math.max(len, mem[i]);
}
//递增序列index列表
List<Integer> upList = new ArrayList<>();
for (int i = 0; i < mem.length; i++) {
if (mem[i] == len) {
upList.add(i);
}
}
for (Integer up : upList) {
List<Integer> re = new ArrayList<>();
int maxLen = mem[up];
for (int i = up; i >= 0; i--) {
if (maxLen - mem[i] == 1 || maxLen - mem[i] == 0) {
re.add(nums[i]);
maxLen--;
}
}
Collections.reverse(re);
resList.add(re);
}
return resList;
}
5. 最长公共子序列
Longest Common Sequence (LCS):给出两个字符串S1和S2,求这两个字符串的最长公共子序列的长度
LCS( m , n ) S1[0…m] 和 S2[0…n] 的最长公共子序列的长度
S1[m] == S2[n] :
LCS(m,n) = 1 + LCS(m-1,n-1)
S1[m] != S2[n] :
LCS(m,n) = max( LCS(m-1,n) , LCS(m,n-1) )
static int LCS(String s1, String s2) {
if (s1.length() == 0 || s2.length() == 0) {
return 0;
}
return maxLen(s1, s2, s1.length() - 1, s2.length() - 1);
}
static int maxLen(String s1, String s2, int m, int n) {
if (m == 0 || n == 0) {
return 0;
}
int res = 0;
if (s1.charAt(m) == s2.charAt(n)) {
res = maxLen(s1, s2, m - 1, n - 1) + 1;
} else {
res = Math.max(maxLen(s1, s2, m - 1, n), maxLen(s1, s2, m, n - 1));
}
return res;
}