最长上升子序列(LIS)模型

题目模板

链接:LeetCode300.最长上升子序列
状态定义:f[i]表示以nums[i]为结尾的最长子序列长度
状态转移:f[i] = f[j] + 1, 其中0 < j < i, 且nums[j] < nums[i]
对于每一个i,找到i之前最长的子序列,并且把i添加到尾部,构成一个新的最长上升子序列

//Java代码
class Solution {
    public int lengthOfLIS(int[] nums) {
        if(nums.length == 0) return 0;
        int[] f = new int[nums.length];
        int res = 0;
        for(int i = 0; i < nums.length; i++){
            f[i] = 1;
            for(int j = 0; j < i; j++){
                if(nums[j] < nums[i]){
                    f[i] = Math.max(f[i], f[j]+1);
                }
            }
            res = Math.max(res, f[i]);
        }
        return res;
    }
}

变形1. 求LIS数量

  • 链接:LeetCode673.最长递增子序列的个数

思路

整体思路不变,增加一个count数组来记录递增子序列的数量
count[i]表示以nums[i]结尾的最长上升子序列的数量
对于每个i,不仅要每次更新LIS的长度,还要更新当前结尾的LIS数量
1.如果f[i] < f[j] + 1,说明这是第一次出现某个长度为f[i]的上升子序列
  把f[i]更新为f[j]+1,且此时以i为结尾的子序列的数量就是以j为结尾的子序列数量
2.如果f[i] == f[j] + 1,说明之前出现过f[j]+1这么长的子序列,这次并不是第一次出现
  f[i]不用更新,但是需要把该子序列的数量更新为加上之前该长度的子序列出现过的数量

Java代码

class Solution {
    public int findNumberOfLIS(int[] nums) {
        int n = nums.length;
        int[] f = new int[n];
        int[] count = new int[n];
        int max = 0;
        for(int i = 0; i < n; i++){
            f[i] = count[i] = 1;
            for(int j = 0; j < i; j++){
                if(nums[i] > nums[j]){
                    // f[i] = Math.max(f[i], f[j]+1);
                    // 分两种情况讨论
                    if(f[i] < f[j] + 1){
                        f[i] = f[j] + 1;
                        count[i] = count[j];
                    }else if(f[i] == f[j] + 1){
                        count[i] += count[j];
                    }
                }
            }
            max = Math.max(f[i],max);
        }
        int res = 0;
        for(int i = 0; i < n; i++){
            if(f[i] == max){
                res += count[i];
            }
        }
        return res;
    }
}

变形2. 求LIS方案

链接:LeetCode368.最大整除子集

思路

这题换了一个背景,首先整除是具有传递性,即a % b == 0且b % c == 0,则a % c == 0
这时候我们就可以将原数组进行排序,从而把问题转换为类似求LIS的问题,只不过此时的序列需要可以连续整除
但是这题的难点并不在于求出一共有几个最长的子序列或者是最长子序列长度,而是需要给出方案
所以我们需要通过f数组来反推记录过的元素下标

首先,我们先记录下最长上升子序列的末位元素对应的下标,每次递推回子序列的上一个元素对应的下标
如何确定上一个元素下标是多少呢?
需要通过两个条件:nums[k] % nums[i] == 0 && f[k] == f[i] + 1
nums[k] % nums[i] == 0:表示前一个元素需要能被当前元素整除
f[k] == f[i] + 1:表示前一个元素所对应的LIS长度恰好比当前元素对应的LIS长度少1

Java代码

class Solution {
    public List largestDivisibleSubset(int[] nums) {
        if(nums.length == 0) return new ArrayList<>();
        Arrays.sort(nums);
        int n = nums.length;
        int[] f = new int[n];
        Arrays.fill(f,1);
        int k = 0;
        for(int i = 1; i < n; i++){
            for(int j = 0; j < i; j++){
                if(nums[i] % nums[j] == 0){
                    f[i] = Math.max(f[i], f[j]+1);
                }
            }
            if(f[k] < f[i]) k = i;
        }
        List res = new ArrayList<>();
        res.add(nums[k]);
        while(f[k] > 1){
            for(int i = 0; i < k; i++){
                if(nums[k] % nums[i] == 0 && f[k] == f[i] + 1){
                    res.add(nums[i]);
                    k = i;
                    break;
                }
            }
        }
        return res;
    }
}

