😊😊 😊😊
不求点赞,只求耐心看完,指出您的疑惑和写的不好的地方,谢谢您。本人会及时更正感谢。希望看完后能帮助您理解算法的本质
😊😊 😊😊
题目描述:
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0。
示例 1:
输入:text1 = “abcde”, text2 = “ace”
输出:3
解释:最长公共子序列是 “ace”,它的长度为 3。
示例 2:
输入:text1 = “abc”, text2 = “abc”
输出:3
解释:最长公共子序列是 “abc”,它的长度为 3。
示例 3:
输入:text1 = “abc”, text2 = “def”
输出:0
解释:两个字符串没有公共子序列,返回 0。
提示:
1 <= text1.length <= 1000
1 <= text2.length <= 1000 输入的字符串只含有小写英文字符。
一、二进制枚举:
思路:
- 明确什么是子序列?
字符串定义见上图; - 从上图发现,将一个字符串的每一位字符都看作一个数,那么对于一个数有选或者不选两种选择。等价于:1234的每位数可以组成多少个不同的数字,每个数字选或者不选,可得 ⇒ 2n种。n表示长度(位数)。
- 所以长度为 n 的字符串,一共有 2n 个子序列。判断两个字符串的最长公共子序列,假设另一个字符串长度为 m,则应该有 2m 个子序列。
- 采用暴力枚举:字符串1的 2n个子序列与字符串2的 2m个子序列进行枚举判断;时间复杂度为:
2
n
∗
2
m
2^n * 2^m
2n∗2m;指数级别超时!
代码:
for (int i=0; i < (1<<n); i++) 遍历的不是每一位字符,而是字符串1的每一种子序列
for (int j=0; j < (1<<m); j ++) 遍历的不是每一位字符,而是字符串2的每一种子序列
- 记录当前的两个子序列:
string st1="";
for (int k=0; k < s1.size(); k ++) 遍历s1的每位字符
if (i>>k&1) 1表示选,0表示不选。用二进制的特性表示选法
st1 += s1[k]; 累加字符串s1的第k位字符
经验:一个序列(不区分数据类型),它的子序列的个数应该等于:2序列长度len;然后要想枚举它的每一个子序列,则外层循环每一种选法,一共2len种选法,内层循环负责取出当前选法里的每一个字符,从而组成一个子序列。
代码:
#include <bits/stdc++.h>
using namespace std;
string s1, s2;
int n, m;
int lcs(string s1, string s2) {
int ans = 0;
for (int i = 0; i < (1 << n); i++) {
for (int j = 0; j < (1 << m); j++) {
string t1 = "", t2 = "";
for (int k = 0; k < n; k++) {
if (i & (1 << k)) {
t1 += s1[k];
}
}
for (int k = 0; k < m; k++) {
if (j & (1 << k)) {
t2 += s2[k];
}
}
if (t1 == t2) {
ans = max(ans, (int)t1.size());
}
}
}
return ans;
}
int main() {
cin >> n >> m >> s1 >> s2;
cout << lcs(s1, s2) << endl; // 输出3
return 0;
}
二、暴搜算法:
思路:倒序递归!
- 判断s1和s2是否为空,如果有一个为空,则返回0。
- 如果s1和s2的最后一个字符相同,则最长公共子序列的长度为它们去掉最后一个字符后的子串的最长公共子序列长度加1。
- 如果s1和s2的最后一个字符不相同,则最长公共子序列的长度为它们分别去掉最后一个字符后的子串的最长公共子序列长度的最大值。
代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e3 + 10, M = 1e3 + 10;
int n, m;
string str1, str2;
int lcs (string s1, string s2)
{
if (s1.empty() || s2.empty())
return 0;
if (s1.back() == s2.back())
return lcs(s1.substr(0, s1.size()-1), s2.substr(0, s2.size()-1)) + 1;
else{
return max(lcs(s1.substr(0, s1.size()-1), s2), lcs(s1, s2.substr(0, s2.size()-1)));
}
}
int main()
{
cin >> n >> m >> str1 >> str2;
cout << lcs(str1, str2);
return 0;
}
三、本题考察算法:LCS😊
思路:
考虑前两种做法,都是直接在序列本身上进行判断,等价于见一个杀一个,毫无章法,现在采用动态规划,递推规律的解法:即从子问题推导原问题,并且沿途记录子问题的答案,以方便后续递推大问题的时候可以直接查表利用!
acbd
abedc
如上两个 序列,假如两个字符串的序列长度均为1,此时的答案=1,那么在考虑长度为2的情况,很明显还是=1,再考虑长度为3的情况,此时第一个字符串中有
a
b
ab
ab子序列,第二个字符串中也有ab子序列,所以此时答案 = 2,考虑长度为4,同理此时答案为 3(abd);所以说递推出来的最大值即为最优解:3。
从左往右,从小到大,顺序递推,找出通用的递推公式,即能做到不重不漏地枚举!
- 由于是两个序列,所以说我们的dp数组是二维的;
f[i][j] :考虑第一个序列的前i个字符,第二个序列的前 j 个字符的公共子序列的长度的最大值。 - 属性:最大值;
- 计算:即通用的递推公式!找最后一个不同点。这里只解释下为什么会取:
f [ i ] [ i ] = m a x ( f [ i − 1 ] [ j ] , f [ i ] [ j − 1 ] ) ; f[i][i] = max(f[i-1][j], f[i][j-1]); f[i][i]=max(f[i−1][j],f[i][j−1]);
代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e3 + 10;
int f[N][N];
int n, m;
char s1[N], s2[N];
int main()
{
cin >> n >> m >> (s1+1) >> (s2+1);
//因为有f[i-1],所以下标从1开始!
for (int i=1; i <= n; i ++)
{
for (int j=1; j <= m; j ++)
{
//假设存在上一次的计算的f[i][j], 那么经过j++后的f[i][j]必然包含上一次的f[i][j](子集),
//那么此次的f[i][j]则会继承上一次的最优解。你有的我一定有!我可能有的,你一定没有!
f[i][j] = max(f[i-1][j], f[i][j-1]);
if (s1[i] == s2[j]) //这就是我可能有的!
f[i][j] = f[i-1][j-1] + 1;
}
}
cout << f[n][m];
return 0;
}