【科技】位运算(bitset)优化最长公共子序列算法
最长公共子序列(LCS)问题
你有两个字符串 \(A,B\),字符集为 \(\Sigma\),求 \(A, B\) 的最长公共子序列。
简单动态规划
首先有一个广为人知的 dp:\(f_{i,j}\) 为 \(A\) 的长度为 \(j\) 的前缀与 \(B\) 长度为 \(i\) 的前缀的 LCS。(注意 \(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。
但是有一个极其重要的性质:
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\) 的性质,不难发现 \(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)\) 与之比对(注意这里是倒着的):
M[i - 1]: [1 0 0 0 0 0 0][1 0 0 0][1][1][1][1][1]
p[B[i]] : [0 1 0 1 1 0 0 0 1 0 0 0 1 1 0 0]
^ ^
| |
j = |A| j = 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)\),然后我们需要取其中最后一位:
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 0 1 1 0 0][1 1 0 0][1][1][1][1][1]
- [0 0 0 0 0 0 1 0 0 0 1 1 1 1 1 1]
--------------------------------------------------
[1 1 0 1 0 1 1][1 0 1 1][0][0][0][0][0]
这么做旨在将每段的末尾 \(0\) 段,然后将原来最右边的 \(1\) 变成 \(0\)。
然后和 \(X\) 进行 异或 操作:
[0 0 0 0 1 1 1][0 1 1 1][1][1][1][1][1]
这样就使最开始的最右边的 \(1\) 到段尾变成 \(1\),其余变成 \(0\)。
最后只要保留第一个 \(1\),那么就刚好是 按位与 \(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)\)。
参考代码
下面的代码实现 并不是倒着的(为了减法方便),于是位移什么的看着就有点诡异。
/*
* Author : _Wallace_
* Source : https://www.cnblogs.com/-Wallace-/
* Problem : LOJ #6564. 最长公共子序列
* Standard : GNU C++ 03
* Optimal : -Ofast
*/
#include <algorithm>
#include <cstddef>
#include <cstdio>
#include <cstring>
typedef unsigned long long ULL;
const int N = 7e4 + 5;
int n, m, u;
struct bitset {
ULL t[N / 64 + 5];
bitset() {
memset(t, 0, sizeof(t));
}
bitset(const bitset &rhs) {
memcpy(t, rhs.t, sizeof(t));
}
bitset& set(int p) {
t[p >> 6] |= 1llu << (p & 63);
return *this;
}
bitset& shift() {
ULL last = 0llu;
for (int i = 0; i < u; i++) {
ULL cur = t[i] >> 63;
(t[i] <<= 1) |= last, last = cur;
}
return *this;
}
int count() {
int ret = 0;
for (int i = 0; i < u; i++)
ret += __builtin_popcountll(t[i]);
return ret;
}
bitset& operator = (const bitset &rhs) {
memcpy(t, rhs.t, sizeof(t));
return *this;
}
bitset& operator &= (const bitset &rhs) {
for (int i = 0; i < u; i++) t[i] &= rhs.t[i];
return *this;
}
bitset& operator |= (const bitset &rhs) {
for (int i = 0; i < u; i++) t[i] |= rhs.t[i];
return *this;
}
bitset& operator ^= (const bitset &rhs) {
for (int i = 0; i < u; i++) t[i] ^= rhs.t[i];
return *this;
}
friend bitset operator - (const bitset &lhs, const bitset &rhs) {
ULL last = 0llu; bitset ret;
for (int i = 0; i < u; i++){
ULL cur = (lhs.t[i] < rhs.t[i] + last);
ret.t[i] = lhs.t[i] - rhs.t[i] - last;
last = cur;
}
return ret;
}
} p[N], f, g;
signed main() {
scanf("%d%d", &n, &m), u = n / 64 + 1;
for (int i = 1, c; i <= n; i++)
scanf("%d", &c), p[c].set(i);
for (int i = 1, c; i <= m; i++) {
scanf("%d", &c), (g = f) |= p[c];
f.shift(), f.set(0);
((f = g - f) ^= g) &= g;
}
printf("%d\n", f.count());
return 0;
}
后记
- 原文地址:https://www.cnblogs.com/-Wallace-/p/14052964.html
- 本文作者:@-Wallace-
- 转载请附上出处。
【科技】位运算(bitset)优化最长公共子序列算法的更多相关文章
- LCSS最长公共子序列算法
0.论文基本介绍以及相关内容 分析移动用户位置的相似性,提取移动用户的相似路径在出行路径预测.兴趣区域发现.轨迹聚类.个性化路径推荐等领域具有广泛的应用. 重点:利用移动用户定位数据找到合适轨迹的表示 ...
- 用python实现最长公共子序列算法(找到所有最长公共子串)
软件安全的一个小实验,正好复习一下LCS的写法. 实现LCS的算法和算法导论上的方式基本一致,都是先建好两个表,一个存储在(i,j)处当前最长公共子序列长度,另一个存储在(i,j)处的回溯方向. 相对 ...
- HDU 1159 Common Subsequence:LCS(最长公共子序列)
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1159 题意: 求最长公共子序列. 题解: (LCS模板题) 表示状态: dp[i][j] = max ...
- [LeetCode每日一题]1143. 最长公共子序列
[LeetCode每日一题]1143. 最长公共子序列 问题 给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度.如果不存在 公共子序列 ,返回 0 . 一个字符串 ...
- HDU 1159 Common Subsequence 【最长公共子序列】模板题
题目链接:https://vjudge.net/contest/124428#problem/A 题目大意:给出两个字符串,求其最长公共子序列的长度. 最长公共子序列算法详解:https://blog ...
- 程序员的算法课(6)-最长公共子序列(LCS)
版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/m0_37609579/article/de ...
- [科技]Loj#6564-最长公共子序列【bitset】
正题 题目链接:https://loj.ac/p/6564 题目大意 给两个序列\(a,b\)求它们的最长公共子序列. \(1\leq n,m,a_i,b_i\leq 7\times 10^4\) 解 ...
- 经典递归问题:0,1背包问题 kmp 用遗传算法来解背包问题,hash表,位图法搜索,最长公共子序列
0,1背包问题:我写笔记风格就是想到哪里写哪里,有很多是旧的也没删除,代码内部可能有很多重复的东西,但是保证能运行出最后效果 '''学点高大上的遗传算法''' '''首先是Np问题的定义: npc:多 ...
- 删除部分字符使其变成回文串问题——最长公共子序列(LCS)问题
先要搞明白:最长公共子串和最长公共子序列的区别. 最长公共子串(Longest Common Substirng):连续 最长公共子序列(Longest Common Subsequence,L ...
随机推荐
- nginx&http 第四章 ngx http代理 upstream流程图
- 测试cephfs写入海量文件
前言 测试cephfs的写入大量文件,通过mdtest写入1K大小的文件1亿个,每个目录里面文件为1万,目录总数为1万,总文件数目就为1亿了 写入的命令 mdtest -C -F -L -z 4 -b ...
- SQL Server数据库Union和Union All查询出数据的区别?
好久没有更新博客了,可能是最近比较忙,总是忽略了一些事情,今天查了做了一些数据分析的数据,突然感觉对Union和Union all有些不太理解了,可能是自己老了吧,就翻了一些资料,进行回忆和学习,趁着 ...
- JWT 实战
上一篇我们讲解了 JWT 的基本原理和结构 你了解JWT吗?,接下来我们具体实战一下! 1. 引入依赖 <!--引入jwt--> <dependency> <groupI ...
- kali 系列学习06 - 攻击之密码破解
参考书 <kali linux渗透测试技术详解> 清华大学出版社 2015 杨波 一.medusa使用 原理:并行登陆破解 1.只输入单个命令,是查看帮助 root@kali:/tool ...
- 上周我面了个三年 Javaer,这几个问题都没答出来
身为 Java Web 开发我发现很多人一些 Web 基础问题都答不上来. 上周我面试了一个三年经验的小伙子,一开始我问他 HTTP/1.HTTP/2相关的他到是能答点东西出来. 后来我问他:你知道 ...
- c# 调用Go 动态库
[StructLayout(LayoutKind.Sequential)] public struct GoMem { public IntPtr data; public UInt64 len; p ...
- node.js报错:Cannot find module 'xxx'的解决办法
从别处拷贝一份node.js项目,如图 控制台启动 nodemon index.js 后报错:Cannot find module xxx.删除node_modules,在启动时仍提示Cannot f ...
- 思维导图软件iMindMap制作技巧有哪些
iMindMap11是iMindMap全新的版本.它可以提供给我们更好的灵活性以便我们将我们的思维进行可视化,并进一步的呈现和开发出属于自己的想法以及思维方式.在iMindMap中我们可以利用思维导图 ...
- 统一软件开发过程(RUP)的概念和方法
统一软件开发过程(Rational Unified Process,RUP)是一种面向对象且基于网络的程序开发方法论. 根据Rational(Rational Rose和统一建模语言的开发者)的说法, ...