[线性dp] aw897. 最长公共子序列(重要模板题+最长公共子序列模型)

文章目录

    • 0. 前言
    • 1. LCS 模板题

0. 前言

LCS(longest common sub-sequences):最长公共子序列

子串: 按原顺序依次出现,禁止跳过某元素的序列,具有连续性

子序列: 在保持元素前后关系的前提下,可以跳过某些元素的序列,不连续性


密切相关:[线性dp] aw895最长上升子序列(知识理解+重要模板题+最长上升子序列模型+LCS转化LIS) 详看下方的知识理解,有用!

1. LCS 模板题

897. 最长公共子序列

[线性dp] aw897. 最长公共子序列(重要模板题+最长公共子序列模型)_第1张图片

重点: 线性 dpLCS 问题及优化

思路:

  • 状态定义:
    • f[i][j] 所有在第一个序列的前 i 个字母,和第二个序列的前 j 个字母构成的所有公共子序列的长度最大值
  • 状态转移:
    • 分类依据:以 a[i]b[j] 是否包含在子序列当中,以此来划分所有状态集合,选与不选各两种情况,故共分为四种情况:
      • 不选 a[i] 不选 b[j] 时,等价于 f[i-1][j-1]
      • 不选 a[i]b[j] 时,乍一看是 f[i-1][j],但是其表示的在第一个串的前 i-1 个字母中出现并在第二个串的前 j 个字母中出现的子序列。但是 f[i-1][j] 仅能保证不选 a[i],却无法保证 b[j] 在第二个串的前 j 个字母中。故该状态不能拿 f[i-1][j] 来直接进行转移。但是,f[i-1][j] 一定是包含了当前这个情况的,而我们最终求的 f[i][j] 又可以将 f[i-1][j] 包含掉。所以针对求 max 这个操作,集合之间有重复情况其实是不影响的,反正最后求解的是 f[i][j] 的最大值,所以 f[i-1][j] 除了覆盖当前情况外还可能覆盖了其它三种状态的部分情况,但是这不重要,它只要不漏就行4 个状态之间的互相覆盖是不影响的。即,只要不少就行,重不重复无所谓。
      • a[i] 不选 b[j] 时,乍一看是 f[i][j-1],同理也是不能保证 a[i] 被选到,但是 f[i][j-1] 一定将该种情况包含,可能会与其它三种情况进行重复,但是取整个集合的 max 操作是不影响的。
      • a[i]b[j] 时,等价于 f[i-1][j-1] + 1
  • 边界处理及初始化
    • f[i][j] 全部初始化为 0 即可,取 max 操作,不相等,默认就是 1
  • 时间复杂度:
    • O ( n 2 ) O(n^2) O(n2)

更新笔记:(2021年4月3号,15:34分)

  • 集合划分可根据 a[i] == b[j] 来进行划分
    • a[i] == b[j] 显然 f[i][j] = f[i-1][j-1]+1
    • a[i] != b[j] 显然 f[i][j] = max(f[i-1][j], f[i][j-1])

这样的集合划分显然更加容易理解一点。更新毕(2021年4月3号,15:34分)

优化代码:

#include 
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N];
int main() {
  cin >> n >> m >> a + 1 >> b + 1;
  for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= m; j++) {
      if (a[i] == b[j]) f[i][j] = f[i - 1][j - 1] + 1;
      else f[i][j] = max(f[i - 1][j], f[i][j - 1]);
    }
  }
  cout << f[n][m] << '\n';
  return 0;
}

其实经过仔细比对,会发现,当 a[i]==b[j] 时,状态一定由 f[i-1][j-1]+1 进行转移,就不需要通过 f[i][j] = max(f[i - 1][j], f[i][j - 1]); 这个转移了。但朴素的写法,会将这个放到前面,达到:在这里插入图片描述
的转移效果。图片取自 传送门:【宫水三叶の相信科学系列】求「最值问题」只需要确保「不漏」即可。


代码:

#include 
#include 

using namespace std;

const int N = 1005;

int n, m;
// char和int类型差别还是很大的
// int一个空间4个字节,char采用scanf读进来
// 4个char字母同时存在一个int空间中
// 这四个char就构成一个二进制数,变成了一个int数字
// 然后后面的int空间全是0,即int中存的字母远小于char中
// 所以它还是“公共子序列”,最终还有可能是正确答案,即可能出现各式各样的错误
char a[N], b[N];	
int f[N][N];

int main() {
    cin >> n >> m;
    scanf("%s%s", a + 1, b + 1);
    
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j) {
            f[i][j] = max(f[i - 1][j], f[i][j - 1]);
            if (a[i] == b[j]) f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1);
        }
    
    cout << f[n][m] << endl;
    return 0;
}

f[i] 只与 f[i - 1] 相关,所以可以用滚动数组。每次只存相邻两层的状态即可。

滚动数组优化是有套路的,直接在所有 f[i][j] 的第一维后面加上 & 1 就行了。

滚动数组优化代码:

#include 

using namespace std;

const int N = 1010;

int n, m;
char a[N], b[N];
int f[2][N];

int main()
{
    cin >> n >> m >> a + 1 >> b + 1;

    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
        {
            f[i & 1][j] = max(f[i - 1 & 1][j], f[i & 1][j - 1]);
            if (a[i] == b[j]) f[i & 1][j] = max(f[i & 1][j], f[i - 1 & 1][j - 1] + 1);
        }

    cout << f[n & 1][m] << endl;

    return 0;
}

2021年08月25日 再度更新。

密切相关:[线性dp] aw895最长上升子序列(知识理解+重要模板题+最长上升子序列模型+LCS转化LIS) 详看下方的知识理解,有用!


源于 lc 的每日一题 1035. 不相交的线,和 [线性dp] aw1012. 友好城市(最长上升子序列模型+思维) 差不多的题,但思想却不一样。

其中三叶姐采用了lcs 的四种状态分类的方式,详细讨论了状态的重叠,但不影响正确最值的情况。并针对 lcslis 这两个重要的相似问题做以讨论、区分,令我受益匪浅。

  • 传送门:【宫水三叶の相信科学系列】求「最值问题」只需要确保「不漏」即可。
  • lcs 需要两个串,状态 f[i][j]其中 a[i]、b[j] 这两个点的元素并不是必须要选的,并不需要包含 a[i]、b[j]。那么 lcs 状态转移即有四种情况。
  • lis 只需要一个串,状态 f[i]其中一定是以 a[i] 结尾的上升子序列长度的最大值a[i] 是一定要被选的。
  • 这也就是两者最大的不同。

虽然,lcs 可以直接判断 a[i]==b[j] 来作为状态划分,但是这种即便状态重复但并不影响最值判断的思想,也是非常重要的。

你可能感兴趣的:(#,LCS,LCS问题,模板题)