求解数组的最长子序列(贪心 + 二分)

541 阅读3分钟

如果我们有一个 HTML 的列表,现在我们对列表的某些元素进行了更新,因为操作 DOM 是一个比较消耗性能的事情,所以我们想尽可能少移动 DOM 来完成更新:

更新前:


<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>

我们想更新成:

<li>1</li>
<li>3</li>
<li>4</li>
<li>2</li>
<li>5</li>

为了方便描述,我们把上面两段 DOM 记作数组:前者 [1, 2, 3, 4, 5],记作 a;后者 [1, 3, 4, 2, 5],记作 b。

解决上述的问题的一种思路是:先求出变化后数组的最长子序列,然后保持最长子序列不动,只移动最长子序列之外的元素到它应该的位置。

在我们的示例中,数组 b 的长度比较短,我们可以通过观察看出它的最长子序列的长度是 3,它的最长子序列可以是 [1, 2, 5] ,也可以是 [3, 4, 5]。求出子序列之后,我们只需要移动 a 数组的另外两个不在最长子序列中的元素到指定位置,就能把它变为 b 数组了。利用这样的思路,我们可以就能只移动非常少的元素,就得到目标数组了。

希望我已经介绍清楚了最长子序列的一个应用场景,现在我们来讨论一下,如何求解数组的最长子序列。

这个问题一般有两个解决思路,一种是使用动态规划,一种是使用贪心加二分的方法。本文讲解的是后者。同时,您可以点击到达对应的: LeetCode 问题

我们示例的数组是:[10,9,2,5,3,7,101,18]

思路讲解

最开始,我们先讲解思路,为了方便大家理解,我们还是拿图说话:

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

讲解

看完上面过程的同学心里应该对我们要做什么有数了,我们再总结一下。

我们在循环中,更新最长子序列的策略有两个,如果我们当前数组的遍历到的值是 target

  1. 如果大于最长子序列的最大值,就把 target 加到当前最长子序列后面
  2. 如果不大于的话,就找到第一个大于或等于 target 的索引,将最长子序列数组对应索引的元素更新为 target

为了提高算法效率,在满足第 2 点的时候,我们会使用二分查找去找到目标值。明白了这两点,我们最后来看一下算法:

public class Solution {
    public int lengthOfLIS(int[] nums) {
        if (nums.length == 0) {
            return 0;
        }

        int[] tails = new int[nums.length];
        int LISIndex = 0;
        tails[0] = nums[0];

        for (int i = 1; i < nums.length; i++) {
            int tail = tails[LISIndex];
            if (tail < nums[i]) {
               LISIndex++;
               tails[LISIndex] = nums[i];
           } else if(tail > nums[i]) {
               searchFirstBiggerAndReplace(tails, 0, LISIndex, nums[i]);
           }
        }

        return LISIndex + 1;
    }

    private void searchFirstBiggerAndReplace(
            int[] tails,
            int lo,
            int hi,
            int target
    ) {
        if (lo >= hi) {
            tails[hi] = target;
            return;
        }

        int mid = lo + (hi - lo) / 2;

        if (tails[mid] < target) {
            searchFirstBiggerAndReplace(tails, mid + 1, hi, target);
        } else {
            searchFirstBiggerAndReplace(tails, lo, mid, target);
        }
    }

    public static void main(String[] args) {
        int[] nums = new int[]{4,10,4,3,8,9};
        Solution solution = new Solution();
        System.out.println(solution.lengthOfLIS(nums));
    }
}