动态规划真的懂了呀系列《DP:最长上升子序列模型》

84 阅读3分钟

价值:记录学习过程的思考,本身就是一场动态规划的前生,记忆化搜索。

Problem: 300. 最长递增子序列

    // 状态定义:dp[i] 表示 前i-1个 小于 当前i的最长的递增子序列
    // 状态递推:dp[i] = max(dp[i],dp[j])
    // 答案:dp[i]+1并且赋值到dp[i]中, 每一步状态都可能是答案,更新每一步 ans,得出答案


    //定义状态:dp[i] 表示以i结尾的最长递增子序列长度
    //状态递推:dp[i] = max(dp[i],dp[j]+1)
    // 答案: 可能不以它结尾,枚举取max
class Solution {
    private int[] arr, cache;
    public int lengthOfLIS(int[] nums) {
        this.arr = nums;
        int n = nums.length;

        //dp[i] = max(dp[j],dp[i]) ==> ans= ++dp[i];
        //回溯
        //int res = lengthOfLIS_hui_su(nums);

        //记忆化搜索
        // cache = new int[n];
        // int res = lengthOfLIS_cache(nums);

        //递推
        int res = dpn(n);

        //另一种dp
        //dp[i] = max(dp[j]+1,dp[i]) ==> ans = dp[i];
        //int res = lengthOfLIS_DP1(nums);
        return res;
    }

    //题意:
    //   递增子序列 IS Increasing Subsequence
    //最长递增子序列 LIS Longest Increasing Subsequence

    //思考本质:
    //从数组中选择一些数,且顺序和数组中的顺序是一致的

    //【回溯实现】
    //集合角度考虑:子序列本质上就是数组的一个子集,那我们就用子集型回溯来思考
    //            对于子集型回溯
    // 我们有 “选或不选” 和 “枚举选哪个” 这两种思路
    // 如果 我们倒着思考,假设nums[n-1]是子序列的最后一个数,
    // 思路1“选或不选”,前面的数字需要和nums[n-1]比较大小,所以知道当前下标外,还需要前边数字的下标
    // 思路2“枚举选哪个”,我们可以直接枚举前面的比nums[n-1]小的数字,当作子序列的倒数第二个数字,只需要知道当前这个下标
    // 综上思路1、思路2,思路2,只需要一个参数下标,更加好写,下面就这样实现思路2

    //【====优化思路===】
    //O(n^2)      回溯 -> 记忆化搜索 -> 递推
    //O(nlogn)    二分+贪心
    //【====优化思路===】



    //回溯三问:子问题?以nums[i] 结尾的LIS长度
    //        当前操作?枚举nums[j]
    //          下一个子问题?以nums[j]结尾的LIS长度
    //回溯思路2:回溯探索: dfs(i) = max{dfs(j)} + 1, j<i && arr[j] < arr[i]
    public int dfs(int i){
        int res = 0;
        for(int j = 0; j < i; j++){
            if(arr[j]<arr[i]){
                res = Math.max(dfs(j),res);
            }
        }
        return res+1;
    }

    //回溯优化思考:再case1中,比如从18开始递归,会用到7,从101开始递归,会用到7,重复子问题
    //动态规划来优化
    public int dfs_cache(int i){
        if(cache[i]>0){
            return cache[i];
        }
        int res = 0;
        for(int j = 0; j < i; j++){
            if(arr[j]<arr[i]){
                res = Math.max(dfs_cache(j),res);
            }
        }
        cache[i] = res+1;
        return res+1;
    }

    //递推:dfs 变为 dp[] ,递归改为循环
    // 状态定义:dp[i] 表示 前i-1个 小于 当前i的最长的递增子序列
    // 状态递推:dp[i] = max(dp[j],dp[i])
    // 答案:dp[i]+1并且赋值到dp[i]中
    public int dpn(int n){
        int ans = 0;
        int[] dp = new int[n];
        for(int i = 0; i< n; i++){
            for(int j =0 ; j< i; j++){
                if(arr[j] < arr[i]){     
                   dp[i] = Math.max(dp[i], dp[j]);
                }
            }
            ans = Math.max(ans,++dp[i]);
        }
        return ans;
    }

    public int lengthOfLIS_cache(int[] nums) {
        int n = nums.length;

        int ans = 0;
        //枚举nums[i]作为子数组的结尾数字
        for(int i =0 ;i <n;i++){
            ans = Math.max(dfs_cache(i),ans);
        }
        return ans;
    }


    public int lengthOfLIS_hui_su(int[] nums) {
        int n = nums.length;

        int ans = 0;
        //枚举nums[i]作为子数组的结尾数字
        for(int i =0 ;i <n;i++){
            ans = Math.max(dfs(i),ans);
        }
        return ans;
    }

    //定义状态:dp[i] 表示以i结尾的最长递增子序列长度
    //状态递推:dp[i] = max(dp[i],dp[j]+1)
    public int lengthOfLIS_DP1(int[] nums) {
        int n = nums.length;
        int i = 0;
        int j = 0;
        int[] dp = new int[n];
        dp[0] = 1;
        for(i = 1; i < n; i++){
            dp[i] = 1;
            for(j = 0 ; j < i; j++){
                if(nums[j] < nums[i]){
                    dp[i] = Math.max(dp[i],dp[j]+1);
                }
            }
        }
        int ans = 0;
        for(int t = 0; t<n; t++){
            ans = Math.max(ans,dp[t]);
        }
        return ans;
    }
}