Skip to content

最长递增子序列

基本介绍

最长递增子序列(Longest Increasing Subsequence,简称 LIS)是计算机科学中一个经典的算法问题。这看上去是很难的一个词语,遇到这种词,最简单的方法就是拆词,这里可以拆为 3 个词:最长递增子序列

  1. 子序列

    js
    [1, 2, 3, 4, 5]

    子序列有多个:

    js
    [1, 2, 3]
    [1, 3]
    [2, 4, 5]
  2. 递增

    js
    [2, 1, 5, 3, 6, 4, 8, 9, 7]

    这个子序列里面的元素必须是递增的:

    js
    [1, 5] // 子序列,并且是递增的
    [1, 3, 6] // 子序列,并且是递增的
    [2, 1, 5] // 子序列,但是不是递增的
  3. 最长

    相当于在上面的基础上,有增加了一个条件,需要是最长的、递增的子序列

    js
    [2, 1, 5, 3, 6, 4, 8, 9, 7]

    最长递增子序列:

    js
    [1, 3, 4, 8, 9]
    [1, 3, 6, 8, 9]
    [1, 5, 6, 8, 9]
    [2, 3, 4, 8, 9]
    [2, 3, 6, 8, 9]
    [2, 5, 6, 8, 9]

    可以看出,即便是最长递增子序列,仍然是可以有多个的。在开发中,不同的算法可能拿到不一样的结果,不过一般拿到其中一个最长递增子序列即可。

实际意义

  • 股票趋势分析
  • 手写识别
  • 文本编辑和版本控制
  • ....

暴力法

暴力法的核心思想是:找到所有的递增子序列,然后从中找到长度最长的那一个。

js
function getSequence(arr) {
  let maxLength = 0; // 记录最长递增子序列的长度
  let longetSeq = []; // 记录最长递增子序列

  /**
   *
   * @param {*} index 列表的下标
   * @param {*} subSeq 当前递增子序列
   */
  function findSubsequence(index, subSeq) {
    let currentNum = arr[index]; // 当前元素
    // 先把之前的递增子序列展开,再加上当前元素
    let newSeq = [...subSeq, currentNum]; // 新的递增子序列

    // 遍历下标之后的内容
    for (let i = index + 1; i < arr.length; i++) {
      // 遍历当前下标之后的元素时,发现有比当前元素大的元素
      if (arr[i] > currentNum) {
        findSubsequence(i, newSeq);
      }
    }

    // 每一次递归结束后,就会得到一个新的递增子序列
    // 相当于找到了所有的递增子序列
    // console.log("newSeq:", newSeq);

    if (newSeq.length > maxLength) {
      maxLength = newSeq.length;
      longetSeq = newSeq;
    }
  }

  for (let i = 0; i < arr.length; i++) {
    findSubsequence(i, []);
  }

  return longetSeq;
}

const list = [2, 1, 5, 3, 6, 4, 8, 9, 7];
const result = getSequence(list);
console.log(result); // [2, 5, 6, 8, 9]

动态规划

动态规划(Dynamic Programming)的核心思想是利用问题的最优子结构重叠子问题特性,将复杂问题分解为更小的子问题,并且在解决这些子问题的时候会保存子问题的解,避免重复计算,从而高效地求解原问题。

js
function getSequence(arr) {
  let maxLength = 0; // 记录最长递增子序列的长度
  let maxSeq = []; // 记录最长递增子序列

  let sequences = new Array(arr.length).fill().map(() => []);

  //   console.log(sequences);

  // 遍历数组
  for (let i = 0; i < arr.length; i++) {
    // 创建一个以当前元素为结尾的递增子序列
    let seq = [arr[i]];
    // 遍历之前的元素,找到比当前元素小的元素,从而构建递增子序列
    for (let j = 0; j < i; j++) {
      if (arr[j] < arr[i]) {
        // 把之前存储的序列和当前元素拼接起来
        seq = sequences[j].concat(arr[i]);
      }
    }

    // 将当前递增子序列存储起来
    sequences[i] = seq;

    // 更新最大的序列
    if (seq.length > maxLength) {
      maxLength = seq.length;
      maxSeq = seq;
    }
  }
  //   console.log(sequences);
  return maxSeq;
}

const list = [2, 1, 5, 3, 6, 4, 8, 9, 7];
const result = getSequence(list);
console.log(result); // [ 1, 3, 4, 8, 9 ]

Vue3中的算法

Vue3 中获取最长递增子序列,用到了 贪心二分 查找。

