Java动态规划问题(三)最长递增子序列

【题目】

给定数组arr,返回arr的最长递增子序列。

【举例】

arr=[2,1,5,3,6,4,8,9,7],返回的最长递增子序列为{1,3,4,8,9}。

【要求】

如果arr长度为N,请实现时间复杂度为O(N^2),O(NlogN)的方法。

【解答】

时间复杂度为O(N^2)的方法实现:

1、生成长度为N的数组dp,dp[i]表示在以arr[i]这个数结尾的情况下,arr[0…i]中的最大递增子序列长度。
2、对第一个数arr[0]来说,令dp[0]=1,接下来从左到右依次算出以每个位置的数结尾的情况下,最长递增子序列长度。
3、假设计算到位置i,求以arr[i]结尾情况下的最长递增子序列长度,即dp[i]。如果最长递增子序列以arr[i]结尾,那么在arr[0…1]中所有比arr[i]小的数都可以作为倒数第二个数。在这么倒数第二个数的选择中,以哪个数结尾的最大递增子序列更大,就选哪个数作为倒数第二个数,所以dp[i]=max{dp[i]+1(0<=j<i, arr[j]<arr[i]},如果arr[0…i-1]中所有的数都不比arr[i]小,令dp[i]=1即可,说明以arr[i]结尾情况下的最长递增子序列只包含arr[i]。
借鉴了 https://blog.csdn.net/qq_41765114/article/details/88415541
在这里插入图片描述
同理可得之后的dp数组数值。
按照步骤1~步骤3可以计算出dp数组,具体过程请参看如下代码中的getdp1方法

public int[] getdp1(int[] arr){
  int[] dp = new int[arr.length];
  for(int i = 0; i<arr.length; i++){
    dp[i]=1;
    for(int j = 0;j<i;j++){
      if(arr[i] > arr[j]){
      dp[i] = Math.max(dp[i],dp[j]+1);
      }
    }
  }
  return dp;
}

接下来解释如何根据求出的dp数组得到最长递增子序列。以题目的例子来说明,arr=[1,5,3,6,4,8,9,7], 求出的数组dp=[1,1,2,2,3,3,4.5,4]。

1.遍历dp数组,找到最大值以及位置。在本例中,最大值为5,位置为7,说明最终的最
长递增子序列的长度为5,并且应该以arr[7]这个数(arr[7]=9) 结尾。

2.从arr 数组的位置7开始从右向左遍历。如果对某一个位置i,既有arr[i]<arr[7],又有
dp[i]=dp[7]-1,说明arr[i]可以作为最长递增子序列的倒数第二个数。在本例中,arr[6]<arr[7],并且dp[6]=dp[7]-1,所以8应该作为最长递增子序列的倒数第二个数。

3.从arr数组的位置6开始继续向左遍历,按照同样的过程找到倒数第三个数。在本例中,位置5满足arr[5]<arr[6]并且dp[5]=dp[6]-1,同时位置4也满足。选arr[5]或者arr[4]作为倒数第三个数都可以。

4.重复这样的过程,直到所有的数都找出来。

public int[] generateLIS(int[] arr, int[] dp){
  int len = 0;
  int index = 0;
  for(int i =0;i<dp.length;i++){
    if(dp[i]>len){
      len = dp[i];
      index = i;
      }
  }
  int[] lis = new int[len];
  lis[--len] = arr[index];
  for(int i = index;i>=0;i--){
    if(arr[i]<arr[index] && dp[i] == dp[index]-1){
      lis[--len] = arr[i];
      index = i;
    }
  }
return lis;
}

整个过程的主方法如下:

public int[] list1(int[] arr){
  if(arr == null || arr.length == 0){
    return null;
  }
  int[] dp = getdp1(arr);
  return generateLIS(arr,dp);
}

【复杂度分析】

计算dp数组过程的时间复杂度为O(N^2),根据dp数组得到最长递增子序列过程的时间复杂度为O(N),所以整个过程时间复杂度为O(N2)。

时间复杂度为O(NlogN)的方法实现:

通过dp数组的过程是利用利用二分查找来进行的优化。先生成一个长度为N的数组ends,初始时ends[0]=arr[0],其他位置上的值为0。生成整型变量right,初始时right = 0。
//在从左到右遍历arr数组的过程中,求解dp[i]过程需要使用ends数组和right变量。
遍历过程中,ends[0…right]为有效区,ends[right+1…N-1]为无效区。对有效区上的位置b,如果有ends[b]=c,则表示遍历到目前为止,在所有长度为b+1的递增序列中,
最小的结尾数是c。无效区的位置则没有意义。
比如,arr=[2,1,5,3,6,4,8,9,7],初始时,dp[0]=1,ends[0]=2, right=0。ends[0…0]为有效区,ends[0]=2的含义是,在遍历过arr[0]之后,所有长度为1的递增序列中(此时只有[2]),最小的结尾数是2。之后的遍历继续用这个例子来说明求解过程。
在这里插入图片描述
两种情况在这里插入图片描述
具体代码如下:

public int[] getdp2(int[] arr){
//初始化
  int[] dp = new int[arr.length];
  int[] ends = new int[arr.length];
  ends[0] = arr[0];
  dp[0] = 1;
  int right = 0;
  int l = 0;
  int r = 0;
  int m = 0;
  for(int i = 1;i<arr.length;i++){
    l = 0;
    r = right;
    while(l<=r){
      m=(l+r)/2;
      if(arr[i]>ends[m]){
        l = m+1;
      }else{
      r = m-1;
      }
    }
    right = Math.max(right,l);
    ends[l] = arr[i];
    dp[i] = l+1;
  }
  return dp;
}

整个过程的主方法如下:

public int[] list2(int[] arr){
  if(arr == null || arr.length == 0){
    return null;
  }
  int[] dp = getdp2(arr);
  return generateLIS(arr,dp);
}

猜你喜欢

转载自blog.csdn.net/sand_wich/article/details/105530858