首页 > 试题广场 >

最长严格上升子数组(一)

[编程题]最长严格上升子数组(一)
  • 热度指数:3262 时间限制:C/C++ 1秒,其他语言2秒 空间限制:C/C++ 256M,其他语言512M
  • 算法知识视频讲解
给定一个长度为n的正整数数组nums,可以任意改变数组的其中一个元素,改变的元素范围也在[1,100000]之内,然后返回nums的最长"严格上升"子数组的长度。
1.子数组是连续的,比如[1,3,5,7,9]的子数组有[1,3],[3,5,7]等等,但是[1,3,7]不是子数组
2.严格上升指在数组上任意位置都满足 nums[i] < nums[i+1],比如[1,2,2,3],其中[1,2,2]不是严格上升的子数组,[1,2]是的
数据范围:
要求: 空间复杂度 ,时间复杂度
示例1

输入

[7,2,3,1,5,6]

输出

5

说明

将1改为4,最长严格上升子数组为[2,3,4,5,6]      
示例2

输入

[1,2,3,4]

输出

4

说明

最长严格上升子数组为[1,2,3,4]      
示例3

输入

[1,2,2,3]

输出

3

说明

改变一个元素之后,最长严格上升子数组为[1,2,3]或者[2,3,4],长度都为3      

算法流程

用经典的动态规划求解,需要用到预处理技巧,所以会有O(n)的空间复杂度消耗。
  1. 构建两个dp数组dpLeftdpRight,其中的元素表示分别以数组中某个数结尾和开头的最长递增子数组长度,可以直接用O(n)时间复杂度的动态规划把这两个数组求出来。
  2. 然后遍历数组,检查是否可以通过修改nums[i]将左右两边的最长递增子数组连起来。如果不能连起来,就看nums[i]是修改完成为左边递增子数组的结尾能使递增子数组更长,还是修改完成为右边递增子数组的开头能使递增子数组更长。
import java.util.*;


public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param nums int整型一维数组 
     * @return int整型
     */
    public int maxSubArrayLengthTwo (int[] nums) {
        // 预处理
        int n = nums.length;
        if(n == 1) return 1;
        int[] dpLeft = new int[n];     // dpLeft[i]表示以nums[i]结尾的最长递增子数组长度
        dpLeft[0] = 1;
        for(int i = 1; i < n; i++){
            dpLeft[i] = nums[i] > nums[i - 1]? dpLeft[i - 1] + 1: 1;
        }
        int[] dpRight = new int[n];     // dpRight[i]表示以nums[i]开头的最长递增子数组长度
        dpRight[n - 1] = 1;
        for(int i = n - 2; i >= 0; i--){
            dpRight[i] = nums[i] < nums[i + 1]? dpRight[i + 1] + 1: 1;
        }
        int maxLen = 1;
        for(int i = 0; i < n; i++){
            if(0 < i && i < n - 1 && nums[i + 1] - nums[i - 1] > 1){
                // 左右两边的递增子数组可以接起来
                maxLen = Math.max(maxLen, dpLeft[i - 1] + dpRight[i + 1] + 1);
            }else{
                // 两边接不起来就看nums[i]接前面更长还是接后面更长
                if(i > 0){
                    maxLen = Math.max(maxLen, dpLeft[i - 1] + 1);
                }
                if(i < n - 1 && nums[i + 1] > 1){
                    // 由于是正数数组,后一个数比1大才有可能接起来
                    maxLen = Math.max(maxLen, dpRight[i + 1] + 1);
                }
            }
        }
        return maxLen;
    }
}

 复杂度分析

总共遍历了3遍数组,时间复杂度为O(n),开辟了两个dp数组,额外空间复杂度O(n)。
发表于 2021-12-21 15:08:53 回复(3)
不知道
发表于 2021-10-26 07:30:01 回复(1)
class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param nums int整型vector 
     * @return int整型
     */
    int maxSubArrayLengthTwo(vector<int>& nums) {
        // write code here
        int maxLen = 0;
        int lastLen = 0;
        int start = 0;
        for (int i = 0; i < nums.size(); ++i) {
            
            int pre = 0;
            if (i > 0) pre = nums[i - 1];
            if (nums[i] > pre) {
                //len++
                maxLen = max(maxLen, i - start + 1 + lastLen);//包含第i位 start-i的长度 加上 前一段的长度
            } else {
                maxLen = max(maxLen, i - start + (lastLen == 0 ? 1 : lastLen));//处理结束的可能 如果lastlen=0 表示并没有用到修改 可以+1(第i位)
                if (i + 1 >= nums.size() || nums[i+1] - pre > 1 || (i - 2 >= 0 && nums[i] - nums[i-2] > 1)) {
                    //能通过修改一个值 num[i] 或者 num[i-1] 连起来
                    //前一段的长度 i - start
                    lastLen = i - start;
                } else {
                    //不能完整的连起来
                    //但是可以修改一个值
                    //前一段能修改的话 lastlen = 1
                    lastLen = 1;
                    if (nums[i] == 1) lastLen = 0;
                }
                start = i;
            }
        }
        return maxLen;
    }
};