js
function getSequence(arr) {
  // 用于记录每个位置的前驱索引,以便最后重建序列
  const p = arr.slice();
  // 存储当前找到的最长递增子序列的索引
  const result = [0];
  // 声明循环变量和辅助变量
  let i, j, u, v, c;
  // 获取输入数组的长度
  const len = arr.length;
  // 遍历输入数组
  for (i = 0; i < len; i++) {
    const arrI = arr[i];
    // 忽略值为 0 的元素(Vue源码中的diff算法对0有特定处理)
    if (arrI !== 0) {
      // 获取当前最长序列中最后一个元素的索引
      j = result[result.length - 1];
      // 贪心算法部分:如果当前元素大于当前最长序列的最后一个元素,直接添加
      if (arr[j] < arrI) {
        // 记录当前元素的前驱索引为 j
        p[i] = j;
        // 将当前元素的索引添加到 result 中
        result.push(i);
        continue;
      }
      // 二分查找部分:在 result 中寻找第一个大于等于 arrI 的元素位置
      u = 0;
      v = result.length - 1;
      while (u < v) {
        // 取中间位置
        c = ((u + v) / 2) | 0;
        // 比较中间位置的值与当前值
        if (arr[result[c]] < arrI) {
          // 如果中间值小于当前值,搜索区间缩小到 [c + 1, v]
          u = c + 1;
        } else {
          // 否则,搜索区间缩小到 [u, c]
          v = c;
        }
      }
      // 如果找到的值大于当前值,进行替换
      if (arrI < arr[result[u]]) {
        // 如果 u 不为 0,记录前驱索引
        if (u > 0) {
          p[i] = result[u - 1];
        }
        // 更新 result 中的位置 u 为当前索引 i
        result[u] = i;
      }
    }
  }
  // 重建最长递增子序列
  u = result.length;
  v = result[u - 1];
  while (u-- > 0) {
    // 将索引替换为对应的前驱索引
    result[u] = v;
    v = p[v];
  }
  // 返回最长递增子序列的索引数组
  return result;
}

追踪流程:

  1. 初始化:

    • p = [2, 1, 5, 3, 6, 4, 8, 9, 7] 用于记录每个元素的前驱索引,初始为原数组的副本。
    • result = [0] 初始化结果数组,开始时只包含第一个元素的索引 0。
  2. 遍历数组:

    • i = 0, arrI = 2 第一个元素,索引已在 result 中,继续下一次循环。

    • i = 1, arrI = 1

      • arr[result[result.length - 1]] = arr[0] = 2
      • arrI (1) < 2,需要二分查找替换位置。
      • 二分查找 (u = 0, v = 0):
        • c = 0
        • arr[result[0]] = 2 > arrI (1)
        • v = c = 0
      • arrI (1) < arr[result[u]] (2),替换 result[0] = 1
      • 更新 result = [1]
    • i = 2, arrI = 5

      • arr[result[result.length - 1]] = arr[1] = 1
      • arrI (5) > 1,贪心算法:直接添加到 result
      • p[2] = 1
      • result.push(2)
      • 更新 result = [1, 2]
    • i = 3, arrI = 3

      • arr[result[result.length - 1]] = arr[2] = 5
      • arrI (3) < 5,需要二分查找。
      • 二分查找 (u = 0, v = 1):
        • c = 0
        • arr[result[0]] = arr[1] = 1 < arrI (3)
        • u = c + 1 = 1
        • arr[result[1]] = arr[2] = 5 > arrI (3)
        • v = c = 1
      • arrI (3) < arr[result[u]] (5),替换 result[1] = 3
      • p[3] = result[0] = 1
      • 更新 result = [1, 3]
    • i = 4, arrI = 6

      • arr[result[result.length - 1]] = arr[3] = 3
      • arrI (6) > 3,贪心算法:直接添加到 result
      • p[4] = 3
      • result.push(4)
      • 更新 result = [1, 3, 4]
    • i = 5, arrI = 4

      • arr[result[result.length - 1]] = arr[4] = 6
      • arrI (4) < 6,需要二分查找。
      • 二分查找 (u = 0, v = 2) :
        • c = 1
        • arr[result[1]] = arr[3] = 3 < arrI (4)
        • u = c + 1 = 2
        • arr[result[2]] = arr[4] = 6 > arrI (4)
        • v = c = 2
      • arrI (4) < arr[result[u]] (6),替换 result[2] = 5
      • p[5] = result[1] = 3
      • 更新 result = [1, 3, 5]
    • i = 6, arrI = 8

      • arr[result[result.length - 1]] = arr[5] = 4
      • arrI (8) > 4,贪心算法:直接添加到 result
      • p[6] = 5
      • result.push(6)
      • 更新 result = [1, 3, 5, 6]
    • i = 7, arrI = 9

      • arr[result[result.length - 1]] = arr[6] = 8
      • arrI (9) > 8,贪心算法:直接添加到 result
      • p[7] = 6
      • result.push(7)
      • 更新 result = [1, 3, 5, 6, 7]
    • i = 8, arrI = 7

      • arr[result[result.length - 1]] = arr[7] = 9
      • arrI (7) < 9,需要二分查找。
      • 二分查找 (u = 0, v = 4) :
        • c = 2
        • arr[result[2]] = arr[5] = 4 < arrI (7)
        • u = c + 1 = 3
        • c = 3
        • arr[result[3]] = arr[6] = 8 > arrI (7)
        • v = c = 3
      • arrI (7) < arr[result[u]] (8),替换 result[3] = 8
      • p[8] = result[2] = 5
      • 更新 result = [1, 3, 5, 8, 7]
  3. 重建序列:

    • u = result.length = 5
    • v = result[u - 1] = result[4] = 7
    • 迭代过程:
      • result[4] = v = 7
      • v = p[7] = 6
      • result[3] = v = 6
      • v = p[6] = 5
      • result[2] = v = 5
      • v = p[5] = 3
      • result[1] = v = 3
      • v = p[3] = 1
      • result[0] = v = 1
      • v = p[1]p[1] 初始为 1)
    • 最终 result = [1, 3, 5, 6, 7]
  4. 映射回原数组的值:

    • result.map(index => list[index]) 得到 [1, 3, 4, 8, 9]
    • 这是输入数组中的一个最长递增子序列

-EOF-

Released under the MIT License.