题解 | #最长递增子序列#

最长递增子序列

http://www.nowcoder.com/practice/9cf027bf54714ad889d4f30ff0ae5481

eachMaxSequenceLength 即上面的 dp 数组,表示以第 i 个元素结尾的最长递增子序列的长度

maxSequence 表示遍历数组时候存储的”伪最长递增子序列“,后面会解释为什么说是”伪“

首先对上面两个变量进行初始化,即将数组的第一个元素塞进 maxSequence ,对应的 eachMaxSequenceLength [0] = 1

然后对数组进行遍历,规则如下:

  • 如果正在遍历的元素比 maxSequence 最后一个元素大,代表找到了更长的递增子序列,所以将当前元素赛进 maxSequence 末尾,eachMaxSequenceLength [i] 的值同步为 maxSequence 的长度
  • 如果正在遍历的元素比 maxSequence 最后一个元素小,表明当前元素对于原有最长递增子序列(也就是现在的 maxSequence)再增长不会有贡献,但是它可能会为后面新的最长递增子序列做贡献,所以要将当前元素赛进 maxSequence ,规则是从 maxSequence 最左开始找到第一个不小于当前元素的位置(由于maxSequence 是有序的,所以这里可以用二分法),用当前元素替换掉它,当前元素在 maxSequence 的索引位置也就是以当前元素结尾最长递增子序列的长度

就这两个规则,一直遍历结束,也就得到了 eachMaxSequenceLength,利用该数组即可得到最长递增子序列。

根据前面的遍历规则,我们理解为什么说 maxSequence 是”伪“最长递增子序列了,因为按照第二个规则,在最长递增子序列右侧的元素都会被替换到它的左侧,那显然替换后的 maxSequence 都没按照数组元素原顺序排列,所以说 maxSequence 是”伪“的,但是 maxSequence 却发挥了它的作用:指导求 eachMaxSequenceLength

对于 [1,2,8,6,7,8,9],假设按我们的步骤走到了如下状态:

maxSequence = [1, 2, 8]
eachMaxSequenceLength = [1, 2, 3]

当前遍历到了第四个元素:6,这个时候 maxSequence 还没变“伪”,至少目前它还是纯正的最长递增子序列,现在想求以 6 结尾的最长递增子序列长度,我们可以直接看 maxSequence 有多少个小于 6 的数,因为 maxSequence 出现的数一定都是在 6 的左边,也就是满足序列顺序要求的,通过 maxSequence 很快可以求得以 6 结尾的最长递增子序列长度是 3。

这个时候 6 要怎么处理呢?不能丢掉,因为后面可能还有递增序列依赖这个 6,所以要想办法把它放进 maxSequence。让它替换了 2 的位置,也就变成了:

maxSequence = [1, 6, 8]
eachMaxSequenceLength = [1, 2, 3, 2]

按照我们的替换原则,我们可能有两点担心,担心过去和未来:

  • 6 加入了 maxSequence,它就不再是“序列”了,不会影响后面继续求比 8 更大的序列吗?
  • 6 加入了 maxSequence,后面如果有依赖 6 的递增序列怎么办?

仔细思考一下,这两个问题都不是问题!

首先,如果后续有比 8 大的数,我们仍然可以按照前面定的第一个规则得到最大递增子序列长度加 1。可能会有疑惑:1 6 8 在数组中不是序列,怎么还能这样算?别忘了,6 是替换了之前存在的元素的,虽然 1 6 8 序列是不对的,但是它们的长度是没问题的,所以说 maxSequence 是“伪”序列,但是也能其指导作用。

第二个担心,后续如果有依赖 6 的递增序列,比如是 7 ,它会按照一样的规则替换掉 8 ,我们会发现它和 6 的相对顺序是没有变化的,所以也不影响后续求新的递增序列长度!

综上,通过 maxSequence 这个“伪”最大递增子序列的构建规则,我们实现了 O(N*logN) 的时间复杂度,比动态规划快!

public int[] LIS(int[] arr) {
    if (arr == null || arr.length < 2) {
        return arr;
    }

    int n = arr.length;
    // eachMaxSequenceLength[i] 表示以第 i 个元素结尾的最长递增子序列的长度
    int[] eachMaxSequenceLength = new int[n];
    eachMaxSequenceLength[0] = 1;
    // maxSequence 表示使用贪心遍历时的最长递增子序列
    List<Integer> maxSequence = new ArrayList<>();
    maxSequence.add(arr[0]);
    for (int i = 1; i < n; i++) {
        if (arr[i] > maxSequence.get(maxSequence.size() - 1)) {
            // 最长递增子序列长度可以再加 1
            maxSequence.add(arr[i]);
            eachMaxSequenceLength[i] = maxSequence.size();
        } else {
            // 在 maxSequence 中找到第一个不小于 arr[i] 的位置,并替换它
            int index = find(maxSequence, arr[i], 0, maxSequence.size()-1);
            maxSequence.set(index, arr[i]);
            eachMaxSequenceLength[i] = index + 1;
        }
    }
    // 根据 maxSequenceLength 和 eachMaxSequenceLength 来找到最长递增子数组
    int[] res = new int[maxSequence.size()];
    int index = maxSequence.size();
    for (int i = n - 1; index > 0; i--) {
        if (eachMaxSequenceLength[i] == index) {
            // 每个最大长度取 eachMaxSequenceLength 数组中最右侧的值,可以反证
            res[--index] = arr[i];
        }
    }
    return res;
}

// 在 [left, right] 范围内找到最左边不小于 traget 的位置
private int find(List<Integer> maxSequence, int target, int left, int right) {
    if (left == right) {
        return left;
    }

    int mid = left + (right - left) / 2;
    if (maxSequence.get(mid) == target) {
        return mid;
    } else if (maxSequence.get(mid) > target) {
        return find(maxSequence, target, left, mid);
    } else {
        return find(maxSequence, target, left + 1, right);
    }
}
全部评论

相关推荐

点赞 评论 收藏
转发
点赞 收藏 评论
分享
牛客网
牛客企业服务