算法基础课—动态规划(二)线性动态规划、区间动态规划
线性动态规划
递推的顺序有模糊的线性
对于这个二维矩阵,有明显的线性存储的,如是一行一行来存的
数字三角形
题目
给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输入格式
第一行包含整数 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
思想
一个注意点:对于动态规划的初始化
1、有可能a[i][j] == 0, 那么如果不进行初始化,边界也会是0,所以应该将边界设置成-inf
模板
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510, INF = 1e9;
int n;
int a[N][N];
int f[N][N];
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= i; j ++ )
scanf("%d", &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]);
printf("%d\n", res);
return 0;
}
作者:yxc
链接:https://www.acwing.com/activity/content/code/content/58479/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
代码
#include <iostream>
using namespace std;
const int N = 502;
int INF = 1e9;
int f[N][N], a[N][N];
int main(){
int n, i, j;
cin>>n;
for(i = 1; i <= n; i ++){
for(j = 1; j <= i; j ++){
cin>>a[i][j];
}
}
for(i = 1; i <= n; i ++){
for(j = 0; j <= n + 1; j ++){
f[i][j] = -INF;
}
}
for(i = 1; i <= n; i ++){
for(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;
}
最长上升子序列
题目
给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数 N。
第二行包含 N 个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
数据范围
1≤N≤1000,
−109≤数列中的数≤109
输入样例:
7
3 1 2 1 8 5 6
输出样例:
4
思想
f[i] 表示以第i个数结尾的最大上升子序列长度
状态转移:
不包含前面任何一个数
包含倒回去第1个数
包含倒过去第2个数
。。。。
包含倒过去第i-1个数
在这些情况中取一个最大值
状态时n,状态转移时是n,所以时间复杂的是O(n^2)
模板
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n;
int a[N], f[N];
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
for (int i = 1; i <= n; i ++ )
{
f[i] = 1; // 只有a[i]一个数
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]);
printf("%d\n", res);
return 0;
}
作者:yxc
链接:https://www.acwing.com/activity/content/code/content/58497/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
代码
#include <iostream>
using namespace std;
const int N = 1e3 + 10;
int f[N];
int a[N];
int main(){
int i, j, n;
cin>>n;
for(i = 1; i <= n; i ++)
cin>>a[i];
for(i = 1; i <= n; i ++){
f[i] = 1;
for(j = 1; j < i; j ++){
if(a[j] < a[i])
f[i] = max(f[i], f[j] + 1);
}
}
int res = 0;
for(i = 1; i <= n; i ++)
if(f[i] > res) res = f[i];
cout<<res<<endl;
}
代码——记录规划方案
动态规划方案记录——记录每一个状态取到最大值时是由哪个状态转换过来的,之后再从最终状态遍历回去即可
#include <iostream>
#include <vector>
using namespace std;
const int N = 1e3 + 10;
int f[N];
int a[N], g[N];
vector<int> path;
int main(){
int i, j, n;
cin>>n;
g[0] = -1;
for(i = 1; i <= n; i ++)
cin>>a[i];
for(i = 1; i <= n; i ++){
f[i] = 1;
g[i] = 0;
for(j = 1; j < i; j ++){
if(a[j] < a[i]){
if(f[i] < f[j] + 1){
g[i] = j;
f[i] = f[j] + 1;
}
}
}
}
int res = 0;
for(i = 1; i <= n; i ++){
if(f[i] > res) {
res = f[i];
j = i;
}
}
cout<<res<<endl;
for(i = j; g[i] != -1; i = g[i]){
path.push_back(a[i]);
}
for(i = path.size() - 1; i >= 0; i --)
cout<<path[i]<<endl;
}
优化
题目
给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数 N。
第二行包含 N 个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
数据范围
1≤N≤100000,
−109≤数列中的数≤109
输入样例:
7
3 1 2 1 8 5 6
输出样例:
4
思想
在研究是否存在冗余计算的时候发现,如果在长度一样的情况下,后面出现结尾更小的长度相同的上升序列时,前面结尾更大的长度相同的上升序列就没有用了,所以我们可以按长度进行存储,存储的是对应长度下之前结尾最小的上升子序列
设置一个单调数组 q【i】,i表示长度,q【i】表示结尾的值。设置len记录当前单调数组的长度。
设置循环遍历·1-n,每次查找q中比当前a[i] 小的那个最大的数,如果找到的是最后一个,说明长度要加1,如果是在中间,则替换对应的值。
最后结果即为len
模板
#include <iostream>
#include <algorithm>
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;
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];
}
printf("%d\n", len);
return 0;
}
作者:yxc
链接:https://www.acwing.com/activity/content/code/content/62458/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
代码
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int a[N], q[N];
int main(){
int n, i, j, len = 0;
cin>>n;
for(i = 0; i < n; i ++) cin>>a[i];
for(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在最右侧,则长度一定是新增了
q[r + 1] = a[i];
}
cout<<len<<endl;
}
最长公共子序列
题目
给定两个长度分别为 N 和 M 的字符串 A 和 B,求既是 A 的子序列又是 B 的子序列的字符串长度最长是多少。
输入格式
第一行包含两个整数 N 和 M。
第二行包含一个长度为 N 的字符串,表示字符串 A。
第三行包含一个长度为 M 的字符串,表示字符串 B。
字符串均由小写字母构成。
输出格式
输出一个整数,表示最大长度。
数据范围
1≤N,M≤1000
输入样例:
4 5
acbd
abedc
输出样例:
3
思想
对于有两个字符串的,就经常可以用如上图所示的方法来表示i,j
在真实写代码的时候,第二类和第三类会包含第一类的情况,所以不需要单独列出第一类
模板
#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%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);
}
printf("%d\n", f[n][m]);
return 0;
}
作者:yxc
链接:https://www.acwing.com/activity/content/code/content/58527/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
代码
#include <iostream>
using namespace std;
const int N = 1e3 + 10;
int f[N][N];
char a[N], b[N];
int main(){
int n, m, i, j;
scanf("%d%d", &n, &m);
scanf("%s%s", a + 1, b + 1);//a和b要加1
for(i = 1; i <= n; i ++){
for(j = 1; j <= m; j ++){
f[i][j] = max(f[i][j - 1], f[i - 1][j]);
if(a[i] == b[j]) f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1);
}
}
cout<<f[n][m]<<endl;
}
最短编辑距离
题目
给定两个字符串 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
思想
这里的状态表示:
f【i】【j】表示将a【1-i】变成b【1-j】的最小操作次数
状态计算:
从前面已经匹配好的状态再转换成新状态,存在删、增、改三种操作
如果是删除即
第i位和第j位不匹配,于是将第i位删去,同时f【i-1】【j】匹配,所以公式为
f【i-1】【j】 + 1
如果是增加即
第i位和第j-1位之前均匹配,需要加一个b【j】
所以公式为
f【i】【j】= f【i】【j-1】 + 1
如果是修改——a【i】!=b【j】但第i-1 和第j-1以前均匹配,
所以将第i位和第j位修改即可,
一个很重要的点:
状态数组的初始化
1、题目背景的初始化如本题
2、min、max的情况
模板
#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;
}
作者:yxc
链接:https://www.acwing.com/activity/content/code/content/62472/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
代码
#include <iostream>
using namespace std;
const int N = 2000;
char a[N],b[N];
int f[N][N];
int main(){
int n, m, i, j;
scanf("%d%s",&n, a + 1);
scanf("%d%s",&m, b + 1);
//边界的初始化,对于动态规划边界的初始化问题,因题目而定,因min,max而定
for (int i = 0; i <= m; i ++ ) f[0][i] = i;
for (int i = 0; i <= n; i ++ ) f[i][0] = i;
for(i = 1; i <= n; i ++){
for(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;
}
编辑距离
题目
给定 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
模板
#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;
}
作者:yxc
链接:https://www.acwing.com/activity/content/code/content/62479/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
代码
#include <iostream>
#include <string.h>
using namespace std;
const int N = 2010, M = 20;
char a[N][M],b[M];
int f[M][M];
int edit_distance(char a[], char b[]){
int i,j;
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(i = 1; i <= la; i ++){
for(j = 1; j <= lb; 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[la][lb];
}
int main(){
int n, m, i, j, q;
scanf("%d%d",&n, &m);
for(i = 0; i < n; i ++)
scanf("%s", a[i] + 1);
for(i = 0; i < m; i ++){
scanf("%s%d", b + 1, &q);
int cnt = 0;
for(j = 0; j < n; j ++){
int res = edit_distance(a[j], b);
if(res <= q) cnt ++; //不知道为啥加上res 就不对了?????
}
cout<<cnt<<endl;
}
}
区间动态规划
区间合并问题
题目
设有 N 堆石子排成一排,其编号为 1,2,3,…,N。
每堆石子有一定的质量,可以用一个整数来描述,现在要将这 N 堆石子合并成为一堆。
每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。
例如有 4 堆石子分别为 1 3 5 2, 我们可以先合并 1、2 堆,代价为 4,得到 4 5 2, 又合并 1,2 堆,代价为 9,得到 9 2 ,再合并得到 11,总代价为 4+9+11=24;
如果第二步是先合并 2,3 堆,则代价为 7,得到 4 7,最后一次合并代价为 11,总代价为 4+7+11=22。
问题是:找出一种合理的方法,使总的代价最小,输出最小代价。
输入格式
第一行一个数 N 表示石子的堆数 N。
第二行 N 个数,表示每堆石子的质量(均不超过 1000)。
输出格式
输出一个整数,表示最小代价。
数据范围
1≤N≤300
输入样例:
4
1 3 5 2
输出样例:
22
思想
f【i】【j】 表示从第i堆石子合并到第j堆石子的最小代价
状态计算
即最后一次分隔的位置为k,k有可能从i到j-1。去比较不同k位置的情况
代价计算式前缀和
由于区间长度要求计算当前状态的时候,需要的之前的状态都已经算好了,所以不太好用循环来做,更推荐用区间长度来枚举
模板
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 310;
int n;
int s[N];
int f[N][N];
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i ++ ) scanf("%d", &s[i]);
for (int i = 1; i <= n; i ++ ) s[i] += s[i - 1];
for (int len = 2; len <= n; len ++ )
for (int i = 1; i + len - 1 <= n; i ++ )
{
int l = i, r = i + len - 1;
f[l][r] = 1e8;
for (int k = l; k < r; k ++ )
f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
}
printf("%d\n", f[1][n]);
return 0;
}
作者:yxc
链接:https://www.acwing.com/activity/content/code/content/58545/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
代码
#include<iostream>
using namespace std;
const int N = 310;
int f[N][N];
int a[N], s[N];
int main(){
int i, j, len, n, k;
cin>>n;
for(i = 1; i <= n; i ++)
cin>>a[i];
for(i = 1; i <= n; i ++) s[i] = s[i - 1] + a[i];
for(len = 2; len <= n; len ++){
for(i = 1; i + len - 1 <= n; i ++){
int l = i, r = i + len - 1;
f[l][r] = 1e9;
for(k = l; k < r; k ++){
f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);//注意这里是l-1
}
}
}
cout<<f[1][n]<<endl;
}