文章目录
一、前言
在上一篇博客《动态规划-线性Dp和区间Dp详解》中,已经详解了一些比较基础的线性Dp,比如:数字三角形、最长上升子序列、最长公共子序列,而这篇博客将介绍比较困难的线性Dp思路,将会详解:最长上升子序列优化算法、最短编辑距离、编辑距离。
二、最长上升子序列(优化)
1.问题描述
给定一个长度为 N的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数 N。
第二行包含 N个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
数据范围
1≤N≤100000,
−109≤数列中的数≤109
输入样例
7
3 1 2 1 8 5 6
输出样例
4
2.算法
- 状态表示:q[i]表示在从长度为i的所有上升子序列集合中,取末尾元素最小的一个方案,该方案的末尾元素即为q[i]的值,我们可以知道q[i]时严格单调递增的
- 状态计算:我们可以将所有元素a[N]一个个插入q[N]中(用折半查找法),找到比a[i]大的最小的一个q[j],更新q[j]为a[i]即可,并及时更新最长长度
- 该题目的思路更接近贪心
代码
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 10010;
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; //设置边界,使得总能找到所需要的q[i]
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); //更新最长长度,r+1是该情况的长度
q[r + 1] = a[i];
}
printf("%d\n", len);
return 0;
}
三、最短编辑距离
1.题目描述
给定两个字符串 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
2.算法
- 状态表示:两个数组分别表示为a[N]、b[N],则f[i][j]表示所有将a[1…i]变成b[1…j]的操作方式集合中,操作次数最少的方案,该方案的操作次数即为f[i][j]的值
- 状态计算:我们可以将f[i][j]的计算从其上一步来源出发,即a的最后一个元素a[i]要如何处理,分为三种情况:增、删、改(有条件)
- 增:f[i][j-1]+1
- 删:f[i-1][j]+1
- 改:a[i]==b[j]则f[i-1][j-1],a[i]!=b[j]则f[i-1][j-1]+1
代码
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N];
int main()
{
scanf("%d%s", &n, a + 1);
scanf("%d%s", &m, b + 1);
//初始化
for(int i = 0; i <= m; i ++ ) f[0][i] = i; //全增
for(int i = 0; i <= n; i ++ ) f[i][0] = 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);
}
printf("%d\n", f[n][m]);
return 0;
}
四、编辑距离
1.题目描述
给定 n 个长度不超过 1010 的字符串以及 m 次询问,每次询问给出一个字符串和一个操作次数上限。
对于每次询问,请你求出给定的 n 个字符串中有多少个字符串可以在上限操作次数内经过操作变成询问给出的字符串。
每个对字符串进行的单个字符的插入、删除或替换算作一次操作。
输入格式
第一行包含两个整数 n 和 m。
接下来 n 行,每行包含一个字符串,表示给定的字符串。
再接下来 m 行,每行包含一个字符串和一个整数,表示一次询问。
字符串中只包含小写字母,且长度均不超过 1010。
输出格式
输出共 m 行,每行输出一个整数作为结果,表示一次询问中满足条件的字符串个数。
数据范围
1≤n,m≤1000,
输入样例
3 2
abc
acd
bcd
ab 1
acbd 2
输出样例
1
3
2.算法
- 此题的状态表示和状态计算与上一题一致
- 区别:上一题是一对一,该题是多对多,加两个循环就可以了
代码
#include <iostream>
#include <algorithm>
#include <string.h>
using namespace std;
const int N = 15, M = 1010;
int n, m;
int f[N][N];
char str[M][N];
int edit_distance(char a[], char b[])
{
int la = strlen(a + 1), lb = strlen(b + 1);
for (int i = 0; i <= lb; i ++ ) f[0][i] = i;
for (int i = 0; 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()
{
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);
}
return 0;
}
五、总结
动态规划和贪心有一定的相似之处,有些问题两种思路都可以解决。由于Dp问题多变,还需多见题型,重在思考与思路,体会如何找到一道题的状态表示,并根据数据关系(多是上一步的起源)找到状态计算方法。