动态规划(二):线性DP
动态规划:线性DP(数字三角形、最长上升子序列、最长公共子序列、最短编辑距离)
AcWing 898.数字三角形
给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。

输入格式
第一行包含整数 \(n\),表示数字三角形的层数。 接下来 \(n\) 行,每行包含若干整数,其中第 \(i\) 行表示数字三角形第 \(i\) 层包含的整数。
输出格式
输出一个整数,表示最大的路径数字和。
数据范围
\(1≤n≤500, −10000≤三角形中的整数≤10000\)
输入样例:
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输出样例:
30
Code:
#pragma once
#include <iostream>
using namespace std;
const int N = 510, INF = 1e9;
int f[N][N]; //存放状态
int number_triangle[N][N]; //存放数字三角形的数据
int n; //三角形有几层
void top_down_version()
{
std::cin >> n; //输入层
// 输入每层的数字,从 1 - n, 因为状态转移方程有i - 1
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= i; ++j)
std::cin >> number_triangle[i][j];
// 将所有状态初始化成负无穷,因为我们要求最大值
// 还要从左上和正上获取数据,没有的数据的状态需要初始化成负无穷
// 否则影响最大值判断
for (int i = 0; i <= n; ++i) // 这里也要从0开始多初始化一列,因为还有从左上来的
for (int j = 0; j <= i + 1; ++j) //这里要多初始化一个,因为也有从上方来的
f[i][j] = -INF;
f[1][1] = number_triangle[1][1]; //状态初始化为第一个数字
for (int i = 2; i <= n; ++i) //从第二行开始更新状态
for (int j = 1; j <= i; ++j) //这里i就够了, 因为更新状态的所用的状态已经被初始化了,只需要考虑有数字的地方状态值就可以了
// 书写状态方程,分为从左上来的和从上方来的
f[i][j] = std::max(f[i - 1][j - 1] + number_triangle[i][j], f[i - 1][j] + number_triangle[i][j]);
int res = -INF;
for (int i = 1; i <= n; ++i)
res = std::max(res, f[n][i]); //最终答案就是最后一行最大的那个
std::cout << res << std::endl;
}
// 自底向上版本,更好,不用考虑边界值
void bottom_up_version()
{
cin >> n;
// 输入数据, 状态转移数组和三角形数组是一个数组
// 因为我们要自底向上推,所以之前的状态需要知道
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= i; ++j)
cin >> f[i][j];
// 状态转移方程数组和三角形数组是一个数组
// 因为是右下和正下,所以从n-1开始,和i从2开始时一个道理
// 但是好处是不用给f数组初始化状态,默认输入的数据就是初始状态
for (int i = n - 1; i >= 1; --i)
for (int j = i; j >= 1; --j)
// 从右下方和从正下方的数据中最大的加上自己本身就是最大的
// 一步一步往上推
f[i][j] = max(f[i + 1][j + 1], f[i + 1][j]) + f[i][j];
cout << f[1][1] << endl;
}
AcWing 895. 最长上升子序列
给定一个长度为 \(N\) 的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数 \(N\)。 第二行包含 \(N\) 个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
数据范围
\(1≤N≤1000, −10^9 \leq 数列中的数 \leq 10^9\)
输入样例:
10
-5 -7 -2 1 -9 -7 4 5 6 1
输出样例:
6
f[1 ~ 10] = {1, 1, 2, 3, 1, 2, 4, 5, 6, 3};
Code:
#include <iostream>
using namespace std;
const int N = 1010;
int a[N], f[N], n;
int main()
{
// 处理输入
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> a[i];
// 状态计算
for (int i = 1; i <= n; ++i)
{
f[i] = 1; //初始化为1,f[i]=1,表示初始化以第i个字符为结尾的最长上升子序列长度为1,即假设到i为止一直在降序
for (int j = 1; j < i; ++j) //然后遍历1 ~ i-1, 把升序的字符一个一个加进来,j表示i的上一个小于它的字符
if (a[j] < a[i]) //如果第j个字符小于第i个字符,说明f[j]是升序子序列,且j小于i,那么f[i]应该更新成f[j] + 1;多加的这个1就是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]); //然后哪个f[i]最大,注意并不是f[n]就一定是最大的, 因为f[i]表示以a[i]结尾的最长子序列的长度
/*
例如这个:f[n]就不是
10
-5 -7 -2 1 -9 -7 4 5 6 1
*/
cout << res << endl;
return 0;
}
AcWing 896. 最长上升子序列II
给定一个长度为N的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式 第一行包含整数N。 第二行包含N个整数,表示完整序列。
输出格式 输出一个整数,表示最大长度。
数据范围 1≤N≤100000, −109≤数列中的数≤109 输入样例: 7 3 1 2 1 8 5 6 输出样例: 4
Code: 上升子序列长度的结尾值随着长度的增加而单调递增, 即q数组是严格单调递增的。 因此,a[ i ]接到最大的小于a[ i ]的结尾值的序列的后面(采用二分实现)
#include <iostream>
using namespace std;
const int N = 100010;
int n, a[N], q[N]; //q[i]表示所有长度为i的上升子序列结尾值得集合,q[i]得属性值是这些集合得最小值
int main()
{
cin >> n;
for (int i = 0; i < n; ++i) cin >> 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;
// 如果q[m]小于a[i]说明a[i]因该在q[mid]得后面
if (q[mid] < a[i]) l = mid;
// 否则说明应该在前面
else r = mid - 1;
}
len = max(len, r + 1);
q[r + 1] = a[i]; // 这里l, r 都行,因为最终l==r
}
cout << len << endl;
return 0;
}
AcWing 897. 最长公共子序列
给定两个长度分别为 \(N\) 和 \(M\) 的字符串 \(A\) 和 \(B\),求既是 \(A\) 的子序列又是 \(B\) 的子序列的字符串长度最长是多少。
输入格式
第一行包含两个整数 \(N\) 和 \(M\)。
第二行包含一个长度为 \(N\) 的字符串,表示字符串 \(A\)。
第三行包含一个长度为 \(M\) 的字符串,表示字符串 \(B\)。
字符串均由小写字母构成。
输出格式
输出一个整数,表示最大长度。
数据范围
\(1≤N,M≤1000\)
输入样例:
4 5
acbd
abedc
输出样例:
3
Code:
#pragma once
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N];
// 集合:所有在第一个序列前i个字母出现,且在第二个序列的前j个字母出现得子序列
void lcs()
{
scanf("%d%d", &n, &m);
scanf("%s%s", a + 1, b + 1);
// 集合划分:假设第一个序列得最后一个字母是a[i]
// 第二个序列的最后一个字母是a[j]
// 分为四类:00,10,01,11;(10分别代表是否包含a[i]不包含b[j],以此类推)
// 注意f[i - 1][j] 不等价于01
// 因为f[i - 1][j] 代表在前i-1个字母出现,且在前j个字母出现的公共子序列的最大值,不一定包含b[j]
// 而01代表所有不包含a[i]但包含b[j]的公共子序列
// 例如:abdcd和abcd, 01表示 ad, bd, abd,这个集合的最大值只能用abd表示
// 而f[i - 1][j]表示a, b, c, d, ab, ac, ad, bc, bd, abd, abc 这个集合的最大值;可以用abd表示,也可以用abc表示
// 但是幸运的是f[i-1][j]包含了01,而且我们是求最大值,与别的部分有重复也可以
// 因为集合的总数量是不变的,所有就算有重复,总的最大值也是恒定的
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]); // 这两种情况涵盖了00
if (a[i] == b[j]) //只有当a[i] == b[j]的时候才可能出现11这种情况
f[i][j] = f[i - 1][j - 1] + 1; //11这种情况肯定是最大的,直接用它更新f[i][j]即可
}
cout << f[n][m] << endl;
}
AcWing 902. 最短编辑距离
给定两个字符串 \(A\) 和 \(B\),现在要将 \(A\) 经过若干操作变为 \(B\),可进行的操作有:
①删除–将字符串A中的某个字符删除。
②插入–在字符串A的某个位置插入某个字符。
③替换–将字符串A中的某个字符替换为另一个字符。
现在请你求出,将 \(A\) 变为 \(B\) 至少需要进行多少次操作。
输入格式
第一行包含整数 \(n\),表示字符串 \(A\) 的长度。
第二行包含一个长度为 \(n\) 的字符串 \(A\)。
第三行包含整数 \(m\),表示字符串 \(B\) 的长度。
第四行包含一个长度为 \(m\) 的字符串 \(B\)。
字符串中均只包含大写字母。
输出格式
输出一个整数,表示最少操作次数。
数据范围
\(1≤n,m≤1000\)
输入样例:
10
AGTCTGACGC
11
AGTAAGTAGGC
输出样例:
4
Code:
#pragma once
#include <iostream>
using namespace std;
const int N = 1010;
char a[N], b[N]; //字符串a和b
int f[N][N]; //表示所有将字符串a的1~i个字母和字符串b的1~j个字母相同采取得操作得集合
// 集合得属性值是最小值
// 有增加删除以及修改三种操作
void the_shortest_edit_times()
{
int n, m; //字符串a 和 b的长度
scanf("%d%s", &n, a + 1);
scanf("%d%s", &m, b + 1);
// 当a字符串为零的时候,只能使用添加操作
for (int j = 0; j <= m; ++j) f[0][j] = j; //有多少个就要添加多少次
// 当b字符串为零得时候,只能采取删除操作
for (int i = 0; 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); //需要最后一步修改操作
}
printf("%d\n", f[n][m]); //把a得前n个字母,边长b得前m个字母
}
AcWing 899. 编辑距离
给定 \(n\) 个长度不超过 \(10\) 的字符串以及 \(m\)次询问,每次询问给出一个字符串和一个操作次数上限。
对于每次询问,请你求出给定的 \(n\) 个字符串中有多少个字符串可以在上限操作次数内经过操作变成询问给出的字符串。
每个对字符串进行的单个字符的插入、删除或替换算作一次操作。
输入格式
第一行包含两个整数 \(n\) 和 \(m\)。 接下来 \(n\) 行,每行包含一个字符串,表示给定的字符串。
再接下来 \(m\) 行,每行包含一个字符串和一个整数,表示一次询问。
字符串中只包含小写字母,且长度均不超过 \(10\)。
输出格式
输出共 \(m\) 行,每行输出一个整数作为结果,表示一次询问中满足条件的字符串个数。
数据范围
\(1≤n,m≤1000\)
输入样例:
3 2
abc
acd
bcd
ab 1
acbd 2
输出样例:
1
3
Code:
本题是最短编辑距离算法的运用
#pragma once
#include <iostream>
#include <string.h>
const int N = 15, M = 1010;
int f_e[N][N];
char str[M][N];
using namespace std;
int edit_distance(char a[], char b[])
{
int la = strlen(a + 1), lb = strlen(b + 1);
for (int j = 0; j <= lb; ++j) f_e[0][j] = j;
for (int i = 0; i <= lb; ++i) f_e[0][i] = i;
for (int i = 1; i <= la; ++i)
for (int j = 1; j <= la; ++j)
{
f_e[i][j] = min(f_e[i - 1][j] + 1, f_e[i][j - 1] + 1);
f_e[i][j] = min(f_e[i][j], f_e[i - 1][j - 1] + (a[i] != b[j]));
}
return f_e[la][lb];
}
void edit_times()
{
int n, m;
scanf("%d%d", &n, &m);
for (int i = 0; i < n; ++i) scanf("%s", str[i] + 1);
while (m--)
{
char s[N];
int limit;
scanf("%s%d", s + 1, &limit);
int res = 0;
for (int i = 0; i < n; ++i)
if (edit_distance(str[i], s) <= limit)
res++;
printf("%d\n", res);
}
}