最长递增子序列(LIS)问题的O(NlogN)算法思路

文章目录

      • O(N^2)的动态规划解法
      • 优化:O(NlogN)的算法
      • 最优解法
      • 参考资料

O(N^2)的动态规划解法

很容易就能想到,如果用 d p i dp_i dpi 代表以 i i i 结尾的LIS长度,用 A i A_i Ai 代表数组第 i i i 项,可以写出以下式子:
d p i = m a x   { d p j + 1 }   f o r   j < i   a n d   A j < A i dp_i = max \ \{dp_j + 1\} \ for \ j < i \ and \ A_j<A_i dpi=max {dpj+1} for j<i and Aj<Ai
最大的 d p i dp_i dpi 就是我们需要的解,代码如下:

    public int findLongest(int[] array) {
        // init
        int n = array.length;
        int[] dp = new int[n];
        for (int i = 0; i < n; i++) dp[i] = 1;

        // compute dp[i]
        for (int i = 1; i < n; i++) {
            int maxLen = 1;
            for (int j = 1; j < i; j++) {
                if (array[j] < array[i] && dp[j] > maxLen) {
                    maxLen = dp[j];
                }
            }
            dp[i] = maxLen + 1;
        }

        // find max dp[i]
        int lis = 1;
        for (int i : dp) {
            if (i > lis) lis = i;
        }

        return lis;
    }

但是,由于计算dp过程中中间内层循环的存在,使得我们的算法复杂度变成了O(N^2),我们想,是否可以通过某种方式消去内层循环。实际上内层循环所作的事是尽可能扩展前面得到的LIS,要进行这样的扩展,需要满足已经提到的两个条件:

  1. A_j
  2. m a x   { d p j + 1 } max \ \{dp_j + 1\} max {dpj+1}

也就是说对于每一个“新来的”array[i],要在符合上升序列的前提下,找到一个最长的可扩展序列去扩展,我们的内层循环就是进行一个这样的可扩展序列的搜索,那我们可能会想利用二分搜索之类的方式把这个搜索过程从O(N)优化到O(logN),由此可以得到更优的算法:

优化:O(NlogN)的算法

那么怎么找到一个可以扩展的序列呢?思路是这样的:

  1. 有没有长度为1的序列可供扩展?如果可以扩展,那么这个序列的末尾一定要比array[i]要小,那么其实只需要找到所有长度为1的序列中,末尾最小的那个,如果最小的末尾比array[i]小那么一定可以扩展
  2. 有没有长度为2的序列可供扩展?如果可以扩展,那么这个序列的末尾一定要比array[i]要小,那么其实只需要找到所有长度为2的序列中,末尾最小的那个,如果最小的末尾比array[i]小那么一定可以扩展
  3. 有没有长度为3的序列可供扩展?如果可以扩展,那么这个序列的末尾一定要比array[i]要小,那么其实只需要找到所有长度为3的序列中,末尾最小的那个,如果最小的末尾比array[i]小那么一定可以扩展

……
我们需要扩展上面那些可扩展序列中最长的那个,这可以通过引入一个辅助数组来通过二分搜索查到,这个辅助数组(叫minEnd[])中,minEnd[k]存储长度为k - 1的LIS序列的最小末尾,而minEnd[]是递增的(简单的反证法就可以知道这一点),那我们可以在minEnd[]中二分搜索array[i],就可以找到最长的那个可扩展序列的长度,这个长度再加1就是dp[i],根据这个思路,代码如下:

首先一个辅助的二分查找函数,这个函数的解释参见我的第一篇博文:

    private int binarySearch(int[] array, int n, int key) {
        int first = 0, last = n;
        while (first < last) {
            int mid = first + (last - first) / 2;
            if (array[mid] < key) {
                first = mid + 1;
            } else {
                last = mid;
            }
        }
        return first;
    }
    public int findLongestBetter(int[] array) {
        // init
        int n = array.length;
        int[] dp = new int[n];
        int[] minEnd = new int[n];
        for (int i = 0; i < n; i++) {
            minEnd[i] = Integer.MAX_VALUE;
        }


        // compute dp[i]
        for (int i = 0; i < n; i++) {
            // binary search expanded LIS and set dp[i]
            int expandedLen = binarySearch(minEnd, i, array[i]) - 1 + 1; // which length should expand?
            dp[i] = expandedLen + 1;

            // don't forget to update minEnd[]
            minEnd[expandedLen] = array[i];
        }

        // find max dp[i]
        int lis = 1;
        for (int i : dp) {
            if (i > lis) lis = i;
        }

        return lis;
    }

最优解法

你可能发现了,其实dp[]现在已经没用了,我们只需要minEnd[]就可以了,只要返回minEnd[]中不是Integer.MAX_VALUE的最后一个位置就可以了,我们可以聪明一点,记录更新的minEnd[]的最后一个位置,这样可以避免对minEnd[]再进行一次搜索:

    public int findLongestBest(int[] array) {
        // init
        int n = array.length;
        int[] minEnd = new int[n];
        for (int i = 0; i < n; i++) {
            minEnd[i] = Integer.MAX_VALUE;
        }

        int end = 0;
        // compute dp[i]
        for (int i = 0; i < n; i++) {
            // binary search expanded LIS
            int expandedLen = binarySearch(minEnd, i, array[i]); // which length should expand?

            // update minEnd[]
            minEnd[expandedLen] = array[i];

            // set end
            end = expandedLen > end ? expandedLen : end;
        }

        return end + 1;
    }

参考资料

https://www.nowcoder.com/questionTerminal/585d46a1447b4064b749f08c2ab9ce66

什么是动态规划?动态规划的意义是什么? - 徐凯强 Andy的回答 - 知乎

你可能感兴趣的:(算法)