问题描述
LCS问题就是最长公共子序列问题,即给定一个数组 target 和另一个数组 arr ,要求返回最长公共子序列的长度。特别地,当 target中没有重复的元素时,这个问题等价于求最长上升子序列问题,即LIS问题。
例题
LeetCode 1713. 得到子序列的最少操作次数
给你一个数组 target ,包含若干 互不相同 的整数,以及另一个整数数组 arr , arr 可能包含重复元素。
每一次操作中,你可以在 arr 的任意位置插入任一整数。比方说,如果 arr = [1,4,1,2] ,那么你可以在中间添加 3 得到 [1,4,3,1,2] 。你可以在数组最开始或最后面添加整数。
请你返回 最少 操作次数,使得 target 成为 arr 的一个子序列。
一个数组的 子序列 指的是删除原数组的某些元素(可能一个元素都不删除),同时不改变其余元素的相对顺序得到的数组。比方说,[2,7,4] 是 [4,2,3,7,2,1,4]的子序列(加粗元素),但 [2,4,2]不是子序列。
示例 1:
1 | 输入: target = [5,1,3], arr = [9,4,2,3,4] |
示例 2:
1 | 输入: target = [6,4,8,1,3,2], arr = [4,7,6,2,3,8,6,1] |
题解
首先,我们不难发现,最终需要返回的结果就是 target 数组的长度 和 两个数组的最长公共子序列的长度 的差,即LCS问题。
一般上来说,使用动态规划解决LCS问题需要复杂度为 O(n*m) (定义dp[i][j] 为target 前 i 项和 arr 前 j 项的公共子序列长度)。
但是由于target 数组是无重复的,我们可以将这一问题转化为LIS问题。我们首先建立target 数组中的元素和下标的一一映射关系,存储在哈希表中。然后遍历 arr 数组,找到 arr 数组中全部与 target 重复的元素,并将其下标记录在一个新的数组 vec 中。这样,我们只要找到 vec 数组的最大升序子序列即可。
使用动态规划暴力解决LIS问题需要的复杂度为 O(n²) (定义 dp[i] 为以 vec[i] 结尾的最长上升子序列的长度,每次计算 dp[i] 需要遍历一次 dp[0] ~ dp[i-1] ,并判断 vec[i] 是否可以接在后面, dp[i] 是其中的最大值)。
而使用贪心法,我们可以将求解LIS问题的复杂度降低到 O(nlogn) 。我们可以维护一个贪心数组 g ,定义 g[i] 为长度为 i 的公共子序列的最小结尾元素。可以证明,g[i] 是单调递增的 (但是我不会证) 。因为我们是从左往右扫描 vec 数组的,所以越小的结尾元素能让越多的后续元素接上。我们只要保存最小的结尾元素,就可以保证找到最长的升序子序列(的长度)。
举个例子,对于 vec = [6,7,0,0,2,0,2,6,7] 来说:
将
6加入到数组g中。此时g[1] = 6将
7与g的最后一个元素比较,发现7 > 6,所以直接将7放入数组g中。此时g[1] = 6 , g[2] = 7将
0与g的最后一个元素比较,发现0 < 7,所以长度为1的子序列的最小结尾元素是0,我们更新g[1]为0。此时g[1] = 0 , g[2] = 7。将
0与g的最后一个元素比较,发现0 < 7,所以长度为1的子序列的最小结尾元素是0,g保持不变。此时g[1] = 0 , g[2] = 7。将
2与g的最后一个元素比较,发现2 < 7,所以长度为2的子序列的最小结尾元素是2,我们更新g[2]为2。此时g[1] = 0 , g[2] = 2。将
0与g的最后一个元素比较,发现0 < 2,所以长度为1的子序列的最小结尾元素是0,g保持不变。此时g[1] = 0 , g[2] = 2。将
2与g的最后一个元素比较,发现2 = 2,所以长度为2的子序列的最小结尾元素是2,g保持不变。此时g[1] = 0 , g[2] = 2。将
6与g的最后一个元素比较,发现6 > 2,所以直接将6放入数组g中。此时g[1] = 0 , g[2] = 2 , g[3] = 6。将
7与g的最后一个元素比较,发现7 > 6,所以直接将7放入数组g中。此时g[1] = 0 , g[2] = 2 , g[3] = 6 , g[4] = 7。遍历完毕,此时数组
g的长度为4,说明vec的最长升序子序列的长度为4。
当扫描到的 vec[i] 小于数组 g 最后一个元素时,我们应该找到数组 g 中大于 vec[i] 的最小值,并更新这个值为 vec[i] 。由于数组 g 是单调递增的,所以我们可以使用二分查找的方法来找到待更新的值,以达到 O(logn) 的查找复杂度。
代码:
1 | class Solution { |
有的没的
小黄鸭调试法
小黄鸭调试法是软件工程中使用的调试代码方法之一。就是在程序的调试、纠错或测试过程中,耐心地向小黄鸭解释每一行程序的作用,以此来激发灵感。
将自己学会的东西记录在Blog里,在写博文的过程中就知道什么东西自己已经掌握了,什么东西自己还没搞懂。
