最长公共子序列(LCS)问题

你有两个字符串 \(A,B\),字符集为 \(\Sigma\),求 \(A, B\) 的最长公共子序列。

简单动态规划

首先有一个广为人知的 dp:\(f_{i,j}\) 为 \(A\) 的长度为 \(j\) 的前缀与 \(B\) 长度为 \(i\) 的前缀的 LCS。(注意 \(i\) 和 \(j\) 分别对于那个串)

那么显然有:

\[f_{i,j} =
\begin{cases}
f_{i-1, j-1} + 1 & (A_j = B_i) \\
\max(f_{i, j-1}, f_{i-1, j}) & (A_j \ne B_i)
\end{cases}
\]

然而这是 \(O(n^2)\) 的,在略大的数据下就很容易 TLE。

还有一个 \(O(n\log n)\) 的算法,但只是针对排列的情况。

然后我们介绍一个基于 位运算 的优化方法。

这怎么就能位运算了呢?看着就不怎么 01。

但是有一个极其重要的性质:

\[\begin{cases}
f_{i,j} \ge f_{i-1,j} \\
f_{i,j} \ge f_{i,j-1} \\
|f_{i,j} - f_{i,j-1}| \le 1
\end{cases}
\]

即 \(f\) 的同一行内是 单调不减 并且 相邻两个相差不超过一

矩阵 \(M\)

我们定义矩阵 \(M\) 为 \(f\) 数组每行分别 差分 的结果,即:

\[f_{i,j} = \sum_{k=1}^j M_{i,j}
\]

根据上述 \(f\) 的性质,不难发现 \(M\) 是个 01矩阵。那么可以直接 压位(类似 std::bitset)。

然后考虑直接转移 \(M_i\) 整行,最后 \(\sum_{j}M_{|B|,j}\) 就是答案。这就是优化的基本思想。

字符比较串 \(p\)

我们定义 \(p(c)\) 为字符 \(c\) 在字符串 \(A\) 中出现的所有位置的集合,\(p(c)_i=1\) 表示 \(A_i=c\)。这是我们转移的工具。

要预处理 \(p\) 我们需要 \(O(|\Sigma|\times |A|)\) 的空间。然而我们发现 \(p\) 中只有 \(0/1\),所以我们可以用类似于 \(M\) 进行压位优化,那就只要 \(O\left(\frac{|\Sigma|\times |A|}{w}\right)\),一般来说还是一个可承受的量级。

\(M\) 的实际意义

上面只提到 \(M\) 是个差分数组,现在来考虑它的实际意义是什么,以便推出它的转移方式。

考虑一个 \(M_{i,j}\) 什么时候会是 \(1\)。观察原转移方程,发现 \(f_{i,j-1}\) 方向必然不会使 \(f_{i,j}\) 加一,唯一两个方向就是 \(f_{i-1,j-1}\) 或 \(f_{i-1,j}\)。

如果是从 \(f_{i-1,j-1}+1\) 而来,那么说明这个位置 \(A_j\) 发生了配对,从而答案 \(+1\);

如果是 \(f_{i-1,j}\),仔细思考一下还是一样的,在下面总有一个位置会和上面一条相同。

总而言之就是 \(A_j\) 被计入答案 了,但注意这不意味着 \(M_i\) 中所有的 \(1\) 都对应一个被选中的 \(A_j\)。

正确的理解是 \(M_{i,j}\) 如果为 \(1\),设 \(k\) 为当前位到第一位之间 \(1\) 的个数,那就说明当前一个 LCS 长度为 \(k\) 的方案,最后的一位为 \(j\)。事实我们也是只需要考虑当前 LCS 的最后一位,添加时答案只要保证在当前方案的最后一位之后即可。

转移方式

对于一整行 \(M_{i-1}\),我们对其分段,每段有前面一个极长 \(0\) 段,由一个单独的 \(1\) 结尾,最后一整段 \(0\) 单独成段。

然后用当前 \(B\) 的字符 \(p(B_i)\) 与之比对(注意这里是倒着的):

  1. M[i - 1]: [1 0 0 0 0 0 0][1 0 0 0][1][1][1][1][1]
  2. p[B[i]] : [0 1 0 1 1 0 0 0 1 0 0 0 1 1 0 0]
  3. ^ ^
  4. | |
  5. j = |A| j = 1

然后将两者做 按位或 操作,再对于每个段按位或的结果取 段中的最后一个 \(1\),得:

  1. M[i] : [0 0 0 0 1 0 0 0 1 0 0 1 1 1 1 1]

这个过程相当于 \(M_{i-1}\) 借助 \(p(B_i)\) 将这些 \(1\) 尽量向字符串的开头移,以便为之后的匹配留足更大的机会。

至于其中的意义可以结合上面理解,大概就是对于每个长度的方案,都在不超过下一个长度的前提下前移。具体细节我也说不清楚

转移实现

上面的转移过于复杂,很难用我们熟知的位运算进行优化,于是尝试将它翻译成位运算。

