LeetCode刷题: 【300】 最长上升子序列(动态规划 / 贪心算法)

1. 题目

在这里插入图片描述

2. 解法(动态规划)

2.1 解题思路

暴力法(使用回溯法遍历所有子序列,判定严格上升序列,并取最大值,时间复杂度O(n*2^n))好,动态规划,分别计算以每个元素结束的上升子序列长度,需要向前遍历寻找最长上升子序列,再加1,时间复杂度O(n^2)

2.2 代码

class Solution {
    
    
public:
    int lengthOfLIS(vector<int>& nums) {
    
    
        // 解空间 
        // 保存以nums[i]结尾的最长上升子序列长度
        // 初始时单个数字为最长上升子序列 默认值为1
        vector<int> dp(nums.size(), 1);
        int ans = 0; // 答案

        for(int i = 0; i < nums.size(); i++){
    
    
            int max = 0; // 当 j < max 时,子序列长度已经不可能比max大了
            for(int j = i; j >= max; j--){
    
    
                if(nums[i] > nums[j] && dp[j] > max){
    
    
                    max = dp[j];
                }
            }
            // 状态转移公式
            // dp[i] = max{ dp[j] | 0 <= j < i, nums[i] > nums[j] } + 1
            dp[i] = max + 1;
            if(dp[i] > ans){
    
    
                ans = dp[i];
            } // 记录最长上升子序列长度
        }

        return ans;
    }
};

3. 解法(贪心算法)

3.1 解题思路

贪心思想:当目前保存的上升序列末尾越小,越可能延长
在遍历nums的过程中,保存长度为i的上升序列的最小末尾tail[i]


  1. nums[i]大于所有tail (即 > tail[max_length]) 时,说明当前维护的最长上升序列末尾应追加元素,使tail追加元素nums[i]max_length+1
  2. tail中存在等于nums[i]的元素,说明以nums[i]结尾的上升序列已存在
  3. 替换第一个比nums[i]大的tail,说明tail该位置对应长度的上升序列可以有一个更小的末尾【注:此操作不会影响更长的上升序列,而是继承更短的上升序列变化,故贪心有效 (请看 流程解析 )】

tail在计算过程中始终保持严格单调递增
所以可以使用二分法定位第一个比nums[i]大的tail的位置
最终tail的长度即为最长上升子序列长度
时间复杂度 O(n * logn)

3.2 流程解析

示例:[10, 9, 2, 5, 3, 7, 101, 18, 6, 22, 11, 9]
以下为上升序列搜索和维持示意,
其中各序列末尾tail元素,行数自上而下为序列长
最后一行为当前最长上升序列(与tail不一定一样)


10 :长度为1的上升序列


9 :替换缩小


2 :替换缩小


2
2 5 :延伸长度1的序列为长度2


2
2 3 :替换缩小长度为2的序列末尾元素


2
2 3
2 3 7 :延伸长度2的序列为长度3


2
2 3
2 3 7
2 3 7 101 :延伸长度3的序列为长度4


2
2 3
2 3 7
2 3 7 18 :替换缩小长度为4的序列末尾元素


2
2 3
2 3 6 :替换缩小长度为3的序列末尾元素
2 3 7 18 》 此时6在18右侧,所以7不变


2
2 3
2 3 6
2 3 7 18 》 此时由于6在18右侧,只能延长第4行,不受之前行影响
2 3 7 18 22 :延伸长度4的序列为长度5


2
2 3
2 3 6
2 3 6 11 :替换缩小长度为4的序列末尾元素,继承更短序列
2 3 7 18 22 》此时由于11在22右侧,不能继续继承


2
2 3
2 3 6
2 3 6 9 :替换缩小长度为4的序列末尾元素
2 3 7 18 22 》此时由于9在18右侧,不能继承


最终最长上升子序列【2 3 7 18 22】长度为 5

3.2 代码

class Solution {
    
    
public:
    int lengthOfLIS(vector<int>& nums) {
    
    
        // 辅助空间 在计算过程中保持严格单增
        // tail[i] = 长度为i的上升子序列的最小末尾 
        vector<int> tail(nums.size() + 1, INT_MIN);
        int max_length = 0; // 已知最长上升子序列长度

        for(int i = 0; i < nums.size(); i++){
    
    
            if(nums[i] > tail[max_length]){
    
    
                // 如果num[i]严格大于所有tail,则追加
                // 在计算过程中保持严格单增
                // 即最长上升子序列+1,并以num[i]结尾
                tail[++max_length] = nums[i];
            }else{
    
    
                // 使用二分搜索定位第一个大于num[i]的tail
                int l = 0, r = max_length;
                while(l <= r){
    
    
                    int mid = (l + r) >> 1; // 位运算 除以2
                    if(tail[mid] < nums[i]){
    
    
                        l = mid + 1;
                    }else{
    
    
                        // 当tail中含有相等num[i]的元素时不必进行替换操作,此处为了写法简便,进行了替换
                        r = mid - 1;
                    }
                }
                // 替换(缩小)第一个大于num[i]的上升子序列的最小末尾
                tail[l] = nums[i];
            }/*
            for(int q = 1; q <= max_length; q++){
                cout<<tail[q]<<" ";
            }
            cout<<endl;*/
        }

        // 最终tail的长度即为最长上升子序列长度
        return max_length;
    }
};

3.3 疑问

class Solution {
    
    
public:
    int lengthOfLIS(vector<int>& nums) {
    
    
        // 辅助空间 在计算过程中保持严格单增
        // tail[i] = 长度为i的上升子序列的最小末尾 
        vector<int> tail(nums.size() + 1, INT_MIN);
        int max_length = 0; // 已知最长上升子序列长度

        for(int i = 0; i < nums.size(); i++){
    
    
            if(nums[i] > tail[max_length]){
    
    
                // 如果num[i]严格大于所有tail,则追加
                // 在计算过程中保持严格单增
                // 即最长上升子序列+1,并以num[i]结尾
                tail[++max_length] = nums[i];
            }else{
    
    
                // 使用二分搜索定位第一个大于num[i]的tail
                int l = 0, r = max_length;
                int mid = 0;
                while(l < r){
    
    
                    mid = (l + r) / 2;
                    if(tail[mid] == nums[i]){
    
    
                        // 当tail中含有相等num[i]的元素时不能进行替换操作
                        mid = -1;
                        break;
                    }else if(tail[mid] < nums[i]){
    
    
                        l = mid + 1;
                    }else{
    
    
                        r = mid - 1;
                    }
                }
                if(mid != -1){
    
    
                    // 替换(缩小)第一个大于num[i]的上升子序列的最小末尾
                    // 此处即使不进行if-else判断也可以通过LeetCode
                    // 但是这样并没有保证每次换掉的是第一个更大的
                    // tail序列将会不同,但长度不变
                    // 可能的猜想:
                    // 1. 算法可以优化,可能不需要保存所有之前更短的状态
                    // 2. LeetCode判例不足
                    //if(tail[l] > nums[i])
                        tail[l] = nums[i];
                    //else
                    //    tail[l+1] = nums[i];
                }
            }
            // for(int q = 1; q <= max_length; q++){
    
    
            //     cout<<tail[q]<<" ";
            // }
            // cout<<endl;
        }

        // 最终tail的长度即为最长上升子序列长度
        return max_length;
    }
};

经测试,在这里插入图片描述
[11,12,13,14,15,16,1,2,3,4,1,5,6,7,1,8]
无法通过该判例,但LeetCode官网可以提交通过

猜你喜欢

转载自blog.csdn.net/Activity_Time/article/details/104871350