动态规划(Dynamic Programming)

  • 练习1:找零钱
    • 找零钱 - 暴力递归
    • 找零钱 - 记忆化搜索
    • 找零钱 - 递推
    • 思考题:输出找零钱的具体方案(具体是用了哪些面值的硬币)
    • 找零钱 - 通用实现
  • 动态规划(Dynamic Programming)
    • 动态规划的常规步骤
    • 动态规划的一些概念
    • 有后效性与无后效性
  • 练习2:最大连续子序列和
    • 最大连续子序列和 – 实现
    • 最大连续子序列和 – 优化
  • 练习3:最长上升子序列(LIS)
    • 最长上升子序列 – 实现
    • 最长上升子序列 – 二分搜索实现
  • 练习4 – 最长公共子序列(LCS)
    • 最长公共子序列 – 递归实现
    • 最长公共子序列 – 非递归实现
    • 最长公共子序列 – 滚动数组优化
    • 最长公共子序列 – 一维数组优化
  • 练习5 – 最长公共子串
    • 最长公共子串 – 实现
    • 最长公共子串 – 一维数组优化
  • 练习6 – 0-1背包
    • 0-1背包 – 实现
    • 0-1背包 – 一维数组
    • 0-1背包 – 一维数组优化
    • 0-1背包 – 恰好装满 – 实现

数据结构与算法笔记:恋上数据结构笔记目录

动态规划(Dynamic Programming),简称 DP

  • 是求解最优化问题的一种常用策略