我也不知道原论文作者怎么想到的,这里就说只一下做法吧。

我们记 \(X = M_{i-1}\ \texttt{OR}\ p(B_i)\),然后我们需要取其中最后一位:

  1. X : [1 1 0 1 1 0 0 1 1 0 0 1 1 1 1 1]

然后将 \(M_{i-1}\) 右移一位,头部补上 \(1\),并用 \(X\) 数值减 这个 01 串,得:

  1. [1 1 0 1 1 0 0][1 1 0 0][1][1][1][1][1]
  2. - [0 0 0 0 0 0 1 0 0 0 1 1 1 1 1 1]
  3. --------------------------------------------------
  4. [1 1 0 1 0 1 1][1 0 1 1][0][0][0][0][0]

这么做旨在将每段的末尾 \(0\) 段,然后将原来最右边的 \(1\) 变成 \(0\)。

然后和 \(X\) 进行 异或 操作:

  1. [0 0 0 0 1 1 1][0 1 1 1][1][1][1][1][1]

这样就使最开始的最右边的 \(1\) 到段尾变成 \(1\),其余变成 \(0\)。

最后只要保留第一个 \(1\),那么就刚好是 按位与 \(X\) 的结果。

于是得到:

\[M_i = ((X-((M_{i-1}\ \texttt{<<}\ 1) + 1))\ \texttt{xor}\ X)\ \texttt{and}\ X、
\]

那么在实现时,只要手写一个 bitset,支持按位与、或、异或、数值相减、位移即可。

复杂度

每次转移需要 \(O\left(\frac {|A|} w\right)\),总时间复杂度为 \(O\left(\frac{|A|\times |B|}{w}\right)\)

空间瓶颈为 \(p\) 集合,为 \(O\left(\frac{|A|\times |\Sigma|}{w}\right)\),如果字符集 \(\Sigma\) 不确定可以离散化,空间为 \(O\left( \frac{|A|^2}{w} \right)\)。

参考代码

下面的代码实现 并不是倒着的(为了减法方便),于是位移什么的看着就有点诡异。

LOJ 提交入口

  1. /*
  2. * Author : _Wallace_
  3. * Source : https://www.cnblogs.com/-Wallace-/
  4. * Problem : LOJ #6564. 最长公共子序列
  5. * Standard : GNU C++ 03
  6. * Optimal : -Ofast
  7. */
  8. #include <algorithm>
  9. #include <cstddef>
  10. #include <cstdio>
  11. #include <cstring>
  12. typedef unsigned long long ULL;
  13. const int N = 7e4 + 5;
  14. int n, m, u;
  15. struct bitset {
  16. ULL t[N / 64 + 5];
  17. bitset() {
  18. memset(t, 0, sizeof(t));
  19. }
  20. bitset(const bitset &rhs) {
  21. memcpy(t, rhs.t, sizeof(t));
  22. }
  23. bitset& set(int p) {
  24. t[p >> 6] |= 1llu << (p & 63);
  25. return *this;
  26. }
  27. bitset& shift() {
  28. ULL last = 0llu;
  29. for (int i = 0; i < u; i++) {
  30. ULL cur = t[i] >> 63;
  31. (t[i] <<= 1) |= last, last = cur;
  32. }
  33. return *this;
  34. }
  35. int count() {
  36. int ret = 0;
  37. for (int i = 0; i < u; i++)
  38. ret += __builtin_popcountll(t[i]);
  39. return ret;
  40. }
  41. bitset& operator = (const bitset &rhs) {
  42. memcpy(t, rhs.t, sizeof(t));
  43. return *this;
  44. }
  45. bitset& operator &= (const bitset &rhs) {
  46. for (int i = 0; i < u; i++) t[i] &= rhs.t[i];
  47. return *this;
  48. }
  49. bitset& operator |= (const bitset &rhs) {
  50. for (int i = 0; i < u; i++) t[i] |= rhs.t[i];
  51. return *this;
  52. }
  53. bitset& operator ^= (const bitset &rhs) {
  54. for (int i = 0; i < u; i++) t[i] ^= rhs.t[i];
  55. return *this;
  56. }
  57. friend bitset operator - (const bitset &lhs, const bitset &rhs) {
  58. ULL last = 0llu; bitset ret;
  59. for (int i = 0; i < u; i++){
  60. ULL cur = (lhs.t[i] < rhs.t[i] + last);
  61. ret.t[i] = lhs.t[i] - rhs.t[i] - last;
  62. last = cur;
  63. }
  64. return ret;
  65. }
  66. } p[N], f, g;
  67. signed main() {
  68. scanf("%d%d", &n, &m), u = n / 64 + 1;
  69. for (int i = 1, c; i <= n; i++)
  70. scanf("%d", &c), p[c].set(i);
  71. for (int i = 1, c; i <= m; i++) {
  72. scanf("%d", &c), (g = f) |= p[c];
  73. f.shift(), f.set(0);
  74. ((f = g - f) ^= g) &= g;
  75. }
  76. printf("%d\n", f.count());
  77. return 0;
  78. }

