巧妙求解最长单调递增子序列LIS序列本身的O(nlogn)的代码实现

文章详细解析了如何使用Dart语言,通过动态规划和O(nlogn)的时间复杂度来求解一个整数序列的最长单调递增子序列(LIS),并提供了从尾巴数组求解LIS序列本身的代码实现。这种方法包括一次倒序遍历,确保了LIS的正确性和效率。
摘要由CSDN通过智能技术生成

《算法导论》第15章动态规划的习题中,有道求一个N个数的序列的最长单调递增子序列的习题。

 该习题网上有各种实现,算法的时间复杂度有O(N^{2}),也有O(nlogn),有通过多重循环实现的,有动态规划实现的,这里都不再重复的。但看了很多网上的实现,都是求解了LIS的长度,并没有求解出LIS本身(即最长单调递增子序列本身)。

当然了,可以通过对原始序列A进行升序排序,得到{A}',然后利用书上教的算法,求解A和{A}'的LCS即可。但这种实现偏复杂,这里不做实现。

先感谢博主“

美利坚合众国圣安东尼奥马刺村”的博客:

最长递增子序列LIS的O(nlogn)的求法_最长单调递增子序列nlogn_美利坚合众国圣安东尼奥马刺村的博客-CSDN博客

 图文并茂的讲解了LIS长度的求法,这里不重复了。但求出长度、tails数组以后,如何求解LIS序列本身呢?下面给出代码实现。(代码由Dart实现。类似Java的语言,也是Flutter的实现语言)

// inconsecutive, O(nlgn)
// Return: the lis itself.
List<int> lis5(List<int> a) {
  var t = List.filled(a.length, 0), len = 0;
  for (var x in a) {
    var i = 0, j = len;
    while (i != j) {
      var m = (i + j) >> 1;
      if (t[m] < x) {
        i = m + 1;
      } else {
        j = m;
      }
    }
    t[i] = x;
    if (len == i) len++;
  }

// 以上求解出LIS的长度len;
// 以及tails数组t(t[i]表示所有长度为i+1的递增子序列的最小的尾元素)

// 下面,仅通过一次倒序遍历,求解出LIS序列本身;
  var p = len - 1, lis = List.filled(len, 0);
  for (var i = a.length - 1; i >= 0 && p >= 0; i--) {
    if (a[i] == t[p] || (a[i] > t[p] && a[i] < lis[p + 1])) lis[p--] = a[i];
  }
  return lis;
}

详细说下这3行代码:

var p = len - 1, lis = List.filled(len, 0);
for (var i = a.length - 1; i >= 0 && p >= 0; i--) {
  if (a[i] == t[p] || (a[i] > t[p] && a[i] < lis[p + 1])) lis[p--] = a[i];
}

首先定义p,指向tails数组t最后一个元素,定义LIS,长度为len,初始值设为0(实际什么值都可以)。

第二行,开始倒序遍历原始序列。为什么要倒序,因为结合tails数组,可以找出符合LIS序列的值。(读者可以自己思考或试试,正序遍历为何不行,哈哈)

关键的第三行,判断a[i]是否属于LIS,属于,则将其保持至LIS数组。说一下遍历可能遇到的情况:

  1. 当 i = a.length - 1 时,a[i]只有两种可能:要么等于 t[p](此时LIS的最后一个元素,这个可以通过反证法或tails数组本身的定义获知),要么a[i]小于t[p];大家可以思考一下为什么不会大于t[p]。这一点很关键,当a[i]小于t[p]时,for循环会继续遍历,直到找到第一个a[i]等于t[p]的那个值;并且,除非原始序列为空,否则一定存在这样的值,这点也很关键,在if 条件判断的 第一部分:
  2. a[i] == t[p]

    这个判断确保了后面 || 符号之后的 判断:

  3. (a[i] > t[p] && a[i] < lis[p + 1])

    中的lis[p+1]不会边界溢出。因为首次找到这个a[i]之前,上面这个判断不会执行。

  4. 找第一个a[i]等于t[p]的值以后,lis数组倒序插入第一个值(因为时倒序遍历,所以要倒序插入)。同时,tails数组的尾部指针p,向前移动一位(p--);
  5. lis[p--] = a[i];

    在上述代码执行后,这个就会确保第三行后续的判断条件 lis[p+1]不会边界溢出的对应的执行条件。第5点执行前,第3点判断不会生效。第一个值比较特殊,所以单独说这么多。

  6. 好,继续向前遍历。这时候如果a[i]符合在LIS序列中的条件,则符合下面两种情况:

(1)a[i] == t[p],和第2点相同,执行第5点。

(2)同第3点,(a[i] > t[p] && a[i] < lis[p + 1])

说明,上面第(1)小点容易理解;看第(2)小点,由于刚才插入LIS后,p已经减1了,所以刚才进入LIS的值为 lis[p+1];用反证法可以很容易证明,如果a[i]>=lis[p+1],那么就与LIS的定义不符了。并且a[i]还要满足a[i] > t[p]的条件(注意等于的情况已经在第(1)小点判断处理了),这个和求解tails本身的实现及tails的定义有关系:用于存储在tails[i]中,所有长度为i+1的递增子序列的最小的尾元素;注意这里的“最小的尾元素”的表述。举个例子就很容易明白了,比如下面序列A:

[10, 20, 30, 40, 1, 22];

tails数组为:[1, 20, 22, 40, 0, 0],而LIS为 [10, 20, 30, 40],在找到元素40以后,向前遍历,遇到30,30也是符合LIS的值,所以30一定小于40(废话);但30作为长度为3的递增子序列的值时,一定大于等于tails[2](即长度为3的子序列的最小的尾部),但等于的情况单独前置判断处理了,所以就只剩大于了。

读者可以自己再举其他例子,a[i]如果不符合上述(1)或(2)两种情况,则一定不会在LIS中。

至此,通过上述3行代码,一次遍历,就可求出LIS序列本身。如果不求LIS本身,而是求LIS中元素的位置,则将

if (a[i] == t[p] || (a[i] > t[p] && a[i] < lis[p + 1])) lis[p--] = a[i];

替换为:

// get the index of each element in LIS.
if (a[i] == t[p] || (a[i] > t[p] && a[i] < lis[p + 1])) lis[p--] = i;

即可。

最后:时间复杂度,求解LIS序列本身的代码,仅一次倒序遍历,时间复杂度为O(N),加上前述求解len和tails的O(nlogn),整体复杂度仍为O(nlogn)。

验证:

void main() {
  // var a = [8, 6, 7, 5, 1, 2, 3, 8, 9, 2];
  // var a = [8, 3, 4, 7, 5, 2, 6, 1];
  var a = [10, 20, 30, 40, 1, 22];
  // var a = [9];
  // var a = [1, 2, 3, 4];
  // var a = [8, 6, 7];
  // var a = <int>[];

  var sq = lis5(a);
  print(sq);
  print(sq.length);
}

输出如下:

[10, 20, 30, 40]
4

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值