通常的使用套路(一步一步优化):
① 暴力递归(自顶向下,出现了重叠子问题)
② 记忆化搜索(自顶向下
③ 递推(自底向上

直接学习动态规划的概念有点难以理解,先理解透一道例题再学习概念。

练习1:找零钱

leetcode_322_零钱兑换:https://leetcode-cn.com/problems/coin-change/

假设有25分、20分、5分、1分的硬币,现要找给客户41分的零钱,如何办到硬币个数最少

  • 此前用贪心策略得到的并非是最优解(贪心得到的解是 5 枚硬币:25、5、5、5、1)

假设 dp(n)凑到 n 分需要的最少硬币个数

  • 如果第 1 次选择了 25 分的硬币,那么 dp(n) = dp(n - 25) + 1
  • 如果第 1 次选择了 20 分的硬币,那么 dp(n) = dp(n - 20) + 1
  • 如果第 1 次选择了 5 分的硬币,那么 dp(n) = dp(n - 5) + 1
  • 如果第 1 次选择了 1 分的硬币,那么 dp(n) = dp(n - 1) + 1
  • 所以 dp(n) = min { dp(n - 25), dp(n - 20), dp(n - 5), dp(n - 1) } + 1

找零钱 - 暴力递归

类似于斐波那契数列的递归版,会有大量的重复计算,时间复杂度较高。

/*** 暴力递归(自顶向下的调用, 出现了重叠子问题)*/
static int coins(int n) {// 递归基if (n < 1) return Integer.MAX_VALUE;if (n == 1 || n == 5 || n == 20 || n == 25) return 1; // 边界情况// 求出四种取法的最小值int min1 = Math.min(coins(n - 1), coins(n - 5));int min2 = Math.min(coins(n - 20), coins(n - 25));return Math.min(min1, min2) + 1;
}

找零钱 - 记忆化搜索

static int coins(int n) {if (n < 1) return -1; // 处理非法数据int[] dp = new int[n + 1];int[] faces = { 1, 5, 20, 25 }; // 给定的面值数组for (int face : faces) {// 如果我要凑的钱是20元, 那么我肯定用不到25元面值if (face > n) break; // 用不到的面值不用初始化dp[face] = 1; // 初始化可能用到的面值}return coins(n, dp);
}
static int coins(int n, int[] dp) {// 递归基if (n < 1) return Integer.MAX_VALUE;if (dp[n] == 0) { // 记忆化搜索, dp[n] == 0 表示以前没有算过, 那便初始化一下int min1 = Math.min(coins(n - 5, dp), coins(n - 1, dp));int min2 = Math.min(coins(n - 25, dp), coins(n - 20, dp));dp[n] = Math.min(min1, min2) + 1;}return dp[n];
}

找零钱 - 递推

/*** 递推(自底向上)*/
static int coins(int n) {if (n < 1) return -1; // 处理非法数据int[] dp = new int[n + 1];// 自底向上的递推for (int i = 1; i <= n; i++) {int min = Integer.MAX_VALUE;if (i >= 1) min = Math.min(min, dp[i - 1]);if (i >= 5) min = Math.min(min, dp[i - 5]);if (i >= 20) min = Math.min(min, dp[i - 20]);if (i >= 25) min = Math.min(min, dp[i - 25]);dp[i] = min + 1;}return dp[n];
}

可以修改一下写法:

static int coins(int n) {if (n < 1) return -1; // 处理非法数据int[] dp = new int[n + 1];// 递推(自底向上)过程for (int i = 1; i <= n; i++) {int min = dp[i - 1]; // 由于下面两行是必然执行的, 直接这么写就行了// int min = Integer.MAX_VALUE;// if (i >= 1) min = Math.min(min, dp[i - 1]);if (i >= 5) min = Math.min(min, dp[i - 5]);if (i >= 20) min = Math.min(min, dp[i - 20]);if (i >= 25) min = Math.min(min, dp[i - 25]);dp[i] = min + 1;}return dp[n];
}

思考题:输出找零钱的具体方案(具体是用了哪些面值的硬币)

static int coins4(int n) {if (n < 1) return -1; // 处理非法数据int[] dp = new int[n + 1];// faces[i]是凑够i分时最后选择的那枚硬币的面值int[] faces = new int[dp.length]; // 存放硬币面值(为了输出)for (int i = 1; i <= n; i++) {int min = Integer.MAX_VALUE;if (i >= 1 && dp[i - 1] < min) {min = dp[i - 1];faces[i] = 1;}// 上面一步其实必然执行, 可以直接写成下面这样// int min = dp[i - 1];// faces[i] = 1;if (i >= 5 && dp[i - 5] < min) {min = dp[i - 5];faces[i] = 5;}if (i >= 20 && dp[i - 20] < min) {min = dp[i - 20];faces[i] = 20;}if (i >= 25 && dp[i - 25] < min) {min = dp[i - 25];faces[i] = 25;}dp[i] = min + 1;print(faces, i); // 打印凑够面值 1 ~ n 的方案}// print(faces, n); // 打印凑够面值 n 的方案return dp[n];
}
// 打印凑够面值 n 的方案
static void print(int[] faces, int n) {System.out.print("[" + n + "] = ");while (n > 0) {System.out.print(faces[n] + " ");n -= faces[n];}System.out.println();
}

尝试打印了 n = 41 的情况,打印出了凑够 1~41 所有面值的情况:

[1] = 1
[2] = 1 1
[3] = 1 1 1
[4] = 1 1 1 1
[5] = 5
[6] = 1 5
[7] = 1 1 5
[8] = 1 1 1 5
[9] = 1 1 1 1 5
[10] = 5 5
[11] = 1 5 5
[12] = 1 1 5 5
[13] = 1 1 1 5 5
[14] = 1 1 1 1 5 5
[15] = 5 5 5
[16] = 1 5 5 5
[17] = 1 1 5 5 5
[18] = 1 1 1 5 5 5
[19] = 1 1 1 1 5 5 5
[20] = 20
[21] = 1 20
[22] = 1 1 20
[23] = 1 1 1 20
[24] = 1 1 1 1 20
[25] = 25
[26] = 1 25
[27] = 1 1 25
[28] = 1 1 1 25
[29] = 1 1 1 1 25
[30] = 5 25
[31] = 1 5 25
[32] = 1 1 5 25
[33] = 1 1 1 5 25
[34] = 1 1 1 1 5 25
[35] = 5 5 25
[36] = 1 5 5 25
[37] = 1 1 5 5 25
[38] = 1 1 1 5 5 25
[39] = 1 1 1 1 5 5 25
[40] = 20 20
[41] = 1 20 20
3

找零钱 - 通用实现

public static void main(String[] args) {System.out.println(coins5(41, new int[]{1, 5, 20, 25})); // 3
}static int coins(int n, int[] faces) {if (n < 1 || faces == null || faces.length == 0 ) return -1;int[] dp = new int [n + 1];for (int i = 1; i <= n; i++) {int min = Integer.MAX_VALUE;for (int face : faces) {// 假如给我的面值是20, 要凑的是15, 则跳过此轮循环if (face > i) continue; // 如果给我的面值比我要凑的面值还大, 跳过此轮循环min = Math.min(dp[i - face], min);}dp[i] = min + 1;}return dp[n];
}

改进,如果不能凑成则返回 -1:

static int coins5(int n, int[] faces) {if (n < 1 || faces == null || faces.length == 0 ) return -1;int[] dp = new int [n + 1];for (int i = 1; i <= n; i++) {int min = Integer.MAX_VALUE;for (int face : faces) {// 假如给我的面值是20, 要凑的是15, 则跳过此轮循环if (face > i) continue; // 如果给我的面值比我要凑的面值还大, 跳过此轮循环// 比如给的面值是{4}, 要凑的是6, 先给出一张4, 再看6-4=2, 是否能凑成// 2无法凑成, 则跳过此轮循环   int v = dp[i - face];if (v < 0 || v >= min) continue;min = v;}// 说明上面的循环中每次都是continue, 要凑的面值比给定的所有面值小if (min == Integer.MAX_VALUE) {dp[i] = -1;} else {dp[i] = min + 1;}}return dp[n];
}

动态规划(Dynamic Programming)

动态规划的常规步骤

动态规划,简称 DP

  • 是求解最优化问题的一种常用策略

通常的使用套路(一步一步优化):
① 暴力递归(自顶向下,出现了重叠子问题)
② 记忆化搜索(自顶向下
③ 递推(自底向上

动态规划中的 “动态” 可以理解为是 “会变化的状态”

  • 定义状态状态是原问题、子问题的解
    比如定义 dp(i)dp(i)dp(i) 的含义
  • 设置初始状态边界
    比如设置 dp(0)dp(0)dp(0) 的值
  • 确定状态转移方程
    比如确定 dp(i)dp(i)dp(i) 和 dp(i−1)dp(i - 1)dp(i−1) 的关系

动态规划的一些概念

维基百科的解释

Dynamic Programming is a method for solving a complex problem by breaking it down into a collection of simpler subproblems, solving each of those subproblems just once, and storing their solutions.

① 将复杂的原问题拆解成若干个简单的子问题
② 每个子问题仅仅解决1次,并保存它们的解
③ 最后推导出原问题的解

可以用动态规划来解决的问题,通常具备2个特点:

  • 最优子结构最优化原理):通过求解子问题的最优解,可以获得原问题的最优解
  • 无后效性
    某阶段的状态一旦确定,则此后过程的演变不再受此前各状态及决策的影响(未来与过去无关
    在推导后面阶段的状态时,只关心前面阶段的具体状态值,不关心这个状态是怎么一步步推导出来的

有后效性与无后效性

首先了解一下什么是有后效性

然后再去理解什么是无后效性

练习2:最大连续子序列和

题目:给定一个长度为 n 的整数序列,求它的最大连续子序列和

状态定义:假设 dp(i)dp(i)dp(i) 是以 nums[i] 结尾的最大连续子序列和(nums 是整个序列)

  • 比如 -2、1、-3、4、-1、2、1、-5、4 的最大连续子序列和是 4 + (-1) + 2 + 1 = 6;
    nums[0] -2 结尾的最大连续子序列是 -2,所以 dp(0)=−2dp(0) = -2dp(0)=−2
    nums[1] 1 结尾的最大连续子序列是 1,所以 dp(1)=1dp(1) = 1dp(1)=1
    nums[2] -3 结尾的最大连续子序列是 1、-3,所以 dp(2)=dp(1)+(−3)=−2dp(2) = dp(1) + (-3) = -2dp(2)=dp(1)+(−3)=−2
    nums[3] 4 结尾的最大连续子序列是 4,所以 dp(3)=4dp(3) = 4dp(3)=4
    nums[4] -1 结尾的最大连续子序列是 4、-1,所以 dp(4)=dp(3)+(−1)=3dp(4) = dp(3) + (-1) = 3dp(4)=dp(3)+(−1)=3
    nums[5] 2 结尾的最大连续子序列是 4、-1、2,所以 dp(5)=dp(4)+2=5dp(5) = dp(4) + 2 = 5dp(5)=dp(4)+2=5
    nums[6] 1 结尾的最大连续子序列是 4、-1、2、1,所以 dp(6)=dp(5)+1=6dp(6) = dp(5) + 1 = 6dp(6)=dp(5)+1=6
    nums[7] -5 结尾的最大连续子序列是 4、-1、2、1、-5,所以 dp(7)=dp(6)+(−5)=1dp(7) = dp(6) + (-5) = 1dp(7)=dp(6)+(−5)=1
    nums[8] 4 结尾的最大连续子序列是 4、-1、2、1、-5、4,所以 dp(8)=dp(7)+4=5dp(8) = dp(7) + 4 = 5dp(8)=dp(7)+4=5

状态转移方程和初始状态
状态转移方程

  • 如果 dp(i–1)≤0dp(i – 1) ≤ 0dp(i–1)≤0,那么 dp(i)=nums[i]dp(i) = nums[i]dp(i)=nums[i]
  • 如果 dp(i–1)>0dp(i – 1) > 0dp(i–1)>0,那么 dp(i)=dp(i–1)+nums[i]dp(i) = dp(i – 1) + nums[i]dp(i)=dp(i–1)+nums[i]

初始状态

  • dp(0)dp(0)dp(0) 的值是 nums[0]nums[0]nums[0]

最终的解

  • 最大连续子序列和是所有 dp(i)dp(i)dp(i) 中的最大值 max{dp(i)},i∈[0,nums.length)max \{ dp(i) \},i ∈ [0, nums.length)max{dp(i)},i∈[0,nums.length)

最大连续子序列和 – 实现

static int maxSubArray(int[] nums) {if (nums == null || nums.length == 0) return 0;int[] dp = new int[nums.length];int max = dp[0] = nums[0];for (int i = 1; i < dp.length; i++) {if (dp[i - 1] > 0) {dp[i] = dp[i - 1] + nums[i];} else {dp[i] = nums[i];}max = Math.max(max, dp[i]);}return max;
}

最大连续子序列和 – 优化

static int maxSubArray(int[] nums) {if (nums == null || nums.length == 0) return 0;int dp = nums[0];int max = dp;for (int i = 1; i < nums.length; i++) {if (dp > 0) {dp = dp + nums[i];} else {dp = nums[i];}max = Math.max(max, dp);}return max;
}

练习3:最长上升子序列(LIS)

最长上升子序列最长递增子序列,Longest Increasing Subsequence,LIS)

leetcode_300_最长上升子序列: https://leetcode-cn.com/problems/longest-increasing-subsequence/

题目:给定一个无序的整数序列,求出它最长上升子序列的长度(要求严格上升)

  • 比如 [10, 2, 2, 5, 1, 7, 101, 18] 的最长上升子序列是 [2, 5, 7, 101]、[2, 5, 7, 18],长度是 4

假设数组是 nums, [10, 2, 2, 5, 1, 7, 101, 18]

  • dp(i)dp(i)dp(i) 是以 nums[i]nums[i]nums[i] 结尾的最长上升子序列的长度,i∈[0,nums.length)i ∈ [0, nums.length)i∈[0,nums.length)
    nums[0] 10 结尾的最长上升子序列是 10,所以 dp(0)=1dp(0) = 1dp(0)=1
    nums[1] 2 结尾的最长上升子序列是 2,所以 dp(1)=1dp(1) = 1dp(1)=1
    nums[2] 2 结尾的最长上升子序列是 2,所以 dp(2)=1dp(2) = 1dp(2)=1
    nums[3] 5 结尾的最长上升子序列是 25,所以 dp(3)=dp(1)+1=dp(2)+1=2dp(3) = dp(1) + 1 = dp(2) + 1 = 2dp(3)=dp(1)+1=dp(2)+1=2
    nums[4] 1 结尾的最长上升子序列是 1,所以 dp(4)=1dp(4) = 1dp(4)=1
    nums[5] 7 结尾的最长上升子序列是 257,所以 dp(5)=dp(3)+1=3dp(5) = dp(3) + 1 = 3dp(5)=dp(3)+1=3
    nums[6] 101 结尾的最长上升子序列是 257101,所以 dp(6)=dp(5)+1=4dp(6) = dp(5) + 1 = 4dp(6)=dp(5)+1=4
    nums[7] 18 结尾的最长上升子序列是 25718,所以 dp(7)=dp(5)+1=4dp(7) = dp(5) + 1 = 4dp(7)=dp(5)+1=4

状态方程

状态的初始值

  • dp(0)=1dp(0) = 1dp(0)=1
  • 所有的 dp(i)dp(i)dp(i) 默认都初始化为 1

最终的解

  • 最长上升子序列的长度是所有 dp(i)dp(i)dp(i) 中的最大值 max{dp(i)},i∈[0,nums.length)max \{ dp(i) \},i ∈ [0, nums.length)max{dp(i)},i∈[0,nums.length)

最长上升子序列 – 实现

时间复杂度:O(n2),空间复杂度:O(n)

static int lengthOfLIS(int[] nums) {if (nums == null || nums.length == 0) return 0;int[] dp = new int[nums.length];int max = dp[0] = 1; // 只有一个元素则长度为1for (int i = 1; i < dp.length; i++) {dp[i] = 1; // 默认只有一个元素时长度为1for (int j = 0; j < i; j++) {// 无法成为一个上升子序列if (nums[j] >= nums[i]) continue;dp[i] = Math.max(dp[j] + 1, dp[i]);}max = Math.max(dp[i], max);}return max;}

最长上升子序列 – 二分搜索实现



普通实现(非二分搜索):

static int lengthOfLIS(int[] nums) {if (nums == null || nums.length == 0) return 0;// 牌堆的数量int len = 0;// 牌顶数组int[] top = new int[nums.length];// 遍历所有的牌for (int num : nums) {int j = 0;while (j < len) {// 找到一个>=nums的牌顶if (top[j] >= num) {top[j] = num;break;}// 牌顶 < numsj++;}if (j == len) { // 新建一个牌堆len++;top[j] = num;}}return len;
}

二分搜索实现:

static int lengthOfLIS(int[] nums) {if (nums == null || nums.length == 0) return 0;// 牌堆的数量int len = 0;// 牌顶数组int[] top = new int[nums.length];// 遍历所有的牌(二分搜索)for (int num : nums) {int begin = 0;int end = len;while (begin < end) {int mid = (begin + end) >> 1;if (num <= top[mid]) {end = mid;} else {begin = mid + 1;}}// 覆盖牌顶top[begin] = num;// 检查是否要新建一个牌堆if (begin == len) len++;}return len;
}

练习4 – 最长公共子序列(LCS)

最长公共子序列(Longest Common Subsequence,LCS)

leetcode_1143_最长公共子序列:https://leetcode-cn.com/problems/longest-common-subsequence/

题目:求两个序列的最长公共子序列长度

  • [1, 3, 5, 9, 10][1, 4, 9, 10] 的最长公共子序列是 [1, 9, 10],长度为 3
  • ABCBDABBDCABA 的最长公共子序列长度是 4,可能是
    ABCBDAB 和 BDCABA > BDAB
    ABCBDAB 和 BDCABA > BDAB
    ABCBDAB 和 BDCABA > BCAB
    ABCBDAB 和 BDCABA > BCBA

思路

最长公共子序列 – 递归实现

  • 空间复杂度:O(k) , k = min{n, m},n、m 是 2 个序列的长度
  • 时间复杂度:O(2n) ,当 n = m 时
/*** 递归实现*/
static int lcs(int[] nums1, int[] nums2) {if (nums1 == null || nums1.length == 0) return 0; // 检测非法数据if (nums2 == null || nums2.length == 0) return 0; // 检测非法数据return lcs(nums1, nums1.length, nums2, nums2.length);
}
/*** 求nums1前i个元素和nums2前j个元素的最长g公共子序列长度* @param nums1* @param i* @param nums2* @param j*/
static int lcs(int[] nums1, int i, int[] nums2, int j) {if (i == 0 || j == 0) return 0;// 最后一个元素相等, 返回前面的公共子序列长度 + 1if (nums1[i - 1] == nums2[j - 1]) {return lcs(nums1, i - 1, nums2, j - 1) + 1;}return Math.max(lcs(nums1, i - 1, nums2, j), lcs(nums1, i, nums2, j - 1));
}

最长公共子序列 – 非递归实现

  • 空间复杂度:O(n ∗ m)
  • 时间复杂度:O(n ∗ m)
static int lcs(int[] nums1, int[] nums2) {if (nums1 == null || nums1.length == 0) return 0;if (nums2 == null || nums2.length == 0) return 0;int[][] dp = new int[nums1.length + 1][nums2.length + 1];for (int i = 1; i <= nums1.length; i++) {for (int j = 1; j <= nums2.length; j++) {if (nums1[i - 1] == nums2[j - 1]) {dp[i][j] = dp[i - 1][j - 1] + 1;} else {dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);}}}return dp[nums1.length][nums2.length];
}

最长公共子序列 – 滚动数组优化

可以使用滚动数组化空间复杂度

/*** 非递归实现(滚动数组优化)*/
static int lcs(int[] nums1, int[] nums2) {if (nums1 == null || nums1.length == 0) return 0;if (nums2 == null || nums2.length == 0) return 0;int[][] dp = new int[2][nums2.length + 1];for (int i = 1; i <= nums1.length; i++) {int row = i & 1;int prevRow = (i - 1) & 1;for (int j = 1; j <= nums2.length; j++) {if (nums1[i - 1] == nums2[j - 1]) {dp[row][j] = dp[prevRow][j - 1] + 1;} else {dp[row][j] = Math.max(dp[prevRow][j], dp[row][j - 1]);}}}return dp[nums1.length & 1][nums2.length];
}

最长公共子序列 – 一维数组优化


练习5 – 最长公共子串

最长公共子串(Longest Common Substring)

  • 子串是连续的子序列

题目:求两个字符串的最长公共子串长度

  • ABCBA 和 BABCA 的最长公共子串是 ABC,长度为 3

假设 2 个字符串分别是 str1、str2:

  • i ∈ [1, str1.length]
  • j ∈ [1, str2.length]

假设 dp(i,j)dp(i, j)dp(i,j) 是以 str1[i – 1]str2[j – 1] 结尾的最长公共子串长度:

  • dp(i,0)dp(i, 0)dp(i,0)、dp(0,j)dp(0, j)dp(0,j) 初始值均为 0
  • 如果 str1[i – 1] = str2[j – 1],那么 dp(i,j)=dp(i–1,j–1)+1dp(i, j) = dp(i – 1, j – 1) + 1dp(i,j)=dp(i–1,j–1)+1
  • 如果 str1[i – 1]str2[j – 1],那么 dp(i,j)=0dp(i, j) = 0dp(i,j)=0

最长公共子串 – 实现

  • 空间复杂度:O(n ∗ m)
  • 时间复杂度:O(n ∗ m)
 static int lcs(String str1, String str2) {if (str1 == null || str2 == null) return 0;char[] chars1 = str1.toCharArray();if (chars1.length == 0) return 0;char[] chars2 = str2.toCharArray();if (chars2.length == 0) return 0;int [][] dp = new int [chars1.length + 1][chars2.length + 1];int max = 0;for (int i = 1; i <= chars1.length; i++) {for (int j = 1; j <= chars2.length; j++) {if (chars1[i - 1] != chars2[j - 1]) continue;dp[i][j] = dp[i -1][j - 1] + 1;max = Math.max(max, dp[i][j]);}}return max;}

最长公共子串 – 一维数组优化

  • 空间复杂度:O(k) , k = min{n, m}
  • 时间复杂度:O(n ∗ m)
static int lcs(String str1, String str2) {if (str1 == null || str2 == null) return 0;char[] chars1 = str1.toCharArray();if (chars1.length == 0) return 0;char[] chars2 = str2.toCharArray();if (chars2.length == 0) return 0;// 选取长度较短的作为列char[] rowsChars = chars1, colsChars = chars2;if (chars1.length < chars2.length) {colsChars = chars1;rowsChars = chars2;}int[] dp = new int[colsChars.length + 1];int max = 0;for (int row = 1; row <= rowsChars.length; row++) {int cur = 0;for (int col = 1; col <= colsChars.length; col++) {int leftTop = cur;cur = dp[col];if (chars1[row - 1] != chars2[col - 1]) {dp[col] = 0;} else {dp[col] = leftTop + 1;max = Math.max(max, dp[col]);}}}return max;
}

我觉得可以修改成这样:

static int lcs(String str1, String str2) {if (str1 == null || str2 == null) return 0;char[] chars1 = str1.toCharArray();if (chars1.length == 0) return 0;char[] chars2 = str2.toCharArray();if (chars2.length == 0) return 0;// 选取长度较短的作为列int rowsLength = chars1.length;int colsLength = chars2.length;if (chars1.length < chars2.length) {colsLength = chars1.length;rowsLength = chars2.length;}int[] dp = new int[colsLength + 1];int max = 0;for (int row = 1; row <= rowsLength; row++) {int cur = 0;for (int col = 1; col <= colsLength; col++) {int leftTop = cur;cur = dp[col];if (chars1[row - 1] != chars2[col - 1]) {dp[col] = 0;} else {dp[col] = leftTop + 1;max = Math.max(max, dp[col]);}}}return max;
}

练习6 – 0-1背包

0-1背包 – 实现

static int maxValue(int[] values, int[] weights, int capacity) {// 检测非法输入if (values == null || values.length == 0) return 0;if (weights == null || weights.length == 0) return 0;if (weights.length != values.length) return 0;if (capacity <= 0) return 0;// 特征方程: dp(i, j) 是最大承重为 j、有前 i 件物品可选时的最大总价值;int[][] dp = new int[values.length + 1][capacity + 1];// dp 初始化的值默认是0,Java中数组默认初始值即为0for (int i = 1; i <= values.length; i++) {for (int j = 1; j <= capacity; j++){if (weights[i - 1] > j) { // 如果本次挑选的物品重量 > 承重, 则不装入该物品dp[i][j] = dp[i - 1][j];} else { // 本次挑选的物品重量 <= 承重, 可以选择装入// 比较【装】与【不装】分别获得的最大总价值来选择dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weights[i - 1]] + values[i - 1]);}}}return dp[values.length][capacity];
}
public static void main(String[] args) {int[] values = { 6, 3, 5, 4, 6 };int[] weights = { 2, 2, 6, 5, 4 };int capacity = 10;System.out.println(maxValue1(values, weights, capacity));// i = 5// j = 10// 如果不选择第i个物品, dp(i, j) = dp(i - 1, j)// 如果选择第i个物品, dp(i, j) = values[i] + dp(i - 1, j - weights[i])// dp(i, j) = max {dp(i - 1, j), valus[i] + dp(i - 1, j - weights[i])}
}

0-1背包 – 一维数组

/*** 一维数组*/
static int maxValue(int[] values, int[] weights, int capacity) {if (values == null || values.length == 0) return 0;if (weights == null || weights.length == 0) return 0;if (weights.length != values.length) return 0;if (capacity <= 0) return 0;int[] dp = new int[capacity + 1];for (int i = 1; i <= values.length; i++) {for (int j = capacity; j >= 1; j--) {if (j < weights[i - 1]) continue;dp[j] = Math.max(dp[j], dp[j - weights[i - 1]] + values[i - 1]);}}return dp[capacity];
}

0-1背包 – 一维数组优化

/*** 一维数组优化*/
static int maxValue(int[] values, int[] weights, int capacity) {// 检测非法数据if (values == null || values.length == 0) return 0;if (weights == null || weights.length == 0) return 0;if (weights.length != values.length) return 0;if (capacity <= 0) return 0;int[] dp = new int[capacity + 1];for (int i = 1; i <= values.length; i++) {for (int j = capacity; j >= weights[i - 1]; j--) {dp[j] = Math.max(dp[j], dp[j - weights[i - 1]] + values[i - 1]);}}return dp[capacity];
}

0-1背包 – 恰好装满 – 实现

/*** 恰好装满*/
static int maxValue4(int[] values, int[] weights, int capacity) {// 检测非法数据if (values == null || values.length == 0) return 0;if (weights == null || weights.length == 0) return 0;if (weights.length != values.length) return 0;if (capacity <= 0) return 0;int[] dp = new int[capacity + 1];for (int j = 1; j <= capacity; j++) {dp[j] = Integer.MIN_VALUE;}for (int i = 1; i <= values.length; i++) {for (int j = capacity; j >= weights[i - 1]; j--) {dp[j] = Math.max(dp[j], dp[j - weights[i - 1]] + values[i - 1]);}}return dp[capacity] < 0 ? -1 : dp[capacity];
}

【恋上数据结构】动态规划(找零钱、最大连续子序列和、最长上升子序列、最长公共子序列、最长公共子串、0-1背包)相关推荐

  1. 【恋上数据结构】贪心(最优装载、零钱兑换、0-1背包)、分治(最大连续子序列和、大数乘法)

    贪心.分治 贪心(Greedy) 问题1:最优装载(加勒比海盗) 问题2:零钱兑换 零钱兑换的另一个例子 贪心注意点 问题3:0-1背包 0-1 背包 - 实例 一些习题 分治(Divide And ...

  2. MJ恋上数据结构(第1季 + 第2季)笔记

    文章转载自:https://blog.csdn.net/weixin_43734095/article/details/104847976 恋上数据结构完整笔记(第1季 + 第2季) 前言 数据结构 ...

  3. 2021-10-15 红黑树 概念和平衡操作理解以及与AVL对比分析 恋上数据结构笔记

    文章目录 红黑树的由来 红黑树需要遵守的五大规则 红黑树与4阶B树的相互转换!! 红黑树的插入(12种情况) 红黑树的删除(3大类情况) 红黑树的平衡 以及与AVL树的性能比较 红黑树的由来 红黑树: ...

  4. 《恋上数据结构第1季》单向链表、双向链表

    链表(Linked List) 链表的接口设计 单向链表(SingleLinkedList) 获取元素 – get() 清空元素 – clear() 添加元素 – add(int index, E e ...

  5. 《恋上数据结构第1季》动态扩容数组原理及实现

    动态扩容数组 什么是数据结构? 线性表 数组(Array) 动态数组(Dynamic Array) 动态数组接口设计 清除所有元素 - clear() 添加元素 - add(E element).ad ...

  6. 基数排序及其思想 C++代码实现及分析 恋上数据结构笔记

    文章目录 复习梗概 算法思想 时间及空间复杂度 基数排序基础版代码 及输出结果 计数排序函数 基数排序函数 可视化输出 另一种思路 完整版代码 复习梗概 思想 如何取数字各个位位数 计数排序保证稳定性 ...

  7. 快速排序 C++代码实现及其算法思想及时间复杂度分析及优化 恋上数据结构笔记

    文章目录 复习梗概 算法思想 算法复杂度分析及稳定性 如何优化? 快速排序改进版代码C++ 快速排序个人青春版代码 完整代码 复习梗概 算法思想,别的排序名字直接就能让人联想到它的算法思想,唯独快速排 ...

  8. 归并排序算法 C++实现与时间复杂度(考过)恋上数据结构笔记

    复习梗概 画图,自己整个数组,看代码写步骤,这个对理解归并排序还是很有必要的 合并两个有序数组的merge函数写法 时间复杂度的分析方法!!! 其实我觉得去b站找个动态的步骤分解视频也是不错的复习方法 ...

  9. 插入排序算法 及其二分搜索优化版 C++代码实现 恋上数据结构笔记

    复习梗概 文章目录 复习梗概 插入排序算法思想 插入排序时间复杂度与特性(多少,与什么有关?) 插入排序基础版 插入排序2nd优化版(优化了哪里?) !!!插入排序二分搜索优化版(优化了哪里?如何优化 ...

最新文章

  1. jar中的类文件更新遇到的问题:请分析下原因呢
  2. code vs 把所有行拼接成一行_关于SQL Server将一列的多行内容拼接成一行的问题讨论...
  3. 控件setVisible为false会导致控件被移除
  4. 第一次作业之成员介绍
  5. win10计算机屏幕暗怎么办,Win10系统调节不了屏幕亮度怎么办
  6. gm怎么刷东西 rust_网游GM被玩家暴打,无奈为自己特制无敌BUFF,技能介绍格外嘚瑟...
  7. amp mysql升级_【简单的案例分享,停机10分钟】10204升级CRSamp;amp;DB的PSU至1
  8. Kubernetes系列之理解K8s Service的几种模式
  9. 2010 LinuxQuestions 开源软件获奖名单
  10. Cloudflare泄露客户数据,IT部门可从中吸取什么教训?
  11. 38. Linux 备份
  12. Ubuntu18.04安装COMSOL 5.3a
  13. CSDN 创始人蒋涛:开源吞噬世界,得开发者得天下
  14. 幼儿课外活动游戏_这15个户外游戏,让你组织活动收放自如
  15. ADS1256驱动程序
  16. java.util.Date中的loe_Java-学习日记(日期的转换与处理)
  17. python三维建模和cad比较_对比Revit和CAD三维建模的不同
  18. VS Code 的常用快捷键和插件(一)
  19. java8 —— Stream( 流 )
  20. 线程经典实例——吃苹果问题

热门文章

  1. 社区团购战国七雄出场了
  2. 布尔运算,二进制和门电路
  3. 如果买一辆二手的劳斯莱斯在三线城市跑婚车,多久能回本?
  4. 【leveldb】资料
  5. Qt——P1 创建第一个Qt程序
  6. python将时间戳转化为标准时间格式的方法
  7. linux sd卡 分区变大,Linux 动态调整分区大小
  8. sql stuff 函数_SQL STUFF函数概述
  9. angular 的进一步深入理解
  10. SPSS统计基础-均值功能的使用