例题1:密码脱落(2016 年蓝桥真题)
原题链接:蓝桥-密码脱落-链接
题目描述:
X星球的考古学家发现了一批古代留下来的密码。
这些密码是由A、B、C、D 四种植物的种子串成的序列。
仔细分析发现,这些密码串当初应该是前后对称的(也就是我们说的镜像串)。
由于年代久远,其中许多种子脱落了,因而可能会失去镜像的特征。
你的任务是:
给定一个现在看到的密码串,计算一下从当初的状态,它要至少脱落多少个种子,才可能会变成现在的样子。
输入一行,表示现在看到的密码串(长度不大于1000)
要求输出一个正整数,表示至少脱落了多少个种子。
例如,输入:
ABCBA
则程序应该输出:
0
再例如,输入:
ABDCDCBABC
则程序应该输出:
3
算法分析:
dp 数组定义:
dp[i][j]:表示前 i 个字符构成的字符串和后 j 个字符构成的字符串的最长回文子序列
在这里通俗点说,i 就是最长回文子序列的左端点,len-j 就是最长回文子序列的右端点。
dp 数组初始化:
- dp[i][0]=0;
- dp[0][j]=0;
状态转移方程:
这里往回文串方向考虑!
1.如果字符串的子序列两个端点字符一样,即 s[i-1] == s[len-j],(这里为什么是 i-1 而不是 i 是因为字符串类似于字符数组其第一个字符的下标时从 0 开始的!)则有
dp[i][j] = dp[i-1][j-1] + 1;
因为如果当前两端字符匹配,那么以这两个字符为新的端点的最长对称子序列长度,就比去掉这两个字符的子串的最长对称子序列长度多 1(因为这两个字符又构成了一对对称)
2.如果字符串的子序列两个端点字符不一样,则有
dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
这表示当两端字符不匹配时,那么当前位置的最长对称子序列长度可以分两种情况得出:
- 要么是只考虑左边去掉当前字符后的最长对称子序列长度即左边的尾端点长度 i-1+1 表示向右移动一个位置就变成了 i(即
dp[i][j-1]
注意
这里出现 dp[j-1], j-1 是因为字符串字符下标是从 0 开始的所以子字符串长度为 1 时取的是字符串的下标 0,
因此
此时的右边字符子序列的首字符下标为 j-1
) - 要么是只考虑右边去掉当前字符后的最长对称子序列长度即右边字符串的首字符向右移动一个位置即 j-1+1 就变成了 j(即
dp[i-1][j]
注意
这里出现 dp[i-1], i-1 是因为字符串字符下标是从 0 开始的所以子字符串长度为 1 时取的是字符串的下标 0,
因此此时的左边字符子序列的尾字符下标为 i-1
)
然后取上述两种情况的最大值
综上则,最终答案为:字符串原本长度减去最长镜像串的长度
完整代码:
#include<bits/stdc++.h>
using namespace std;
// 数组定义:表示从字符串下标 i 到 len-j 的最长镜像串的长度(镜像串即回文串)
int dp[1010][1010];
int main(){
string s;
cin >> s;
int len = s.length();
for(int i=1;i<=len; i++){
for(int j=1; j<=len; j++){
// 如果字符串的子序列两个端点的字符一样
// 字符数组的下标从 0 开始
if(s[i-1] == s[len-j]){
dp[i][j] = dp[i-1][j-1] + 1;
}
// 如果字符串的子序列两个端点的字符不一样
else{
dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
}
}
cout << len - dp[len][len] << endl;
return 0;
}
例题 2:编辑距离
题目描述:
设A和B是两个字符串。我们要用最少的字符操作次数,将字符串A转换为字符串B。这里所说的字符操作共有三种:
1、删除一个字符;
2、插入一个字符;
3、将一个字符改为另一个字符。
【编程任务】
对任意的两个字符串A和B,计算出将字符串A变换为字符串B所用的最少字符操作次数。
【输入格式】edit.in
第一行为字符串A;第二行为字符串B;字符串A和B的长度均小于200。
【输出格式】edit.out
只有一个正整数,为最少字符操作次数。
【输入样例】
sfdqxbw
gfdgw
【输出样例】
4
算法分析:
dp数组定义:
dp[i][j] 表示记录的字符串 ai 和 bi 的最少编辑次数,其中i 和 j 分别表示字符串 a 和字符串 b 的串长
另一种叙述(似乎这种好理解一点点):表示使得 a字符串的前 i 个字符构成的字符串变成字符串 b 的前 j 个字符的最少编辑次数。
初始化:
两种情况:1.如果 b 串为空,则 a 串要操作 a 串的串长次操作才能完整删除a 串,从而变成 b 串
2.如果 a 串为空;则需要操作 b 串的串长此操作才能让 a 串变成 b 串。
状态转移方程:
1. 当 a[i] == b[i] 时,dp[i][j] = dp[i-1][j-1],即此时 a[i] == b[i] ,是不用编辑的,则 dp[i][j] 就等于前 a 字符串的 i-1 个和 b 字符串的前 j-1 个字符的最少编辑次数。
2. 当 a[i] != b[i] 时,dp[i][j] = min(dp[i-1][j-1]+1,dp[i][j-1]+1,dp[i-1][j]+1),
说明:
- dp[i-1][j-1]+1 : 即将 a[i] 改成 b[i]
- dp[i][j-1]+1 : 即将在 a[i] 后插入b[i]
- dp[i-1][j]+1 : 即删除 a[i]
完整代码:
#include<bits/stdc++.h>
using namespace std;
int dp[210][210];
char a[210], b[210];
int main(){
cin >> a;
cin >> b;
int lenA = strlen(a);
int lenB = strlen(b);
// 字符串 b 为空串
for(int i=1; i<=lenA; i++)
dp[i][0] = i;
// 字符串 a 为空串
for(int i=1; i<=lenB; i++)
dp[0][i] = i;
// 数据处理
for(int i=1; i<=lenA; i++){
for(int j=1; j<=lenB; j++){
if(a[i-1] == b[j-1]){
dp[i][j] = dp[i-1][j-1];
}
else{
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + 1;
}
dp[i][j] = min(dp[i-1][j-1] + 1, dp[i][j]);
}
}
cout << dp[lenA][lenB] << endl;
return 0;
}
总结:
综上,遇到这类题型先分析题目要求根据题目要求给出 dp 数组定义然后分析题目,推导出其状态转移方程,并分析该如何初始化 dp 数组。
其实一般推导出了状态转移方程那么这道题就基本解决了 85%了。