线性DP
898.数字三角形
- 状态方程表示为
f[i,j]=max(f[i-1,j]+a[i,j],f[i-1,j-1]+a[i,j])
#include<iostream>
#include<algorithm>
#include<limits.h>
using namespace std;
const int N=510,INF=1e9;
int f[N][N]; //存状态方程
int a[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];
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;
}
895.最长上升子序列
- 状态转移方程
f[i]=max(f[j])+1,j=0,1,2...,i-1并且满足a[i]>a[j]
#include<iostream>
#include<cstring>
using namespace std;
const int N=1010;
int f[N];
int a[N];
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++){
f[i]=1; //只有a[i]这一个数,最大子序列长度为1
for(int j=1;j<=i-1;j++)
if(a[i]>a[j])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;
}
896.最长上升子序列II
- 注意到数据范围上升到1e6,所以要优化原来的O(n2)做法
- 我们观察原算法的冗余操作
- 以样例数据为例
- 发现在若干长度为1的最大子序列中(3,1,2…)
1的优先度最高
,即倘若枚举长度为2
的最大子序列时,如果某数可以与3拼接,那么一定可以与1拼接,而如果可以与1拼接,却不一定可以与3拼接- 也就是说,
在长度相同的最大子序列当中,我们只保留尾数最小的那个序列
- 可以证明,由此求出的若干最长子序列的尾数是单调递增的
- 那么当数
a[i]
进行拼接时,只需要在长度为1,2,3,...
的若干最长子序列中,找到a[i]>=子序列尾数
并且可以拼接的最长子序列即可- 因为最长子序列数组尾数单调递增,所以查找满足条件的最大尾数只需要二分一次即可,所以时间复杂度为
O(nlogn)
#include<iostream>
using namespace std;
const int N=100010;
int q[N]; //q[i]存储长度为i的最长子序列的尾数
int a[N];
int main(){
int n;
cin>>n;
for(int i=0;i<n;i++)cin>>a[i];
q[0]=-1e9; //因为数组中有负数,所以初始化为最小负数防止第一个值不更新而死循环
int len=0;
for(int i=0;i<n;i++){
int l=0,r=len;
while(l<r){ //找左边界,即小于等于a[i]的第一个值
int mid=l+r>>1;
if(q[mid]>=a[i])r=mid;
else l=mid+1;
}
if(a[i]>q[l]){ //如果a[i]大于找出的左边界,则更新len的长度,并且更新对应的尾数(len有可能不变长,但q[l+1]尾数一定变小或不变,如果不 变小或不变,那么与找的尾数是小于a[i]的最大值冲突)
len=max(len,l+1);
q[l+1]=a[i];
}
else q[l]=a[i]; //如果a[i]等于找出的左边界,则len一定不可能变长,并且q[l]的尾数值变小或不变
}
cout<<len;
}
897.最长公共子序列
- 本题关键在于集合的划分
- 根据
a[i,j]
是否出现在集合中分成四种情况- 其中
f[i-1,j]
f[i,j-1]
可以包含01,10的情况
并且四种情况的最大值可能会有重叠
,但由于最后是求最大值所以没有影响- 注意由于00这种情况
会被包含在01,10情况之内
,所以一般忽略不写
#include<iostream>
#include<cstring>
using namespace std;
const int N=1010;
int f[N][N];
char a[N],b[N];
int main(){
int n,m;
cin>>n>>m;
cin>>a+1>>b+1; //字符串下标从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); //如果a[i]==b[j],更新长度
}
cout<<f[n][m];
}
902 .最短编辑距离
#include<iostream>
using namespace std;
const int N=1010;
char a[N],b[N];
int f[N][N]; //将a[1~i]变成b[1~j]所需要的操作数的最小值
int main(){
int n,m;
cin>>n>>a+1>>m>>b+1;
for(int i=0;i<=m;i++)f[0][i]=i; //a[0]变成b[1~i]需要i步操作
for(int i=0;i<=n;i++)f[i][0]=i; //a[1~i]变成b[0]也需要i步
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++){
f[i][j]=min(f[i][j-1]+1,f[i-1][j]+1); //默认增删
if(a[i]!=b[j])f[i][j]=min(f[i][j],f[i-1][j-1]+1); //如果a[i]和b[j]相同则需要最后一步操作,否则不需要
else f[i][j]=min(f[i][j],f[i-1][j-1]);
}
cout<<f[n][m];
}
899.编辑距离
- 时间复杂度分析
- 一共有n个给定的字符串,处理m次请求,
nm上限均为1000
,处理一对模板串和目标串的时间复杂度是O(n^2^)
,这里模板串和目标串的长度上限均为10
,故时间复杂度是100- 对于m次请求,都要处理n对字符串,每次处理的时间复杂度是100,故总的时间复杂度为 1000 ∗ 1000 ∗ 100 = 1 0 8 1000 *1000 * 100=10^8 1000∗1000∗100=108 可以直接计算
#include<iostream>
#include<string.h>
using namespace std;
const int N=15,M=1010;
char c[M][N]; //存放给定的若干字符串
int f[N][N];
int n,m;
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(){
cin>>n>>m;
for(int i=0;i<n;i++)cin>>c[i]+1;
while(m--){ //处理m次询问
int lim,res=0;
char tar[N]; //目标串
cin>>tar+1>>lim;
for(int i=0;i<n;i++)
if(edit_distance(c[i],tar)<=lim)res++; //小于等于lim,res++
cout<<res<<endl;
}
}
区间DP
282.石子合并
- 状态表示成将第i堆石子到第j堆石子合并成一堆石子的最小代价
- 根据状态方程我们要枚举所有长度为
j-i+1
的区间,求出每个区间合并成一堆区间的最小代价- 状态的计算根据最后一步区间的划分,由于无论如何划分区间,最后一步所需要的代价是固定的
都等于该区间的前缀和s[r]-s[l-1]
,所以当前区间的最小代价只取决于划分出的两个区间f[i,k]+f[k+1,j]的代价和
- 我们从长度
len=2~n
枚举区间后续计算需要用到较小区间的值,且len=1的区间代价为0不需要枚举
- 对于长度为len的所有区间,枚举
k=i~j-1
,将每个区间的状态值f[l][r]
计算出
#include<iostream>
using namespace std;
const int N=310;
int s[N]; //前缀和
int f[N][N];
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>s[i];
s[i]+=s[i-1];
}
for(int len=2;len<=n;len++) // 枚举所有长度的区间
for(int i=1;i+len-1<=n;i++){ //枚举该长度区间所有起止位置l,r
int l=i,r=i+len-1;
f[l][r]=1e9; //初始化改区间代价为一最大值
for(int k=l;k<=r-1;k++){ //对于该区间,再枚举k种划分情况,计算最小代价f[l][r]
f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]+s[r]-s[l-1]);
}
}
cout<<f[1][n];
}