线性DP
本讲前言
我们把具有线性“阶段”划分的动态规划算法统称为线性DP。
本讲我们将学习数字三角形、最长上升子序列(LIS),最长上升子序列二分优化、最长公共子序列、以及一题习题:最短编辑距离 、编辑距离,共六题。
同样的,在学习本讲例题、习题中,我们继续贯彻以集合为核心的分析法去分析每一道题。
经典例题
例题 数字三角形
问题描述
给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输入格式
第一行包含整数
接下来
数据范围:
输出格式
输出一个整数,表示最大的路径数字和。
输入样例
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输出样例
30
题目分析
思路一:
代码如下:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 550;
int a[N][N], f[N][N];
int main() {
int n;
cin >> n;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= i; j++)
cin >> a[i][j];
memset(f, -0x3f, sizeof f);
for(int i = 1; i <= n; i++)
for(int j = 1; j <= i; j++)
if(i == 1 && j == 1) f[i][j] = a[i][j];
else
f[i][j] = max(f[i - 1][j - 1], f[i - 1][j]) + a[i][j];
int ans = -1e9;
for(int i = 1; i <= n; i++) ans = max(ans, f[n][i]);
cout << ans;
return 0;
}
思路二:
#include <iostream>
using namespace std;
const int N = 510;
int n;
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 = 1;i <= n;i++) f[n][i] = a[n][i];
for (int i = n - 1;i >= 1;i--) {
for (int j = 1;j <= i;j++) {
f[i][j] = max (f[i + 1][j],f[i + 1][j + 1]) + a[i][j];
}
}
cout << f[1][1] << endl;
return 0;
}
通过本题可以发现,状态表示方法、含义、条件不同,最后状态转移(集合划分)的计算也会不同,关键是在于我们在处理过程中保证能够得到每一步的答案,且这个答案一定不漏掉任何正确的答案。
其实,DP本身也是对暴力枚举的优化,利用了已知信息去推理!
例题 最长上升子序列I(LIS)
题目描述
给定一个长度为
输入格式
第一行包含整数
第二行包含
数据范围:
输出格式
输出一个整数,表示最大长度。
输入样例
7
3 1 2 1 8 5 6
输出样例
4
题目分析
示例代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int a[N], f[N];
int n;
int main() {
cin >> n;
for(int i = 1; i <= n; i++) {
cin >> a[i];
f[i] = 1;
}
for(int i = 2; i <= n; i++)
for(int j = 1; j < i; j++)
if(a[j] < a[i])
f[i] = max(f[i], f[j] + 1);
int mx = 1;
for(int i = 1; i <= n; i++)
mx = max(mx, f[i]);
cout << mx;
return 0;
}
例题 最长上升子序列II
题目描述
给定一个长度为
输入格式
第一行包含整数
第二行包含
数据范围:
输出格式
输出一个整数,表示最大长度。
输入样例
7
3 1 2 1 8 5 6
输出样例
4
题目分析
本题与上题的区别在于,数据范围扩大了,此时如果还使用例题二的做法(
先来思考一个例子:5 3 ...
此例子开头是
搞清楚这个例子,我们就可以想到,如果当前的子序列是最优子序列(每一个都是最优的值),那么我们只需要往后面添加值或更新已经求过的最小值就好了,有一个比较可行的方法是:用
假设,我们现在已经求出了
第一种情况,
第二种情况,
实际上,这个做法就是,在每一个新来的
而在递增序列中找到最后一个小于
时间复杂度分析:状态数
示例代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5 + 20;
int a[N], f[N];
int main() {
int n;
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
int len = 0;
f[0] = -2e9;
for(int i = 1; i <= n; i++) {
int l = 0, r = len;
while(l < r) {
int mid = l + r + 1 >> 1;
if(f[mid] < a[i]) l = mid;
else r = mid - 1;
}
len = max(len, r + 1);
f[r + 1] = a[i];
}
cout << len;
return 0;
}
使用lower_bound()
代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5 + 20;
int a[N], f[N];
int main() {
int n;
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
int len = 0;
f[0] = -2e9;
for(int i = 1; i <= n; i++) {
if(a[i] > f[len])
f[++len] = a[i];
else
*lower_bound(f, f + len + 1 , a[i]) = a[i];
}
cout << len;
return 0;
}
例题 最长公共子序列
题目描述
给定两个长度分别为
输入格式
第一行包含两个整数
第二行包含一个长度为
第三行包含一个长度为
字符串均由小写字母构成。
数据范围:
输出格式
输出一个整数,表示最大长度。
输入样例
4 5
acbd
abedc
输出样例
3
题目分析
上图值得注意的是:状态计算中集合划分的第一部分其实是被包含在第二部分或者第三部分中的,所以实际代码中可以不写,其次,对于第二个部分和第三部分,所写状态其实不是完全精准的,以第二个部分举例说明:
不选
那是因为我们求的是最大值,而
类似于:求
示例代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1111;
int f[N][N];
int main()
{
int n, m;
string a, b;
cin >> n >> m >> a >> b;
a = " " + a;
b = " " + b;
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];
return 0;
}
习题一 最短编辑距离
问题描述
给定两个字符串
- 删除–将字符串
中的某个字符删除。 - 插入–在字符串
的某个位置插入某个字符。 - 替换–将字符串
中的某个字符替换为另一个字符。
现在请你求出,将
输入格式
第一行包含整数
第二行包含一个长度为
第三行包含整数
第四行包含一个长度为
字符串中均只包含大小写字母。
数据范围:
输出格式
输出一个整数,表示最少操作次数。
输入样例
10
AGTCTGACGC
11
AGTAAGTAGGC
输出样例
4
题目分析
对于状态计算:
- 删除
,意味着需要让 的值加 ,因为删除之后可以让 等于 - 在
处增加一个字符,意味着需要让 的值加 ,因为增加之后可以让增加字符等于 ,那么实现 的 个字符变成b的 字符。 - 修改
:看目前 是否等于 ,如果相等,则没必要改,状态为 ,如果不相等,则需要改,状态为
示例代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int f[N][N];
int main() {
int n, m;
string a, b;
cin >> n >> a >> m >> b;
a = " " + a;
b = " " + b;
for(int i = 0; i <= n; i++) f[i][0] = i;
for(int i = 0; i <= m; i++) f[0][i] = i;
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];
return 0;
}
习题二 编辑距离
问题描述
给定
对于每次询问,请你求出给定的
每个对字符串进行的单个字符的插入、删除或替换算作一次操作。
输入格式
第一行包含两个整数
接下来
再接下来
字符串中只包含小写字母,且长度均不超过
数据范围:
输出格式
输出共
输入样例
3 2
abc
acd
bcd
ab 1
acbd 2
输出样例
1
3
题目分析
直接用习题一的方法即可。
示例代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int f[N][N];
int find(string a, string b) {
int n = a.size(), m = b.size();
a = " " + a;
b = " " + b;
for(int i = 0; i <= n; i++) f[i][0] = i;
for(int i = 0; i <= m; i++) f[0][i] = i;
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); //改
}
return f[n][m];
}
int main() {
int n, m;
string a[N], b;
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> a[i];
while (m -- ) {
int k;
cin >> b >> k;
int sum = 0;
for(int i = 1; i <= n; i++) {
int ans = find(a[i], b);
if(ans <= k) sum ++;
}
cout << sum << endl;
}
return 0;
}