发表于 2021-10-17 13:28:21 回复(0)
我自己做的时候,使用的思路是找到乱序的数组下标,然后再判断每个乱序元素,检查是否满足可替换的条件,如果满足就直接替换,最终计算出两个相邻的乱序下标的差值再加1就是最大递增长度,但是在对乱序数组操作的时候,如果乱序数组仅有一个有效的乱序元素,无法确认是否全局仅有一个乱序元素,试图使用贪心算法解决,但未成功,脑溢血代码如下:
public static int maxSubArrayLengthTwo(int[] nums) {
        // write code here
        if (nums.length <= 1) return nums.length;

        int tmp1 = nums[0], tmp2 = nums[0];

        for (int num : nums) {
            if (num != tmp1) {
                tmp2 = num;
                break;
            }
        }
        if (tmp1 == tmp2) return 2;

        int coordinator = 0;
        int[] deCreArr = new int[nums.length];
        Arrays.fill(deCreArr, nums.length - 1);
        for (int i = 0; i < nums.length - 2; i++) {
            if (nums[i + 1] <= nums[i] && (
                    (i == 0 && nums[1] > 1) ||
                            (i > 0 && i < nums.length - 1 && nums[i + 2] - nums[i] > 1)
            )) deCreArr[coordinator++] = i + 1;
        }

        if (nums.length > 3 && nums[nums.length - 2] <= nums[nums.length - 3] && nums[nums.length - 2] - nums[nums.length - 4] > 1)
            deCreArr[coordinator] = nums.length - 2;
        int res = 0;
        // 只有一个递减 TODO: BUG -> 无法确认全局是否仅有一个乱序元素
        if (deCreArr[0] == deCreArr.length - 1 || deCreArr[1] == deCreArr.length - 1) {
            int distCnt = nums.length;
            for (int i = 0; i < nums.length - 2; i++)
                if ((i == 0 && nums[0] == nums[1] && nums[0] == 1)
                        || (i > 0 && nums[i] == nums[i + 1] && nums[i] - nums[i - 1] == 1))
                    distCnt--;
            res = distCnt;
        } else
            // 多个递减
            for (int i = 0; i < deCreArr.length - 2; i++)
                res = Math.max(res, deCreArr[i + 2] - deCreArr[i] + 1);
        return res;
    }

下面介绍正确的解法:
1. 将数组分为三部分,前一部分 0 ~ i-1 , 中间部分 i, 后一部分 i ~ len - 1
2. 然后分别求出前一部分和后一部分的 LIS
3. 最后改变 i 元素, 检查看这三部分是否能够拼接起来,求出全局最长子序列 res
    1) 如果可以拼接,则当前i 的最大子序列就是 前部分 + 后部分 + 1
    2) 如果不可以拼接
            2.1) 考虑前部分,可以任意改变 i 处对应的元素的值,最大值就是 dpPre[i-1] + 1 或者 dp[i]
            2.2)   考虑后部分,也是可以任意改变 i 处的值,最大值则是 dpSuf[i+1] + 1 或者 dpSuf[I] 
4. 综上,对每个元素 i 都这样操作,算出全局最长子序列 res, 返回

public int maxSubArrayLengthTwo (int[] nums) {
        // write code here
        int n = nums.length;
        int[] dpPre = new int[n];
        dpPre[0] = 1;
        int[] dpSuf = new int[n];
        dpSuf[n - 1] = 1;
        for (int i = 1; i < n; i++) {
            dpPre[i] = nums[i] > nums[i - 1] ? dpPre[i - 1] + 1 : 1;
        }
        for (int i = n - 2; i >= 0; i--) {
            dpSuf[i] = nums[i] < nums[i + 1] ? dpSuf[i + 1] + 1 : 1;
        }
        int res = 1, curMax = 1;
        for (int i = 0; i < n; i++) {
            if (i > 0 && i < n - 1 && nums[i - 1] + 1 < nums[i + 1]) {
                curMax = dpPre[i - 1] + dpSuf[i + 1] + 1;
            } else {
                if (i > 0)
                    curMax = Math.max(dpPre[i], dpPre[i - 1] + 1);

                if (i < n - 1 && nums[i + 1] > 1)
                    curMax = Math.max(Math.max(curMax, dpSuf[i]), dpSuf[i + 1] + 1);
            }
            res = Math.max(res, curMax);
        }
        return res;
    }

总结: 后半部分的dp 数组为 dpSuf, 主要是记录 i ~ len -1  的最长子序列,就是为了方便直接获取到以 i 开头的最长子序列,目的就是为了在任意改变 i 拼接

发表于 2024-07-09 17:44:40 回复(0)
class Solution:
    def maxSubArrayLengthTwo(self , nums: List[int]) -> int:
        n = len(nums)
        dp_left = [1]*n     # 以nums[i]结尾
        dp_right = [1]*n    # 以nums[i]开头
        for i in range(1,n):    
            if nums[i]>nums[i-1]:
                dp_left[i] = dp_left[i-1]+1
        for i in range(n-2,-1,-1):
            if nums[i]<nums[i+1]:
                dp_right[i] = dp_right[i+1]+1
        # 更改nums[i],拼接
        res = 1
        for i in range(n):
            if i > 0 and i < n-1 and nums[i+1]-nums[i-1] > 1:   # 拼接左右
                res = max(res,dp_left[i-1]+dp_right[i+1]+1)
            else:
                if i>0:
                    res = max(res,dp_left[i-1]+1)               # 拼接在结尾
                if i<n-1 and nums[i+1]>1:
                    res = max(res,dp_right[i+1]+1)              # 拼接在开头
        return res

发表于 2025-01-15 14:39:23 回复(0)
真是巨无语,1123这个数组不能变成0123,说好的任意改变呢!!!
发表于 2022-01-22 10:09:51 回复(1)