首页 > 试题广场 >

最长递增子序列

[编程题]最长递增子序列
  • 热度指数:6515 时间限制:C/C++ 2秒,其他语言4秒 空间限制:C/C++ 256M,其他语言512M
  • 算法知识视频讲解
给定数组arr,设长度为n,输出arr的最长递增子序列。(如果有多个答案,请输出其中字典序最小的)

输入描述:
输出两行,第一行包括一个正整数n(n<=100000),代表数组长度。第二行包括n个整数,代表数组arr 


输出描述:
输出一行。代表你求出的最长的递增子序列。
示例1

输入

9
2 1 5 3 6 4 8 9 7

输出

1 3 4 8 9
示例2

输入

5
1 2 8 6 4

输出

1 2 4

说明

其最长递增子序列有3个,(1,2,8)、(1,2,6)、(1,2,4)其中第三个字典序最小,故答案为(1,2,4)

备注:
时间复杂度,空间复杂度
题目要求时间复杂度为O(logN),需要使用左神书上构建单调数组ends的方法,这个方法我已经在第29题“信封嵌套问题”中做了解释,本题就不多说了。本题主要想讨论一下拿到最长自增子序列的长度后怎么还原这个最长递增子序列。与单纯求长度的流程稍有不同,如果需要还原子序列,我们还需要一个变量maxIdx来记录当取得最长递增子序列的长度值时,是以原数组中哪个位置的元素结尾的。我们通过这个位置来进行倒推,就能逐步还原出整个最长自增子序列。流程如下:
  1. 准备一个长度为maxLen的数组,这时候我们要填最后一个元素,由于arr[maxIdx]是最长递增子序列的最后一个元素,所以先把它填进数组的末尾res[maxLen - 1]。然后maxLen自减(因为已经填完一个元素了)继续往maxIdx的左边前遍历dp数组。
  2. 如果遇到dp[i]=maxLen,说明遇到了一个递增子序列长度为maxLen的结尾,再次把arr[i]填入数组的末尾res[maxLen - 1]然后maxLen自减继续往前遍历dp数组,周而复始就能够填完整个数组。
下面解释一下为什么这样可以还原出字典序最小的最长递增子序列,拿题目提供的示例2来进行说明。原始数组为:1 2 8 6 4,dp数组为:1 2 3 3 3。最长递增子序列的长度为3,我们先把4填在末尾,然后再也不用去修改这个递增长度为3的末尾了,我们只管专心找递增长度为2的结尾填在4的前面。那么我们担心的情况无非就是4的前面会不会有一个比它小的数字,它仍然是某个递增长度为3的子序列末尾,这就使得它比4更适合成为长度为3的递增子序列的末尾。这显然是不可能的,如果它比4小,且为递增长度为3的序列,那4就可以接在它的后面凑出一个递增长度为4的序列,这与dp数组中4对应的值为3是矛盾的,4前面比它小的数一定是更短递增子序列的结尾。
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());
        String[] strArr = br.readLine().split(" ");
        int[] arr = new int[n];
        for(int i = 0; i < n; i++){
            arr[i] = Integer.parseInt(strArr[i]);
        }
        int[] dp = new int[n];
        int[] ends = new int[n];
        dp[0] = 1;
        ends[0] = arr[0];
        int tail = 0;      // 有效区域末尾
        int maxLen = 1, maxIdx = 0;    // 最长递增子序列长度和index
        for(int i = 1; i < n; i++){
            int index = lowerbound(ends, 0, tail, arr[i]);
            dp[i] = index + 1;           // 以i结尾的最长递增子序列长度为index+1
            ends[index] = arr[i];
            if(index > tail){
                tail++;                  // 有效区扩充
            }
            if(dp[i] >= maxLen){
                maxLen = dp[i];
                maxIdx = i;
            }
        }
        // 还原字典序最小的最长递增子序列
        int[] increasingSeq = new int[maxLen];
        for(int i = maxIdx; i >= 0; i--){
            // 从后往前更新,遇到递增子序列长度为maxLen的arr[i]就更新结尾位置,然后把maxLen缩短
            if(dp[i] == maxLen){
                increasingSeq[--maxLen] = arr[i];
            }
        }
        for(int i = 0; i < increasingSeq.length; i++){
            System.out.print(increasingSeq[i] + " ");
        }
    }
    
    private static int lowerbound(int[] arr, int L, int R, int target) {
        int left = L, right = R;
        int index = R + 1;
        while(left <= right){
            int mid = left + ((right - left) >> 1);
            if(arr[mid] < target){
                left = mid + 1;
            }else{
                index = mid;
                right = mid - 1;
            }
        }
        return index;
    }
}

编辑于 2021-12-10 10:56:26 回复(1)