最长公共子序列

最长公共子序列

1
LCS是指这两个序列中最长的公共子序列, 子序列:不要求字符在原序列中连续, 但相对顺序必须保持一致

问题:

1
2
3
给定两个字符串X和Y, 我们需要找到它们最长公共子序列
X = "ABCBDAB" Y = "BDCAB"
输出最长公共子序列的长度为4 及"BDAB"

动态规划的思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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]中选择较大值

那么在已经了解完后直接看题

1143. 最长公共子序列

Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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];
    }
};

 

降至一维 

1
2
3
4
5
6
7
8
9
通过观察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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 【模板】最长公共子序列

如何转换请看如下 (根据下面这一句话):

1
2
3
4
5
6
7
8
9
10
11
12
13
最长公共子序列是指在两个序列中,按照顺序能找到的最大长度的相同子序列。重点是顺序要一致
在此题中 每个数字不重复, 也就意味着一一对应
比如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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#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: 计算到达最长公共子序列长度的方案数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
方案数其实很好考虑
 
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:(二维数组):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#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: 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#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;
}

  

 

posted @   Iter-moon  阅读(22)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
点击右上角即可分享
微信分享提示