LeetCode1713得到子序列的最少操作次数
使用了动态规划,贪心算法,二分查找
题目
给你一个数组 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:
输入:target = [5,1,3], arr = [9,4,2,3,4]
输出:2
解释:你可以添加 5 和 1 ,使得 arr 变为 [5,9,4,1,2,3,4] ,target 为 arr 的子序列。
示例 2:
输入:target = [6,4,8,1,3,2], arr = [4,7,6,2,3,8,6,1]
输出:3
提示:
1 <= target.length, arr.length <= 105
1 <= target[i], arr[i] <= 109
target 不包含任何重复元素。
来源:力扣(LeetCode)
解题思路
以实例二为例
可以把题目转换为:
要求最少的操作次数——————>target的长度减去arr中与target的最长公共子序列
就是求arr中与target的最长公共子序列
如果把target中的每一个数用它在数组中的下标来代表,构建一个新数组target1(由于后续构建arr1的操作因此将target1改为一个HashMap);
然后把arr中每一个数对应target中的下标构成一个arr1数组,若这个数不在target中则直接抛弃因为求的是arr中与target的最长公共子序列,不在target中的数用不到(由于不定长的关系,用一个集合代替数组)(如图)
因为target1是单调递增的而且每一个数具有其唯一性,这样题目就变为:
求arr中与target的最长公共子序列——————>求arr1中与target1的最长递增公共子序列
而求两个数组的最长递增公共子序列有两种方法:
1.动态规划
2.贪心算法
这样题目就简单化了。
方法一:动态规划(超时)
1.思想:
数组dp[i]:表示以第i个数字为结尾的最长上升子序列长度
dp数组的长度与arr1一致,然后依次得到dp数组的值,最后求得dp数组中的最大值max即为arr1中与target1的最长递增公共子序列,返回target.length-max得到最终结果
而dp数组如何去求?
我们必须从小到大计算dp 数组的值,在计算 dp[i] 之前,我们已经计算出dp[0…i−1] 的值,则状态转移方程为:
dp[i]=max(dp[j])+1,其中0≤j<i且num[j]<num[i]
就是dp[i]的值等于:用arr1[i]与前边的每一个arr[0]…arr[j]作比较,如果大于,就可以构成一个递增子序列,长度为dp[j]+1,记下这个值为dp[i]的候选值,如果小于则跳过,这样求出多个dp[i]候选值,其中最大的就是dp[i]的值,也就是以第i个数字为结尾的最长上升子序列长度。
2.代码实现
class Solution1713 {
public int minOperations(int[] target, int[] arr) {
//存放target数组每一个元素的下标
HashMap<Integer, Integer> target1=new HashMap<Integer, Integer>();
for (int i = 0; i < target.length; i++) {
target1.put(target[i], i);
}
//存放arr中每一个数在target中的下标
List<Integer> arr1=new ArrayList<Integer>();
for (int i = 0; i < arr.length; i++) {
//排除某个不存在于target中的数
if (target1.containsKey(arr[i])) {
arr1.add(target1.get(arr[i]));
}
}
// for (int i = 0; i < target1.size(); i++) {
// System.out.print(target1.get(target[i])+" ");
// }
// System.out.println();
// for (int i = 0; i < arr1.size(); i++) {
// System.out.print(arr1.get(i)+" ");
// }
// System.out.println();
//找arr1中的最长递增子序列
//---------------------------方法一,动态规划(超时)-----------------------
int[] dp=new int[arr1.size()];
//初始化dp数组
for (int i = 0; i < dp.length; i++) {
dp[i]=1;
}
//max记录最长递增子序列长度
int max=1;
for (int i = 1; i < dp.length; i++) {
for (int j= 0; j < i; j++) {
if (arr1.get(i)>arr1.get(j)) {
dp[i]=dp[i]>(dp[j]+1)?dp[i]:(dp[j]+1);
max=max>dp[i]?max:dp[i];
}
}
}
//因为当arr1中没有数字时max=0,但是这里max初始为1会出错,因此加一条件
if (arr1.size()==0) {
max=0;
}
// for (int i = 0; i < dp.length; i++) {
// System.out.print(dp[i]+" ");
// }
// System.out.println();
return target.length-max;
}
}
此方法的时间复杂度为O(n^2),超时。
方法二:贪心算法+二分查找
1.思想:
所谓贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,它所做出的仅仅是在某种意义上的局部最优解,贪心算法没有固定的算法框架,算法设计的关键是贪心策略的选择。
这里考虑一个简单的贪心,如果我们要使上升子序列尽可能的长,则我们需要让序列上升得尽可能慢,因此我们希望每次在上升子序列最后加上的那个数尽可能的小。
1.基于上面的贪心思路,我们维护一个数组 d[i] (因为不定长,所以d在代码中用集合定义),表示长度为 i的最长上升子序列的末尾元素的最小值,d数组整个数组代表了一个最长递增子序列,用len 记录目前最长上升子序列的长度,起始时len 为 1,d[0]=nums[0]。
2.仍然是便利arr1,当arr1[i]大于d数组的最后一个数的值(也是d数组的最大值)时,直接把这个数添加进d数组,构成一个到目前为止的最长递增子序列;
3.但当小于时,要在d数组中找到d[j]<arr[i]<d[j]时的j,并用arr[i]替换d[j],因为数组d是单调递增的,则可以用二分查找节省时间,这一步的目的是实现贪心策略中的每次在上升子序列最后加上的那个数尽可能的小因此把大数换成小数,就可以插入更多的数,使上升子序列更长。
4.当arr1遍历完毕后,此时的d数组就是一个最长的上升子序列,返回最终结果:target.length-d.size
2.代码实现
class Solution1713_2{
public int minOperations(int[] target, int[] arr) {
//存放target数组每一个元素的下标
HashMap<Integer, Integer> target1=new HashMap<Integer, Integer>();
for (int i = 0; i < target.length; i++) {
target1.put(target[i], i);
}
//存放arr中每一个数在target中的下标的当前最长递增序列
List<Integer> arr1=new ArrayList<Integer>();
for (int i = 0; i < arr.length; i++) {
//排除某个不存在于target中的数
if (target1.containsKey(arr[i])) {
arr1.add(target1.get(arr[i]));
}
}
//---------------------------方法二,贪心加二分查找-----------------------
//因为当arr1中没有数字时进行下面的代码会报错,因此加一条件
if (arr1.size()==0) {
return target.length -0;
}
//d[i]表示长度为i的最长上升子序列的末尾元素的最小值
List<Integer> d=new ArrayList<Integer>();
//初始化d
d.add(arr1.get(0));
//当前最长上升序列的长度(当前d的长度)
int len=1;
//遍历arr1
for (int i = 1; i < arr1.size(); i++) {
//如果当前arr1的值大于d的最后一位的值,直接插入并增加d的长度
if (arr1.get(i)>d.get(len-1)) {
d.add(arr1.get(i));
len++;
}else {
//否则进行二分查找找到当d[j-1]<arr1[i]<d[j](也就是当前arr1的值在d中按顺序排序时较大的后一位)时的j
int j=binarySearch(d, arr1.get(i));
//用这个当前的arr1值替换掉原来j所在位的值(也就是贪心中用小的替换掉大的)
d.set(j,arr1.get(i));
}
}
return target.length-d.size();
}
//二分查找的方法
public int binarySearch(List<Integer> d, int arr1i) {
int low = 0, high = d.size() - 1;
while (low < high) {
int mid = (high - low) / 2 + low;
if (d.get(mid) < arr1i) {
low = mid + 1;
} else {
high = mid;
}
}
return low;
}
}
总结
只能说不愧是困难题,整整看了六个小时才吃透这一道,粗略了解了动态规划和贪心算法,仍然一头雾水,只是会做这一道题,其他的同类型题还不能流畅使用这两种方法,还需要做更多的题练习。