目录
898. 数字三角形
895. 最长上升子序列
最长上升子序列的保存路径问题
895. 最长上升子序列 II
897. 最长公共子序列
902. 最短编辑距离
899. 编辑距离
给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输入格式
第一行包含整数 nn,表示数字三角形的层数。
接下来 nn 行,每行包含若干整数,其中第 ii 行表示数字三角形第 ii 层包含的整数。
输出格式
输出一个整数,表示最大的路径数字和。
数据范围
1≤n≤500,
−10000≤三角形中的整数≤10000
输入样例:
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输出样例:
30
思路:
闫氏DP分析法
根据状态转移列方程即可
代码如下:
#include
#include
using namespace std;
const int N = 510, INF = 1e9;
int n, m;
int a[N][N];
int f[N][N];
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++ )
for(int j = 1; j <= i; j ++ )
cin >> a[i][j];
for(int i = 0; i <= n; i ++ )
for(int j = 0; j <= i + 1; j ++ )
f[i][j] = -INF;
f[1][1] = a[1][1];
for(int i = 2; i <= n; i ++ )
{
for(int j = 1; j <= i; j ++ )
{
f[i][j] = max(f[i-1][j-1] + a[i][j], f[i-1][j] + a[i][j]);
}
}
int res = -INF;
for(int i = 1; i <= n; i ++ ) res = max(res, f[n][i]);
cout << res << endl;
return 0;
}
给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数 N。
第二行包含 N 个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
数据范围
1≤N≤1000,
−10^9≤数列中的数≤10^9
输入样例:
7
3 1 2 1 8 5 6
输出样例:
4
思路:
闫氏DP分析法
代码如下:
#include
#include
using namespace std;
const int N = 1010;
int n;
int a[N];
int f[N];
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
for(int i = 1; i <= n; i ++ )
{
f[i] = 1;
for(int j = 1; j < i; j ++ )
{
if(a[j] < a[i])
f[i] = max(f[i], f[j] + 1);
}
}
int res = 0;
for(int i = 1; i <= n; i ++ )res = max(res, f[i]);
cout << res << endl;
return 0;
}
思路:
开g[N]数组,存路径,g[j] 存的是 j 的上一个点,状态转移时,保存该点的上一个点,即,从哪个状态转移过来的,输出时,先输出 a[k] 点,即,最长子序列的最后一个点,再将 k 变成 g[k](g[k] 表示 k 时从 g[k] 状态转移过来,即,g[k]为k的上一个状态),输出a[k],循环 k 次
代码如下:
#include
#include
using namespace std;
const int N = 1010;
int n;
int a[N];
int f[N], g[N];
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
for(int i = 1; i <= n; i ++ )
{
f[i] = 1;
g[i] = 0;
for(int j = 1; j < i; j ++ )
{
if(a[j] < a[i])
if(f[i] < f[j] + 1)
{
f[i] = f[j] + 1;
g[i] = j;
}
}
}
int k = 0;
for(int i = 1; i <= n; i ++ )
if(f[i] > f[k]) k = i;
cout << f[k] << endl;
for(int i = 0, len= f[k]; i < len; i ++ )
printf("%d ", a[k]), k = g[k];
return 0;
}
给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数 N。
第二行包含 N 个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
数据范围
1≤N≤100000,
−10^9≤数列中的数≤10^9
输入样例:
7
3 1 2 1 8 5 6
输出样例:
4
思路:
本题数据范围较大,不能用第一题的方法,本题方法为 贪心 + 二分,
开 q[N] 数组,保存长度为 i 上升子序列的最后一个数(长度相等的最小的结尾),如此可以得到如图所示的关系
证明:
假设:q[6] < q[5],那么 长度为6的序列的倒数第二个数,即 q[6] 序列的倒数第二个数,一定比 q[5] 小,此时长度为 5 的序列应该时 这个数,而不是 q[5],与原来的 q[5] 矛盾,所以q[6] >= q[5]一定成立,证毕。
接着刚才的思路,我们要求的最长上升子序列就是 q 数组中的 i 的最大值,我们用 len 来保存。
当加入一个数 a[i] 时,a[i] 应该加在 q 数组中的小于 a[i] 的数中的最大的数的后面,我们用二分查找来确定,a[i] 在 q 数组中的位置(此处我们设此位置为 r ),更新 len = max(len, r + 1),再把 a[i] 加到 q[r + 1] 的位置,因为 原本的 q[r +1] 位置上的数一定 >= a[i] ,我们要维护 q 数组,则 q[i] 必须时长度为 i 的序列的最小数结尾,因为 原来的 q[r+1] >= a[i] ,所以,将 q[r+1] 更新为 a[i] 时,直接赋值就行,如此 len 即为所求答案。
#include
#include
using namespace std;
const int N = 100010;
int n;
int a[N];
int q[N];
int main()
{
scanf("%d", &n);
for(int i = 0; i < n; i ++ ) scanf("%d", &a[i]);
int len = 0;
q[0] = -2e9;
for(int i = 0; i < n; i ++ )
{
int l = 0, r = len;
while(l < r)
{
int mid = l + r + 1>> 1;
if(q[mid] < a[i]) l = mid;
else r = mid - 1;
}
len = max(len, r + 1);
q[r + 1] = a[i];
}
cout << len << endl;
return 0;
}
给定两个长度分别为 N 和 M 的字符串 AA 和 BB,求既是 AA 的子序列又是 BB 的子序列的字符串长度最长是多少。
输入格式
第一行包含两个整数 N 和 M。
第二行包含一个长度为 N 的字符串,表示字符串 A。
第三行包含一个长度为 M 的字符串,表示字符串 B。
字符串均由小写字母构成。
输出格式
输出一个整数,表示最大长度。
数据范围
1≤N,M≤1000
输入样例:
4 5
acbd
abedc
输出样例:
3
思路: 闫氏DP分析法!
解释四种状态,
第一种为,A 中不选 a[ i ] ,且 B 中不选 b[ j ] 的状态
第二种为,A 中不选 a[ i ] ,但 B 中选 b[ j ] 的状态
第三种为,A 中选 a[ i ] ,但 B 中不选 b[ j ] 的状态
第四种为,a[ i ] 与 b[ i ] 相等, A 中选 a[ i ] ,且 B 中选 b[ j ] 的状态
解释四种方程:
f [ i-1 ][ j-1 ] 表示为, 在A的前 i - 1 个字符中选,同时在 B 的前 j - 1 个字符中选,即,A 中不选 a[ i ] ,且 B 中不选 b[ j ] 的状态,与 00 状态相符合
f [ i-1][ j ] 表示为,在A的前 i - 1 个字符中选,同时在 B 的前 j 个字符中选,即,A 中不选 a[ i ] ,但 B 中不一定选 b[ j ] 的状态(此处与第二种状态的含义不相同,但此种表示方法包含第二种状态,也包含第一种状态),包含 01 状态
f [ i ][ j-1 ] 表示为,在A的前 i 个字符中选,同时在 B 的前 j - 1 个字符中选,即,A 中不选 a[ i ] ,但 B 中不一定选 b[ j ] 的状态(此处与第三种状态的含义不相同,但此种表示方法包含第三种状态,同时也包含第一种状态),包含 10 状态
f [ i -1 ][ j - 1 ] 表示为,在A的前 i - 1 个字符中选,同时在 B 的前 j - 1 个字符中选,同时选 a[ i ] 和 b [ j ] ,即,A 中选 a[ i ] ,且 B 中选 b[ j ] 的状态,与 11 状态相符
虽然此处我们状态计算的四种状态时有重复的状态的,虽然违背了不重不漏原则,但是不影响求最大值,因此此处的方法时可行的
代码如下:
#include
#include
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N];
int main()
{
scanf("%d%d",&n,&m);
scanf("%s%s",a+1,b+1);
for(int i = 1; i <= n; i ++ )
{
for(int j = 1; j <= m; j ++ )
{
f[i][j] = max(f[i-1][j], f[i][j-1]);
if(a[i] == b[j]) f[i][j] = max(f[i][j], f[i-1][j-1] + 1);
}
}
cout << f[n][m] << endl;
return 0;
}
给定两个字符串 A 和 B,现在要将 A 经过若干操作变为 B,可进行的操作有:
现在请你求出,将 A 变为 B 至少需要进行多少次操作。
输入格式
第一行包含整数 n,表示字符串 A 的长度。
第二行包含一个长度为 n 的字符串 A。
第三行包含整数 m,表示字符串 B 的长度。
第四行包含一个长度为 m 的字符串 B。
字符串中均只包含大小写字母。
输出格式
输出一个整数,表示最少操作次数。
数据范围
1 ≤ n, m ≤ 1000
输入样例:
10
AGTCTGACGC
11
AGTAAGTAGGC
输出样例:
4
思路: 闫氏DP分析法, 线性DP
解释三种状态:
删:删去 a[ i ] ,使得 a[ 1~i ] 变为 b[ 1~j ] 的操作方式
增:在 a[ i ] 后面增加 b[ j ] ,使得 a[ 1~i ] 变为 b[ 1~j ] 的操作方式
改:改变 a[ i ] ,使得 a[ 1~i ] 变为 b[ 1~j ] 的操作方式
解释三种方程:
删:要使删去 a[ i ] 后,a[ 1~i ] 变为 b[ 1~j ],前提为 a[ 1~i-1 ] 与 b[ 1~ j ] 相等,即,需要先做 f[ i-1, j ] (即,将 a[ 1~i-1 ] 变为 b[ 1~j ] 的最小操作数 ),在此基础上,删去 a[ i ],操作数 + 1,达成 f[ i ] [ j ] ,(即,将 a[ 1~i ] 变为 b[ 1~j ] 的最小操作数)。
增:要使在 a[ i ] 后面增加 b[ j ] ,使得 a[ 1~i ] 变为 b[ 1~j ] ,前提为 a[ 1~i ] 与 b[ 1~ j-1 ] 相等,即,需要先做 f[ i, j-1 ] (即,将 a[ 1~i ] 变为 b[ 1~j-1 ] 的最小操作数 ),在此基础上,在 a[ i ] 后面增加一个 b[ j ] ,操作数 + 1,达成 f[ i ] [ j ] ,(即,将 a[ 1~i ] 变为 b[ 1~j ] 的最小操作数)。
改:要使改变 a[ i ] ,使得 a[ 1~i ] 变为 b[ 1~j ] ,前提为 a[ 1~i-1 ] 与 b[ 1~ j-1 ] 相等,即,需要先做 f[ i-1, j-1 ] (即,将 a[ 1~i-1 ] 变为 b[ 1~j-1 ] 的最小操作数 ),在此基础上,判断 a[ i ] 是否需要改(即,a[ i ] 与 b[ j ] 是否相等),此处分为两种情况:
1> a[ i ] == b[ j ] ,已经相等不需要操作
2> a[ i ] != b[ j ],不相等,把 a[ i ] 改为 b[ j ] ,操作数 + 1
如此可达成 f[ i ] [ j ] ,(即,将 a[ 1~i ] 变为 b[ 1~j ] 的最小操作数)
注意初始化问题:
此题与之前的题目不同,
之前的题目当 i == 0 或 j == 0 时,的状态时 0 ,因 f[N][N]数组设为全局变量,不需要初始化为 0 ;
此题中,f[ 0 ][ i ] 含义为,将 空字符串 a ,变为 b[ 1~i ],需要增加操作 1~m 次,(此处 m 为字符串 b 的长度);f[ i ][ 0 ] 含义为,将 a[ 1~i ] 变为 空字符串 b,需要删去操作 1~n 次,(此处 n 为字符串 a 的长度);
思路结束,接下来看代码!
代码如下:
#include
#include
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N];
int main()
{
cin >> n >> a + 1;
cin >> m >> b + 1;
// 初始化
for(int i = 1; i <= m; i ++ ) f[0][i] = i;
for(int i = 1; i <= n; i ++ ) f[i][0] = i;
// DP
for(int i = 1; i <= n; i ++ )
for(int j = 1; j <= m; j ++ )
{
f[i][j] = min(f[i-1][j] + 1, f[i][j - 1] + 1);
if(a[i] == b[j]) f[i][j] = min(f[i][j], f[i-1][j-1]);
else f[i][j] = min(f[i][j], f[i-1][j-1] + 1);
}
cout << f[n][m] << endl;
return 0;
}
给定 n 个长度不超过 10 的字符串以及 m 次询问,每次询问给出一个字符串和一个操作次数上限。
对于每次询问,请你求出给定的 n 个字符串中有多少个字符串可以在上限操作次数内经过操作变成询问给出的字符串。
每个对字符串进行的单个字符的插入、删除或替换算作一次操作。
输入格式
第一行包含两个整数 n 和 m。
接下来 n 行,每行包含一个字符串,表示给定的字符串。
再接下来 m 行,每行包含一个字符串和一个整数,表示一次询问。
字符串中只包含小写字母,且长度均不超过 10。
输出格式
输出共 mm 行,每行输出一个整数作为结果,表示一次询问中满足条件的字符串个数。
数据范围
1 ≤ n , m ≤ 1000,
输入样例:
3 2
abc
acd
bcd
ab 1
acbd 2
输出样例:
1
3
思路:
编辑最短距离的练习题,多实例 时间复杂度为 O(1e8) ,题目给的 2s 内可以跑过,直接套板子
代码如下:
#include
#include
#include
using namespace std;
const int N = 15, M = 1010;
int n, m;
char str[M][N];
int f[N][N];
int edit_distance(char a[], char b[])
{
int la = strlen(a + 1), lb = strlen(b + 1);
for(int i = 1; i <= lb; i ++ ) f[0][i] = i;
for(int i = 1; i <= la; i ++ ) f[i][0] = i;
for(int i = 1; i <= la; i ++ )
for(int j = 1; j <= lb; j ++ )
{
f[i][j] = min(f[i-1][j] + 1, f[i][j-1] + 1);
f[i][j] = min(f[i][j], f[i-1][j-1] + (a[i] != b[j]));
}
return f[la][lb];
}
int main()
{
cin >> n >> m;
for(int i = 0; i < n; i ++ ) cin >> str[i] + 1;
while(m -- )
{
char q[N];
int limit;
cin >> q + 1 >> limit;
int res = 0;
for(int i = 0; i < n; i ++ )
if( edit_distance(str[i], q) <= limit)
res ++;
cout << res << endl;
}
return 0;
}