本文转载于GitHub地址:https://github.com/CyC2018/CS-Notes/,仅作为个人日后复习。整合了多方面的资料,侵删。
动态规划是运筹学中用于求解决策过程中的最优化数学方法。作为算法设计技术,是一种使用多阶段决策过程最优的通用方法。是解决最优化问题的重要工具。
动态规划的特性:
动态规划的本质,是对问题状态的定义和状态转移方程的定义
2.找到状态转移方程
动态规划三要素:
问题阶段
每个阶段的状态
一个阶段如何进入下一个阶段的递推关系式
多阶段决策问题
初始状态\rightarrow→决策1\rightarrow→ 决策2 \rightarrow→........\rightarrow→决策n\rightarrow→结束状态
1)问题是由交叠的子问题所构成,大问题分解为小问题。
2)将交叠子问题的一次次求解\rightarrow→子问题只求解一次,并将结果记录保存。
3)利用空间(子问题存储)来换取时间
动态规划的的四个解题步骤是:
定义子问题
写出子问题的递推关系
确定 DP 数组的计算顺序
空间优化(可选)
这个解题步骤适用于任何一道动态规划题目,能够让你很快理清解题的各个要点。
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
示例:
输入: [1,2,3,1]
输出: 4
解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。偷窃到的最高金额 = 1 + 3 = 4 。
稍微接触过一点动态规划的朋友都知道动态规划有一个「子问题」的定义。什么是子问题?子问题是和原问题相似,但规模较小的问题。例如这道打家劫舍问题,原问题是「从全部房子中能偷到的最大金额」,将问题的规模缩小,子问题就是「从 k 个房子中能偷到的最大金额」,用 f(k) 表示。
打家劫舍问题的子问题定义
可以看到,子问题是参数化的,我们定义的子问题中有参数n 。假设一共有 n 个房子的话,就一共有 n 个子问题。动态规划实际上就是通过求这一堆子问题的解,来求出原问题的解。这要求子问题需要具备两个性质:
原问题要能由子问题表示。例如这道题中,k==n 时实际上就是原问题。否则,解了半天子问题还是解不出原问题,那子问题岂不是白解了。
一个子问题的解要能通过其他子问题的解求出。例如这道题中,f(k) 可以由 f(k-1) 和 f(k-2) 求出,具体原理后面会解释。这个性质就是教科书中所说的「最优子结构」。如果定义不出这样的子问题,那么这道题实际上没法用动态规划解。
打家劫舍问题由于比较简单,定义子问题实际上是很直观的。一些比较难的动态规划题目可能需要一些定义子问题的技巧。
这一步是求解动态规划问题最关键的一步。然而,这一步也是最无法在代码中体现出来的一步。在做题的时候,最好把这一步的思路用注释的形式写下来。做动态规划题目不要求快,而要确保无误。否则,写代码五分钟,找 bug 半小时,岂不美哉?
我们来分析一下这道题的递推关系:
假设一共有 n 个房子,每个房子的金额分别是H0,H1,.....,Hn-1 ,子问题 f(k) 表示偷前 k 个房子(即H0,H1,....,Hk-1 )中能偷到的最大金额。那么,偷 k 个房子有两种偷法:
子问题的递推关系
k 个房子中最后一个房子是Hk-1 。如果不偷这个房子,那么问题就变成在前 k-1 个房子中偷到最大的金额,也就是子问题 f(k-1)。如果偷这个房子,那么前一个房子 Hk-2 显然不能偷,其他房子不受影响。那么问题就变成在前 k-2 个房子中偷到的最大的金额。两种情况中,选择金额较大的一种结果。
另外,我们在写递推关系的时候,还要注意递推关系的 base case。这样才能构成完整的递推关系,后面写代码也不容易在边界条件上出错。在这道题中,是 k==0 和 k==1 时的子问题:
当 k==0 时,没有房子,所以f(0) = 0 。
当 k==1 时,只有一个房子,偷这个房子即可,所以 f(1) = H0。
在确定了子问题的递推关系之后,下一步就是依次计算出这些子问题了。在很多教程中都会写,动态规划有两种计算顺序,一种是自顶向下的、使用备忘录的递归方法,一种是自底向上的、使用 DP 数组的循环方法。不过在普通的动态规划题目中,99% 的情况我们用循环方法就很好解决,所以我们最好在一开始就坚持自底向上方法,使用 DP 数组,这样才有助于领悟动态规划的真正精髓。
DP 数组也可以叫「子问题数组」,因为 DP 数组中的每一个元素都对应一个子问题。如下图所示,dp[k]
对应子问题 f(k),即偷前 k 间房子的最大金额。
DP 数组与子问题的对应关系
那么,只要搞清楚了子问题的计算顺序,就可以确定 DP 数组的计算顺序。对于打家劫舍问题,我们分析子问题的依赖关系,发现每个 f(k) 依赖 f(k-1) 和 f(k-2)。也就是说,dp[k]
依赖 dp[k-1]
和 dp[k-2]
,如下图所示。
DP 数组的依赖顺序
那么,既然 DP 数组中的依赖关系都是向右指的,DP 数组的计算顺序就是从左向右。这样我们可以保证,计算一个子问题的时候,它所依赖的那些子问题已经计算出来了。
确定了 DP 数组的计算顺序之后,我们就可以写出题解代码了:
public int rob(int[] nums) {
if (nums.length == 0) {
return 0;
}
// 子问题:
// f(k) = 偷 [0..k) 房间中的最大金额
// f(0) = 0
// f(1) = nums[0]
// f(k) = max{ rob(k-1), nums[k-1] + rob(k-2) }
int N = nums.length;
int[] dp = new int[N+1];
dp[0] = 0;
dp[1] = nums[0];
for (int k = 2; k <= N; k++) {
// 套用子问题的递推关系
dp[k] = Math.max(dp[k-1], nums[k-1] + dp[k-2]);
}
return dp[N];
}
注意在上面的代码中,有一大段关于子问题定义、递推关系的公式。在面试的时候,我们一般需要先把子问题的定义、递推关系给面试官解释清楚,同时顺手把这些内容用注释的形式写下来。这样一来让自己写代码不容易出错,二来也能让面试官更好地理解自己的思路。
空间优化是动态规划问题的进阶内容了。对于初学者来说,可以不掌握这部分内容,写出来的代码无非是空间复杂度高一些。不过,打家劫舍问题同样是空间优化的一个很好的例子,所以你不妨了解一下。
让我们回顾一下在编程入门阶段学习的斐波那契数列的求解方法。斐波那契数列的递推关系是f(n) = f(n-1)+f(n-2)。这是不是和打家劫舍问题一模一样?计算斐波那契数列的时候,我们只需要用到三个变量,并不需要什么 DP 数组:
// 计算斐波那契数列
// f(1) = f(2) = 1, f(n) = f(n-1) + f(n-2)
int fib(int n) {
if (n <= 2) {
return 1;
}
int a = 1;
int b = 1;
for (int i = 2; i < n; i++) {
int c = a + b;
a = b;
b = c;
}
return b;
}
请记住这段代码,这段我们很早就会的代码里,其实蕴含了打家劫舍类动态规划问题的空间优化技巧。
空间优化的基本原理是,很多时候我们并不需要始终持有全部的 DP 数组。对于打家劫舍问题,我们发现,最后一步计算 f(n) 的时候,实际上只用到了 f(n-1) 和 f(n-2) 的结果。n-3 之前的子问题,实际上早就已经用不到了。
那么,我们可以只用两个变量保存两个子问题的结果,就可以依次计算出所有的子问题。这样一来,空间复杂度也从 O(n) 降到了O(1) 。下面的动图比较了空间优化前和优化后的对比关系:
空间优化前后对比(动图)
优化后的代码如下所示:
public int rob(int[] nums) {
int prev = 0;
int curr = 0;
// 每次循环,计算「偷到当前房子为止的最大金额」
for (int i : nums) {
// 循环开始时,curr 表示 dp[k-1],prev 表示 dp[k-2]
// dp[k] = max{ dp[k-1], dp[k-2] + i }
int temp = Math.max(curr, prev + i);
prev = curr;
curr = temp;
// 循环结束时,curr 表示 dp[k],prev 表示 dp[k-1]
}
return curr;
}
可以看到,我们只用到了三个变量:prev
、curr
和 temp
。在每一轮循环中,prev
都表示 dp[k-2]
,curr
都表示 dp[k-1]
,而 temp
负责计算出 dp[k]
的值,再往前循环复制。这三个变量的操作方式,和斐波那契数列中的三个变量一模一样。
如果你是 Python 选手的话,还可以利用多重赋值的语法,去掉 temp
变量,在循环里只用一行代码:
def rob(self, nums: List[int]) -> int:
prev = 0
curr = 0
# 每次循环,计算「偷到当前房子为止的最大金额」
for i in nums:
# 循环开始时,curr 表示 dp[k-1],prev 表示 dp[k-2]
# dp[k] = max{ dp[k-1], dp[k-2] + i }
prev, curr = curr, max(curr, prev + i)
# 循环结束时,curr 表示 dp[k],prev 表示 dp[k-1]
return curr
题目描述:有 N 阶楼梯,每次可以上一阶或者两阶,求有多少种上楼梯的方法。
定义一个数组 dp 存储上楼梯的方法数(为了方便讨论,数组下标从 1 开始),dp[i] 表示走到第 i 个楼梯的方法数目。
第 i 个楼梯可以从第 i-1 和 i-2 个楼梯再走一步到达,走到第 i 个楼梯的方法数为走到第 i-1 和第 i-2 个楼梯的方法数之和。
考虑到 dp[i] 只与 dp[i - 1] 和 dp[i - 2] 有关,因此可以只用两个变量来存储 dp[i - 1] 和 dp[i - 2],使得原来的 O(N) 空间复杂度优化为 O(1) 复杂度。
代码实现:
public int climbStairs(int n) {
if (n <= 2) {
return n;
}
int pre2 = 1, pre1 = 2;
for (int i = 2; i < n; i++) {
int cur = pre1 + pre2;
pre2 = pre1;
pre1 = cur;
}
return pre1;
}
题目描述:抢劫一排住户,但是不能抢邻近的住户,求最大抢劫量。
定义 dp 数组用来存储最大的抢劫量,其中 dp[i] 表示抢到第 i 个住户时的最大抢劫量。
由于不能抢劫邻近住户,如果抢劫了第 i -1 个住户,那么就不能再抢劫第 i 个住户,所以
代码实现:
public int rob(int[] nums) {
int pre2 = 0, pre1 = 0;
for (int i = 0; i < nums.length; i++) {
int cur = Math.max(pre2 + nums[i], pre1);
pre2 = pre1;
pre1 = cur;
}
return pre1;
}
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
示例 1:
输入: [2,3,2]
输出: 3
解释: 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:
输入: [1,2,3,1]
输出: 4
解释: 你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。偷窃到的最高金额 = 1 + 3 = 4 。
代码实现:
public int rob(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int n = nums.length;
if (n == 1) {
return nums[0];
}
return Math.max(rob(nums, 0, n - 2), rob(nums, 1, n - 1));
}
private int rob(int[] nums, int first, int last) {
int pre2 = 0, pre1 = 0;
for (int i = first; i <= last; i++) {
int cur = Math.max(pre1, pre2 + nums[i]);
pre2 = pre1;
pre1 = cur;
}
return pre1;
}
题目描述:有 N 个 信 和 信封,它们被打乱,求错误装信方式的数量。
定义一个数组 dp 存储错误方式数量,dp[i] 表示前 i 个信和信封的错误方式数量。假设第 i 个信装到第 j 个信封里面,而第 j 个信装到第 k 个信封里面。根据 i 和 k 是否相等,有两种情况:
综上所述,错误装信数量方式数量为:
题目描述:假设农场中成熟的母牛每年都会生 1 头小母牛,并且永远不会死。第一年有 1 只小母牛,从第二年开始,母牛开始生小母牛。每只小母牛 3 年之后成熟又可以生小母牛。给定整数 N,求 N 年后牛的数量。
第 i 年成熟的牛的数量为:
给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
示例:
输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 7
解释: 因为路径 1→3→1→1→1 的总和最小。
代码实现:
public int minPathSum(int[][] grid) {
if (grid.length == 0 || grid[0].length == 0) {
return 0;
}
int m = grid.length, n = grid[0].length;
int[] dp = new int[n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (j == 0) {
dp[j] = dp[j]; // 只能从上侧走到该位置
} else if (i == 0) {
dp[j] = dp[j - 1]; // 只能从左侧走到该位置
} else {
dp[j] = Math.min(dp[j - 1], dp[j]);
}
dp[j] += grid[i][j];
}
}
return dp[n - 1];
}
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
问总共有多少条不同的路径?
例如,上图是一个7 x 3 的网格。有多少可能的路径?
示例 1:
输入: m = 3, n = 2
输出: 3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向右 -> 向下
2. 向右 -> 向下 -> 向右
3. 向下 -> 向右 -> 向右
示例 2:
输入: m = 7, n = 3
输出: 28
代码实现:
public int uniquePaths(int m, int n) {
int[] dp = new int[n];
Arrays.fill(dp, 1);
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[j] = dp[j] + dp[j - 1];
}
}
return dp[n - 1];
}
也可以直接用数学公式求解,这是一个组合问题。机器人总共移动的次数 S=m+n-2,向下移动的次数 D=m-1,那么问题可以看成从 S 中取出 D 个位置的组合数量,这个问题的解为 C(S, D)。
public int uniquePaths(int m, int n) {
int S = m + n - 2; // 总共的移动次数
int D = m - 1; // 向下的移动次数
long ret = 1;
for (int i = 1; i <= D; i++) {
ret = ret * (S - D + i) / i;
}
return (int) ret;
}
给定一个整数数组 nums,求出数组从索引 i 到 j (i ≤ j) 范围内元素的总和,包含 i, j 两点。
示例:
给定 nums = [-2, 0, 3, -5, 2, -1],求和函数为 sumRange()
sumRange(0, 2) -> 1
sumRange(2, 5) -> -1
sumRange(0, 5) -> -3
思路:求区间 i ~ j 的和,可以转换为 sum[j + 1] - sum[i],其中 sum[i] 为 0 ~ i - 1 的和。
代码实现:
class NumArray {
private int[] sums;
public NumArray(int[] nums) {
sums = new int[nums.length + 1];
for (int i = 1; i <= nums.length; i++) {
sums[i] = sums[i - 1] + nums[i - 1];
}
}
public int sumRange(int i, int j) {
return sums[j + 1] - sums[i];
}
}
如果一个数列至少有三个元素,并且任意两个相邻元素之差相同,则称该数列为等差数列。
例如,以下数列为等差数列:
1, 3, 5, 7, 9
7, 7, 7, 7
3, -1, -5, -9
以下数列不是等差数列。
1, 1, 2, 5, 7
数组 A 包含 N 个数,且索引从0开始。数组 A 的一个子数组划分为数组 (P, Q),P 与 Q 是整数且满足 0<=P 如果满足以下条件,则称子数组(P, Q)为等差数组: 元素 A[P], A[p + 1], ..., A[Q - 1], A[Q] 是等差的。并且 P + 1 < Q 。 函数要返回数组 A 中所有为等差数组的子数组个数。 示例: dp[i] 表示以 A[i] 为结尾的等差递增子区间的个数。 当 A[i] - A[i-1] == A[i-1] - A[i-2],那么 [A[i-2], A[i-1], A[i]] 构成一个等差递增子区间。而且在以 A[i-1] 为结尾的递增子区间的后面再加上一个 A[i],一样可以构成新的递增子区间。 综上,在 A[i] - A[i-1] == A[i-1] - A[i-2] 时,dp[i] = dp[i-1] + 1。 因为递增子区间不一定以最后一个元素为结尾,可以是任意一个元素结尾,因此需要返回 dp 数组累加的结果。 代码实现: 给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。 示例 1: 输入: 2 输入: 10 代码实现: 给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。 示例 1: 输入: n = 12 输入: n = 13 代码实现: 一条包含字母 A-Z 的消息通过以下方式进行了编码: 'A' -> 1 示例 1: 输入: "12" 输入: "226" 代码实现: 已知一个序列 {S1, S2,...,Sn},取出若干数组成新的序列 {Si1, Si2,..., Sim},其中 i1、i2 ... im 保持递增,即新序列中各个数仍然保持原数列中的先后顺序,称新序列为原序列的一个 子序列 。 如果在子序列中,当下标 ix > iy 时,Six > Siy,称子序列为原序列的一个 递增子序列 。 定义一个数组 dp 存储最长递增子序列的长度,dp[n] 表示以 Sn 结尾的序列的最长递增子序列长度。对于一个递增子序列 {Si1, Si2,...,Sim},如果 im < n 并且 Sim < Sn,此时 {Si1, Si2,..., Sim, Sn} 为一个递增子序列,递增子序列的长度增加 1。满足上述条件的递增子序列中,长度最长的那个递增子序列就是要找的,在长度最长的递增子序列上加上 Sn 就构成了以 Sn 为结尾的最长递增子序列。因此 dp[n] = max{ dp[i]+1 | Si < Sn && i < n} 。 因为在求 dp[n] 时可能无法找到一个满足条件的递增子序列,此时 {Sn} 就构成了递增子序列,需要对前面的求解方程做修改,令 dp[n] 最小为 1,即: 对于一个长度为 N 的序列,最长递增子序列并不一定会以 SN 为结尾,因此 dp[N] 不是序列的最长递增子序列的长度,需要遍历 dp 数组找出最大值才是所要的结果,max{ dp[i] | 1 <= i <= N} 即为所求。 给定一个无序的整数数组,找到其中最长上升子序列的长度。 示例: 输入: [10,9,2,5,3,7,101,18] 代码实现: 使用 Stream 求最大值会导致运行时间过长,可以改成以下形式: 以上解法的时间复杂度为 O(N2),可以使用二分查找将时间复杂度降低为 O(NlogN)。 定义一个 tails 数组,其中 tails[i] 存储长度为 i + 1 的最长递增子序列的最后一个元素。对于一个元素 x, 例如对于数组 [4,3,6,5],有: 可以看出 tails 数组保持有序,因此在查找 Si 位于 tails 数组的位置时就可以使用二分查找。 题目描述:对于 (a, b) 和 (c, d) ,如果 b < c,则它们可以构成一条链。 代码实现: 数组中的数字交替进行递增递减摆动。 要求:使用 O(N) 时间复杂度求解。 代码实现: 对于两个子序列 S1 和 S2,找出它们最长的公共子序列。 定义一个二维数组 dp 用来存储最长公共子序列的长度,其中 dp[i][j] 表示 S1 的前 i 个字符与 S2 的前 j 个字符最长公共子序列的长度。考虑 S1i 与 S2j 值是否相等,分为两种情况: 综上,最长公共子序列的状态转移方程为: 对于长度为 N 的序列 S1 和长度为 M 的序列 S2,dp[N][M] 就是序列 S1 和序列 S2 的最长公共子序列长度。 与最长递增子序列相比,最长公共子序列有以下不同点: 代码实现:
A = [0, 1, 2, 3, 4]
return: 6, for 3 arithmetic slices in A:
[0, 1, 2],
[1, 2, 3],
[0, 1, 2, 3],
[0, 1, 2, 3, 4],
[ 1, 2, 3, 4],
[2, 3, 4]
dp[2] = 1
[0, 1, 2]
dp[3] = dp[2] + 1 = 2
[0, 1, 2, 3], // [0, 1, 2] 之后加一个 3
[1, 2, 3] // 新的递增子区间
dp[4] = dp[3] + 1 = 3
[0, 1, 2, 3, 4], // [0, 1, 2, 3] 之后加一个 4
[1, 2, 3, 4], // [1, 2, 3] 之后加一个 4
[2, 3, 4] // 新的递增子区间
public int numberOfArithmeticSlices(int[] A) {
if (A == null || A.length == 0) {
return 0;
}
int n = A.length;
int[] dp = new int[n];
for (int i = 2; i < n; i++) {
if (A[i] - A[i - 1] == A[i - 1] - A[i - 2]) {
dp[i] = dp[i - 1] + 1;
}
}
int total = 0;
for (int cnt : dp) {
total += cnt;
}
return total;
}
4.分割整数
4.1 分割整数的最大乘积
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。
示例 2:
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。public int integerBreak(int n) {
int[] dp = new int[n + 1];
dp[1] = 1;
for (int i = 2; i <= n; i++) {
for (int j = 1; j <= i - 1; j++) {
dp[i] = Math.max(dp[i], Math.max(j * dp[i - j], j * (i - j)));
}
}
return dp[n];
}
4.2 按平方数来分割整数
输出: 3
解释: 12 = 4 + 4 + 4.
示例 2:
输出: 2
解释: 13 = 4 + 9.public int numSquares(int n) {
List
4.3 分割整数构成字母字符串
'B' -> 2
...
'Z' -> 26
给定一个只包含数字的非空字符串,请计算解码方法的总数。
输出: 2
解释: 它可以解码为 "AB"(1 2)或者 "L"(12)。
示例 2:
输出: 3
解释: 它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。public int numDecodings(String s) {
if (s == null || s.length() == 0) {
return 0;
}
int n = s.length();
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = s.charAt(0) == '0' ? 0 : 1;
for (int i = 2; i <= n; i++) {
int one = Integer.valueOf(s.substring(i - 1, i));
if (one != 0) {
dp[i] += dp[i - 1];
}
if (s.charAt(i - 2) == '0') {
continue;
}
int two = Integer.valueOf(s.substring(i - 2, i));
if (two <= 26) {
dp[i] += dp[i - 2];
}
}
return dp[n];
}
5.最长递增子序列
5.1 最长递增子序列
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。public int lengthOfLIS(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
for (int i = 0; i < n; i++) {
int max = 1;
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
max = Math.max(max, dp[j] + 1);
}
}
dp[i] = max;
}
return Arrays.stream(dp).max().orElse(0);
}
int ret = 0;
for (int i = 0; i < n; i++) {
ret = Math.max(ret, dp[i]);
}
return ret;
tails len num
[] 0 4
[4] 1 3
[3] 1 6
[3,6] 2 5
[3,5] 2 null
public int lengthOfLIS(int[] nums) {
int n = nums.length;
int[] tails = new int[n];
int len = 0;
for (int num : nums) {
int index = binarySearch(tails, len, num);
tails[index] = num;
if (index == len) {
len++;
}
}
return len;
}
private int binarySearch(int[] tails, int len, int key) {
int l = 0, h = len;
while (l < h) {
int mid = l + (h - l) / 2;
if (tails[mid] == key) {
return mid;
} else if (tails[mid] > key) {
h = mid;
} else {
l = mid + 1;
}
}
return l;
}
5.2 一组整数对能够构成的最长链
Input: [[1,2], [2,3], [3,4]]
Output: 2
Explanation: The longest chain is [1,2] -> [3,4]
public int findLongestChain(int[][] pairs) {
if (pairs == null || pairs.length == 0) {
return 0;
}
Arrays.sort(pairs, (a, b) -> (a[0] - b[0]));
int n = pairs.length;
int[] dp = new int[n];
Arrays.fill(dp, 1);
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
if (pairs[j][1] < pairs[i][0]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
return Arrays.stream(dp).max().orElse(0);
}
5.3 最长摆动子序列
Input: [1,7,4,9,2,5]
Output: 6
The entire sequence is a wiggle sequence.
Input: [1,17,5,10,13,15,10,5,16,8]
Output: 7
There are several subsequences that achieve this length. One is [1,17,10,13,10,16,8].
Input: [1,2,3,4,5,6,7,8,9]
Output: 2
public int wiggleMaxLength(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int up = 1, down = 1;
for (int i = 1; i < nums.length; i++) {
if (nums[i] > nums[i - 1]) {
up = down + 1;
} else if (nums[i] < nums[i - 1]) {
down = up + 1;
}
}
return Math.max(up, down);
}
6.最长公共子序列
public int longestCommonSubsequence(String text1, String text2) {
int n1 = text1.length(), n2 = text2.length();
int[][] dp = new int[n1 + 1][n2 + 1];
for (int i = 1; i <= n1; i++) {
for (int j = 1; j <= n2; j++) {
if (text1.charAt(i - 1) == text2.charAt(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[n1][n2];
}