最长公共子序列
LCS是指这两个序列中最长的公共子序列, 子序列:不要求字符在原序列中连续, 但相对顺序必须保持一致
问题:
给定两个字符串X和Y, 我们需要找到它们最长公共子序列
X = "ABCBDAB" Y = "BDCAB"
输出最长公共子序列的长度为4 及"BDAB"
动态规划的思路
dp[i][j] 表示字符串X的前i个字符和字符串Y的前j个字符组成的最长公共子序列的长度
根据X[i - 1] 和 Y[j - 1]的大小不同可以得到以下的两种情况
if x[i - 1] == y[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
else:
(1) dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
# 当前字符不同, LCS的长度是移除其中一个字符后的较大值
问题(1):
为什么忽略了dp[i - 1][j - 1] 的情况, 是因为当X[i - 1] != Y[j - 1]的情况
它们不可能同时组成最长公共子序列, dp[i - 1][j - 1]是记录了前i - 1个字符和前j - 1个字符组成的最长公共子序列的长度,
由于最后两个字符不同, 导致不能使用这个状态继续递推, 于是从dp[i - 1][j] 和 dp[i][j - 1]中选择较大值, 因为
这两者包括了当前字符的最优状态 作为dp[i][j]的值
简要:
dp[i - 1][j] 和 dp[i][j - 1]始终比dp[i - 1][j - 1]要多一个字符 自身的可能性 永远大于等于dp[i - 1][j - 1], 所以
只需要在dp[i - 1][j] 和 dp[i][j - 1]中选择较大值
那么在已经了解完后直接看题
Code:
class Solution {
public:
int longestCommonSubsequence(string s, string t) {
int n = s.size(), m = t.size();
vector <vector <int>> dp(n + 1, vector <int>(m + 1, 0));
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (s[i - 1] == t[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[n][m];
}
};
降至一维
通过观察dp状态
if X[i] == Y[j]:
dp[i][j] = dp[i - 1][j - 1] + 1
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
它的dp状态是 (上) (左) (左上) 如果要将它变为一维, 也就是
去处理(左上) 因为在计算当前行的时候的状态时, 左时覆盖了左上
那么也意味着去计算左上的状态时, 利用一个变量去储存当前的值
这样就可以降至一维
Code:
class Solution {
public:
int longestCommonSubsequence(string s, string t) {
int n = s.size(), m = t.size();
vector <int> dp(m + 1, 0);
for (int i = 1; i <= n; i++) {
int pre = dp[0];
for (int j = 1; j <= m; j++) {
int tmp = dp[j];
if (s[i - 1] == t[j - 1]) {
dp[j] = pre + 1;
} else {
dp[j] = max(dp[j], dp[j - 1]);
}
pre = tmp;
}
}
return dp[m];
}
};
扩展:
扩展1:题目: (n <= 1e5) 和 (序列元素不出现重复)
P1439 【模板】最长公共子序列
如何转换请看如下 (根据下面这一句话):
最长公共子序列是指在两个序列中,按照顺序能找到的最大长度的相同子序列。重点是顺序要一致
在此题中 每个数字不重复, 也就意味着一一对应
比如P1[3, 2, 1, 4, 5] 和 P2[1, 2, 3, 4, 5]
可以从P1中找到对应的元素, 同时也可以在P2的相同顺序中找到, 这个部分
就是它们的公共子序列
那么可以通过映射P1, P2将P2的元素转化为位置索引, 在从P1直接找到对应P2的元素位置
这个问题就直接等价于转变为位置的比较
idx[] P1映射后的数组
idx[] = [2, 1, 0, 3, 4] 找到最长递增子序列的长度就是最终答案 (LIS在上一节)
idx[]的最长递增子序列[1, 3, 4] 也就对应的原数组[2, 4, 5] 这些元素的顺序也相同及它们的
公共子序列答案为3
Code:
#include <bits/stdc++.h>
using namespace std;
void solve() {
int n;
cin >> n;
vector <int> idx(n + 1, 0), a(n), b(n), res(n + 1, 0), ans;
for (int i = 0; i < n; i++) {
cin >> a[i];
}
for (int i = 0; i < n; i++) {
cin >> b[i];
idx[b[i]] = i;
}
for (int i = 1; i <= n; i++) {
res[i] = idx[a[i - 1]];
}
for (int i = 1; i <= n; i++) { // LIS (最长递增子序列的板子)
if (ans.empty() || res[i] > ans.back()) {
ans.emplace_back(res[i]);
} else {
auto it = lower_bound(ans.begin(), ans.end(), res[i]) - ans.begin();
ans[it] = res[i];
}
}
cout << ans.size() << '\n';
}
int main() {
cin.tie(0) -> sync_with_stdio(false);
int t = 1;
// cin >> t;
while (t--) {
solve();
}
return 0;
}
扩展2: 计算到达最长公共子序列长度的方案数
方案数其实很好考虑
if X[i] == Y[j]:
cnt[i][j] = cnt[i - 1][j - 1]
else:
cnt[i][j] = cnt[i - 1][j] + cnt[i][j - 1]
这里唯一的坑就是去重复 那么接下来我们开始去重复
if X[i] == Y[j]:
cnt[i][j] += (cnt[i - 1][j - 1] == 0 ? 1 : cnt[i - 1][j - 1]);
else:
if (dp[i][j] == dp[i - 1][j]):
cnt[i][j] += cnt[i - 1][j];
if (dp[i][j] == dp[i][j - 1]):
cnt[i][j] += cnt[i][j - 1];
if (dp[i][j] == dp[i - 1][j - 1]): // 唯一的坑 避免重复计数
cnt[i][j] -= cnt[i - 1][j - 1];
Code1:(二维数组):
#include <bits/stdc++.h>
using namespace std;
constexpr int mod = 100000000;
void solve() {
string s, t;
cin >> s >> t;
s = s.substr(0, s.find('.'));
t = t.substr(0, t.find('.'));
int n = s.size(), m = t.size();
vector <vector <int>> dp(n + 1, vector <int> (m + 1, 0)), cnt(n + 1, vector <int> (m + 1, 0));
for (int i = 0; i <= n; i++) {
cnt[i][0] = 1;
}
for (int j = 0; j <= m; j++) {
cnt[0][j] = 1;
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
dp[i][j] = max({dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1] + (s[i - 1] == t[j - 1])});
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
auto &val = cnt[i][j];
if (s[i - 1] == t[j - 1]) {
if (dp[i][j] == dp[i - 1][j - 1] + 1) {
val = (val + cnt[i - 1][j - 1]) % mod;
}
}
if (dp[i][j] == dp[i - 1][j]) {
val = (val + cnt[i - 1][j]) % mod;
}
if (dp[i][j] == dp[i][j - 1]) {
val = (val + cnt[i][j - 1]) % mod;
}
if (s[i - 1] != t[j - 1]) {
if (dp[i][j] == dp[i - 1][j - 1]) {
val = ((val - cnt[i - 1][j - 1]) + mod) % mod;
}
}
}
}
cout << dp[n][m] << '\n';
cout << cnt[n][m] << '\n';
}
int main() {
cin.tie(0) -> sync_with_stdio(false);
int t = 1;
// cin >> t;
while (t--) {
solve();
}
return 0;
}
Code2:
#include <bits/stdc++.h>
using namespace std;
using i64 = int64_t;
constexpr int N = 5e3 + 5, mod = 1e8;
int dp[2][N], cnt[2][N];
string s, t;
void solve() {
cin >> s >> t;
int n = s.size() - 1, m = t.size() - 1;
for (int i = 0; i <= m; i++) {
cnt[0][i] = 1;
}
cnt[1][0] = 1;
for (int i = 1; i <= n; i++) {
int now = i & 1, pre = now ^ 1;
for (int j = 1; j <= m; j++) {
dp[now][j] = max(dp[pre][j], dp[now][j - 1]);
if (s[i - 1] == t[j - 1]) {
dp[now][j] = max(dp[now][j], dp[pre][j - 1] + 1);
}
cnt[now][j] = 0;
if (s[i - 1] == t[j - 1] && dp[now][j] == dp[pre][j - 1] + 1) {
cnt[now][j] = (cnt[now][j] + cnt[pre][j - 1]) % mod;
}
if (dp[now][j] == dp[pre][j]) {
cnt[now][j] = (cnt[now][j] + cnt[pre][j]) % mod;
}
if (dp[now][j] == dp[now][j - 1]) {
cnt[now][j] = (cnt[now][j] + cnt[now][j - 1]) % mod;
}
if (s[i - 1] != t[j - 1] && dp[now][j] == dp[pre][j - 1]) {
cnt[now][j] = ((cnt[now][j] - cnt[pre][j - 1]) + mod) % mod;
}
}
}
cout << dp[n & 1][m] << '\n';
cout << cnt[n & 1][m] % mod << '\n';
}
int main() {
cin.tie(0) -> sync_with_stdio(false);
int t = 1;
// cin >> t;
while (t--) {
solve();
}
return 0;
}