后记

【科技】位运算(bitset)优化最长公共子序列算法的更多相关文章

  1. LCSS最长公共子序列算法

    0.论文基本介绍以及相关内容 分析移动用户位置的相似性,提取移动用户的相似路径在出行路径预测.兴趣区域发现.轨迹聚类.个性化路径推荐等领域具有广泛的应用. 重点:利用移动用户定位数据找到合适轨迹的表示 ...

  2. 用python实现最长公共子序列算法(找到所有最长公共子串)

    软件安全的一个小实验,正好复习一下LCS的写法. 实现LCS的算法和算法导论上的方式基本一致,都是先建好两个表,一个存储在(i,j)处当前最长公共子序列长度,另一个存储在(i,j)处的回溯方向. 相对 ...

  3. HDU 1159 Common Subsequence:LCS(最长公共子序列)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1159 题意: 求最长公共子序列. 题解: (LCS模板题) 表示状态: dp[i][j] = max ...

  4. [LeetCode每日一题]1143. 最长公共子序列

    [LeetCode每日一题]1143. 最长公共子序列 问题 给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度.如果不存在 公共子序列 ,返回 0 . 一个字符串 ...

  5. HDU 1159 Common Subsequence 【最长公共子序列】模板题

    题目链接:https://vjudge.net/contest/124428#problem/A 题目大意:给出两个字符串,求其最长公共子序列的长度. 最长公共子序列算法详解:https://blog ...

  6. 程序员的算法课(6)-最长公共子序列(LCS)

    版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/m0_37609579/article/de ...

  7. [科技]Loj#6564-最长公共子序列【bitset】

    正题 题目链接:https://loj.ac/p/6564 题目大意 给两个序列\(a,b\)求它们的最长公共子序列. \(1\leq n,m,a_i,b_i\leq 7\times 10^4\) 解 ...

  8. 经典递归问题:0,1背包问题 kmp 用遗传算法来解背包问题,hash表,位图法搜索,最长公共子序列

    0,1背包问题:我写笔记风格就是想到哪里写哪里,有很多是旧的也没删除,代码内部可能有很多重复的东西,但是保证能运行出最后效果 '''学点高大上的遗传算法''' '''首先是Np问题的定义: npc:多 ...

  9. 删除部分字符使其变成回文串问题——最长公共子序列(LCS)问题

    先要搞明白:最长公共子串和最长公共子序列的区别.    最长公共子串(Longest Common Substirng):连续 最长公共子序列(Longest Common Subsequence,L ...

随机推荐

  1. 154. Find Minimum in Rotated Sorted Array II(循环数组查找)

    Suppose an array sorted in ascending order is rotated at some pivot unknown to you beforehand. (i.e. ...

  2. mybatis insert转update,duplicate关键字的使用示例,及返回情况说明

    主键存在时又insert转为update某个关键字段,示例如下,注意,如果这条数据曾经不存在,此时执行insert返回条目是1,如果已存在,执行update返回条目是2!!!<insert id ...

  3. C语言设计模式(自我揣摩)

    NBModule.h #ifndef _NBMODULEFRAME_H__ #define _NBMODULEFRAME_H__ #include "total.h" enum N ...

  4. Shodan搜索引擎详解及Python命令行调用

    shodan常用信息搜索命令 shodan配置命令 shodan init T1N3uP0Lyeq5w0wxxxxxxxxxxxxxxx //API设置 shodan信息收集 shodan myip ...

  5. kakafka - 为CQRS而生fka - 为CQRS而生

    前段时间跟一个朋友聊起kafka,flint,spark这些是不是某种分布式运算框架.我自认为的分布式运算框架最基础条件是能够把多个集群节点当作一个完整的系统,然后程序好像是在同一台机器的内存里运行一 ...

  6. 安装swoole扩展

    wget https://github.com/swoole/swoole-src/archive/v1.9.3-stable.tar.gz tar -zxvf v1.9.3-stable.tar.g ...

  7. leetcode 1046

    class Solution {       public int lastStoneWeight(int[] stones) {        MaxHeap s=new MaxHeap(stone ...

  8. docker提示容器已存在

    docker ps -a docker rm 容器id 重启启动

  9. 【震惊】手把手教你用python做绘图工具(一)

    在这篇博客里将为你介绍如何通过numpy和cv2进行结和去创建画布,包括空白画布.白色画布和彩色画布.创建画布是制作绘图工具的前提,有了画布我们就可以在画布上尽情的挥洒自己的艺术细胞. 还在为如何去绘 ...

  10. 日期选择组件(DatePicker)的实现

    一.效果图 日期选择组件大概长这样: 从效果图可以看出,日期选择组件由两部分组成:日历表格和顶部操作栏. 二.日历表格 日期选择组件的核心主体是日历表格: 可以将日历表格表示成一个7️*的二维数组,数 ...