LIS 与 LCS

最长上升子序列(LIS)

模板

给定长度为 \(n(1 \leqslant n \leqslant 10^5)\) 的数组 \(a\),求出 \(a\) 的最长上升子序列。

暴力

  • \(dp_i\) 表示以 \(a_i\) 结尾的最长上升子序列。
  • 我们显然可以得到如下转移: \(dp_{i} = \max\limits_{1 \leqslant j < i, a_j < a_i}\{dp_j\} + 1\),若不存在满足要求的 \(j\)\(dp_i = 1\)

时间复杂度

状态数:\(O(n)\),转移数:\(O(n^2)\),总时间复杂度:\(O(n ^ 2)\)

二分优化

  • \(f_i\) 表示一个长度为 \(i\) 上升子序列末尾元素的最小值。
  • 通过贪心可以发现 \(f_i < f_{i + 1}\),因为假设某个长度为 \(i\) 且末尾元素为 \(f_i\) 的上升子序列倒数第二个数为 \(x\),那么显然存在 \(x < f_i\),而肯定这个上升子序列的前 \(i - 1\) 个构成一个长度 \(i - 1\) 的上升子序列,那么就有 \(f_{i - 1} \leqslant x < f_i\)
  • 并且对于每个 \(i\)\(f_i\) 都越小越好,以便给后面的元素留更大的空间,所以最开始 \(f_0 = -\infty, f_i = \infty(i > 1)\)
  • 而且对于每个元素 \(x\),能把它作为一个长度为 \(i\) 的上升子序列末尾,一定不会让它只做长度为 \(i - 1\) 的上升子序列末尾,否则就算把它作为长度为 \(i - 1\) 的上升子序列末尾,在它后面填一个元素得到的 \(f_i\) 一定大于 \(x\),违背了第二条。
  • 知到了上面三条后,我们可以依次枚举每个元素 \(a_i\), 然后通过二分找到第一个的 \(j\) 使得 \(f_j \geqslant a_i\)。如果现在的最长上升子序列长度为 \(j - 1\),最长上升子序列长度 \(+1\)。然后 \(f_{j} = a_i\)

时间复杂度

  • 枚举每个元素:\(O(n)\)

  • 每个元素进行一次二分:\(O(n \log n)\)

  • 总时间复杂度:\(O(n \log n)\)

Code

int n, m, a[MAXN], f[MAXN];

for (int i = 1; i <= n; i++) {
  int k = lower_bound(f + 1, f + m + 1, a[i]) - f; 找到第一个 k 使得 f[k] >= a[i]
  if (k > m) {
    f[++m] = a[i];
  } else {
    f[k] = a[i];
  }
} 

线段树 / 树状数组优化

我们可以转换一下式子,令 \(dp_i\) 表示以 \(i\) 为结尾的最长上升子序列长度,依次枚举每个元素 \(x\) 时,只有 \(dp_x\) 会更新为 \(\max(dp_x, \max\limits_{i = 0}^{x - 1} \{dp_i + 1\})\),我们发现发现后面那一坨可以用线段树 / 树状数组优化,并且这里只有单点修改和区间查询。

时间复杂度

  • 枚举每个元素:\(O(n)\)
  • 用线段树或树状数组进行查询、修改:\(O(\log n)\)
  • 总时间复杂度:\(O(n \log n)\)

最长公共子序列(LCS)

模板

  • 求一个长度为 \(n\) 的数组 \(a\) 和一个长度为 \(m\) 的数组 \(b\),求 \(a, b\) 最长公共子序列(即找到最长一个数组的 \(s\) 使得 \(s\) 既为 \(a\) 的子序列又为 \(b\) 的子序列)。\((1 \leqslant n, m \leqslant 1000)\)
  • \(dp_{i, j}\) 表示 \(a\) 的前 \(i\) 个元素和 \(b\) 的前 \(j\) 个元素的最长公共子序列。
  • \(dp_{0, i} = dp_{i, 0} = 0\)\(dp_{i, j} = \begin{cases} dp_{i - 1, j - 1} + 1(a_i = b_j) \\ max(dp_{i - 1, j}, dp_{i, j - 1})(a_{i} \ne a_j) \end{cases}\)。第一种转移就是将 \(a_i, b_j\) 接到公共子序列后面,第二种就是不能接的普通转移。
for (int i = 1; i <= n; i++) {
  for (int j = 1; j <= m; j++) {
    if (s[i] == t[j]) { // 第一种情况
      dp[i][j] = dp[i - 1][j - 1] + 1;
    } else { // 第二种情况
      dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]);
    }
  }
}

时间复杂度

状态数:\(O(nm)\),转移数:\(O(nm)\),总时间复杂度:\(O(nm)\)

posted @ 2023-10-08 20:19  xiehanrui0817  阅读(3)  评论(0)    收藏  举报