目录
一、算法解释
适用范围:
dp一般用于解决多阶段决策问题,即每个阶段都要做一个决策,全部的决策是一个决策序列,要你求一个最好的决策序列使得这个问题有最优解。
将待求解的问题分为若干个相互联系的子问题,只在第一次遇到的时候求解,然后将这个子问题的答案保存下来,下次又遇到的时候直接拿过来用即可。
该部分没有固定模板,只有从状态表示和状态计算两个方面思考的大致思路。
注意时间复杂度,例如C++每秒是10的7次方-8次方
和其他算法的区别:
dp和分治的不同之处:在于分治分解而成的子问题必须没有联系(有联系的话就包含大量重复的子问题,那么这个问题就不适宜分治,虽然分治也能解决,但是时间复杂度太大,不划算),所以用dp的问题和用分治的问题的根本区别在于分解成的子问题之间有没有联系,这些子问题有没有重叠,即有没有重复子问题。
dp和贪心的不同之处:在于每一次的贪心都是做出不可撤回的决策(即每次局部最优),而在dp中还有考察每个最优决策子序列中是否包含最优决策子序列,即是否具有最优子结构性质,贪心中每一步都只顾眼前最优,并且当前的选择是不会依赖以前的选择的,而dp,在选择的时候是从以前求出的若干个与本步骤相关的子问题中选最优的那个,加上这一步的值来构成这一步那个子问题的最优解。
(该部分摘自《到底什么是dp思想》)
二、经典例题:数字三角形
给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。
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
输出样例:
30
解析:
#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()
{
cin>>n;
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
cin>>a[i][j];
}
}
//对f进行初始化
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
解析:
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1010;
int n; //原数列长度为n
int a[N],f[N]; //f[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; //只有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]);
cout<<res<<endl;
return 0;
}
四、最长公共子序列
给定两个长度分别为 N 和 M 的字符串 A 和 B,求既是 A 的子序列又是 B 的子序列的字符串长度最长是多少。
输入格式
第一行包含两个整数 N 和 M。
第二行包含一个长度为 N 的字符串,表示字符串 A。
第三行包含一个长度为 M 的字符串,表示字符串 B。
字符串均由小写字母构成。
输出格式
输出一个整数,表示最大长度。
数据范围
1≤N,M≤1000
输入样例:
4 5
acbd
abedc
输出样例:
3
解析:
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1010;
int n,m; //字符串A B的长度
char a[N],b[N];
int f[N][N];
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++) cin>>b[i];
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
//由于第三种情况需ai=bj,所以不一定存在
//这里先讨论2,3种之间的最大值
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];//即在a的前n个字母,b的前m个字母中出现的最大值
return 0;
}
五 、最短编辑距离
给定两个字符串 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
解析:
#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()
{
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
cin>>m;
for(int i=1;i<=m;i++)cin>>b[i];
//初始化
for(int i=0;i<=m;i++)f[0][i]=i;
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);
}
}
cout<<f[n][m];
return 0;
}
六、编辑距离(和上题十分相似)
给定 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> //strlen的库函数
using namespace std;
const int N=15,M=1010;
int n,m;//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=0;i<=lb;i++)f[0][i]=i;
for(int i=0;i<=la;i++)f[i][0]=i;
//DP
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);
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()
{
cin>>n>>m;
for(int i=0;i<n;i++) cin>>str[i];
while(m--)
{
char s[N];//每次询问的字符串
int limit; //操作上限
for(int i=0;i<m;i++) cin>>str[i]>>limit;
int res=0;//一次询问中满足条件的字符串个数
for(int i=0;i<n;i++)
{
if(edit_distance(str[i],s) <= limit) res++;
}
cout<<res;
}
return 0;
}
七 、区间DP(状态表示是一个区间)
石子合并
设有 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
解析:
#include<iostream>
#include<algorithm>
using namespace std;
const int N =310;
int n; //共有n堆
int s[N]; //每堆石子重量
int f[N][N];
int main()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>s[i];
//前缀和算最后一次
for(int i=1;i<=n;i++) s[i]+=s[i-1];
for(int len=2;len<=n;len++){ //长度,长度为1(只有一堆,无需代价)
for(int i=1;i+len-1<=n;i++)
{
int l=r;r=i+len-1; //左右端点
f[l][r]=1e8; //f[i][j]需初始化成一个较大的数,否则输出一直为0
for(int k=l;k<r;k++){ //从左端点到右端点
f[l][r]=min(f[i][r],f[l][k]+f[k+1][r]+ s[r]-s[l-1]);
}
}
}
cout<<f[1][n];
return 0;
}