变形3. 求LIS的最大和

  • 链接:LeetCode1626.无矛盾的最佳球队

思路

两个前提知识点:
1.LIS的最大和不一定是LIS序列的和,如序列[100,1,2,3]
2.LIS最大和不能用二分来优化,最好的时间复杂度就是O(n^2)

状态表示:f[i]表示以nums[i]结尾的上升子序列的最大和
状态转移:f[i] = f[j] + nums[i], 0 < j < i且num[j] < nums[i], 和模板唯一的区别在于这里是加nums[i]
注意:这里num[j] < nums[i]其实不一定是<,也可能是<=,具体需要根据题目对于"上升"的定义来判断

下面这题出自LeetCode第211场周赛,先根据年龄排序,年龄相同再根据分数排序
假如年龄不同但是分数相同的话,根据题意,也能构成"上升"子序列,所以条件为p[j].score <= p[i].score

Java代码

class Solution {
    class Pair{
        int score;
        int age;
        public Pair(int score, int age){
            this.score = score;
            this.age = age;
        }
    }
    public int bestTeamScore(int[] scores, int[] ages) {
        int n = scores.length;
        Pair[] p = new Pair[n];
        for(int i = 0; i < n; i++){
            p[i] = new Pair(scores[i], ages[i]);
        }
        Arrays.sort(p, (o1,o2)->{
            if(o1.age == o2.age) return o1.score - o2.score;
            return o1.age - o2.age;
        });

        int[] f = new int[n];
        int res = 0;
        for(int i = 0; i < n; i++){
            f[i] = p[i].score;
            for(int j = 0; j < i; j++){
                if(p[j].score <= p[i].score){
                    f[i] = Math.max(f[i], f[j] + p[i].score);
                }
            }
            res = Math.max(res, f[i]);
        }
        return res;
    }
}

变形4. 二维LIS

  • 链接:LeetCode354.俄罗斯套娃信封问题

思路

二维LIS和一维没有什么区别,仍然需要对数组进行某一维的排序

为什么只需要排序其中一维即可呢?
根据题意,信封的长宽都必须严格大才能形成套娃信封,只要有其中一维不满足条件就一定不能套进去

Java代码

class Solution {
    public int maxEnvelopes(int[][] e) {
        if(e.length == 0 || e[0].length == 0) return 0;
        int n = e.length;
        Arrays.sort(e, (o1,o2)->o1[0]-o2[0]);
        int[] f = new int[n];
        int res = 0;
        for(int i = 0; i < n; i++){
            f[i] = 1;
            for(int j = 0; j < i; j++){
                if(e[j][0] < e[i][0] && e[j][1] < e[i][1]){
                    f[i] = Math.max(f[i], f[j] + 1);
                }
            }
            res = Math.max(res, f[i]);
        }
        return res;
    }
}

变形5. 三维LIS

  • 链接:面试题08.13.堆箱子

Java代码

class Solution {
    public int pileBox(int[][] box) {
        int n = box.length, m = box[0].length;
        // Arrays.sort(box, (o1,o2)->{
        //     if(o1[0] != o2[0]) return o1[0] - o2[0];
        //     else if(o1[1] != o2[1]) return o1[1] - o2[1];
        //     return o1[2] - o2[2];
        // });
        Arrays.sort(box, (o1,o2)->o1[2]-o2[2]);
        int[] f = new int[n];
        int res = 0;
        for(int i = 0; i < n; i++){
            f[i] = box[i][2];
            for(int j = 0; j < i; j++){
                if(box[j][0] < box[i][0] && box[j][1] < box[i][1] && box[j][2] < box[i][2]){
                    f[i] = Math.max(f[i], f[j] + box[i][2]);
                }
            }
            res = Math.max(res, f[i]);
        }
        return res;
    }
}

你可能感兴趣的:(最长上升子序列(LIS)模型)