动态规划-线性DP
动态规划-线性DP
1. 线性DP的定义
所谓线性DP,实际上就是:这类问题的状态转移方程满足一定的线性关系。即,状态递推的顺序是线性的,我们把这类DP问题称为线性DP问题。
2. 线性DP例题:数字三角形
https://www.acwing.com/problem/content/900/
我们首先给出一个例子,来帮助理解题意。上图中红色路径的值为:20。我们需要找出总和最大的路径。
首先,DP问题需要考虑两大因素:
1. 状态表示
1.1 首先,我们可以根据上图,来计算数字三角形的每一个点的坐标(i,j)。其中,i表示行数,j表示列数。这里的列数指的是斜线而不是竖线。例如,上图中的画圈数字7,它的坐标是(4,2)。我们在这里规定:行从1开始,列从1开始。
1.2 计算完每一个点的坐标之后,我们就可以发现这个问题的状态为f[i][j]。那么f[i][j]所表示的集合为:所有从起点开始到(i,j)这个点的所有路径的集合。f[i][j]所存储的内容(集合的属性):所有路径中总和的最大值。
2. 状态计算
2.1 根据上述的状态表示,我们可以计算出f[i][j]。具体步骤如下:
2.1.1 我们可以将f[i][j]分为两个子集:从(i,j)的左上方来的路径所构成的集合。从(i,j)的右上方来的路径所构成的集合。
2.1.2 那么我们如何用二维的状态来表示这两个子集呢?计算的方式跟背包问题类似。首先,无论从(i,j)的左上方还是右上方来的路径,这些路径中都包含(i,j)。因此,我们先将这些路径中的(i,j)点去掉。
a. 那么,从(i,j)左上方来的路径所构成的集合可以表示为:从起点开始到(i-1,j-1)这个点的所有路径构成的集合。属性仍为最大值(原因可以参考背包问题,这里不再赘述)。我们可以用f[i-1][j-1]来表示,最后再把(i,j)这个点的值加进去,最终的结果就是:f[i-1][j-1] + a[i][j]。
b. 从(i,j)右上方来的路径所构成的集合可以表示为:从起点开始到(i-1,j)这个点的所有路径构成的集合。属性仍为最大值。我们可以用f[i-1][j]来表示,最后再把(i,j)这个点的值加进去,最终的结果就是:f[i-1][j] + a[i][j]。
因此,根据上述的分析,这个问题的状态转移方程为:
f[i][j] = Max(f[i-1][j-1] + a[i][j] , f[i-1][j] + a[i][j])
注意:在递推的过程中,有些点的左上方或右上方的路径是不存在的(这个状态不存在)。例如:上述案例中:(2,1)这个点3,它的右上方路径是存在的,但是左上方路径不存在。对于不存在的状态,我们需要将其初始化为负无穷大(因为这道题中,数字三角形中的值可能为负数)。这里的行列我们都从1开始,但是初始化的时候我们要从0开始,n结束(具体原因不再赘述)。
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 510;
const int INF = 0x3f3f3f3f;
//代表存储二维状态的数组
int f[N][N];
//代表存储数字三角形的数组
int a[N][N];
int 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]);
}
}
//进行初始化负无穷(从0开始,n结束)
for(int i=0;i<=n;i++){
for(int j=0;j<=n;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",res);
return 0;
}
3. 线性DP例题:最长上升子序列
https://www.acwing.com/problem/content/897/
上图是对本题样例的解释。
本题也是按照两种DP角度来求解:
1. 状态表示
本题的状态表示只需要1维即可。即,f[i]。那么该状态所表示的内容如下:
1.1 集合:所有以a[i](第i个数)结尾的上升子序列。
1.2 属性:以上集合中,所有子序列长度的最大值。
根据以上的状态,我们就可以求解本题的答案:将所有以1-n(第一个数与第n个数)结尾的状态进行遍历,求出最大值即可。
2. 状态计算
我们将f[i]划分为一个个的子集,划分的规则如下:
2.1 由于f[i]的集合中,每一个都包含a[i]。因此,我们从第i-1个数开始考虑。
2.2 首先,没有第i-1个数,那么这个序列中只有a[i]。
其次,第i-1个数是整个序列中的第1个数(a[1]),此时序列中后两个数依次为:a[1]a[i]。那么,我们要求所有上升子序列中包含a[1]a[i]这两个数的最大长度,实际上可以表示为:f[1] + 1。
其次,第i-1个数是整个序列中的第2个数(a[2])。此时序列中后两个数依次为:a[2]a[i]。那么,我们要求所有上升子序列中包含a[2]a[i]这两个数的最大长度,实际上可以表示为:f[2] + 1。
...
其次,第i-1个数是整个序列中的第i-1个数(a[i-1])。此时序列中后两个数依次为:a[i-1]a[i]。那么,我们要求所有上升子序列中包含a[i-1]a[i]这两个数的最大长度,实际上可以表示为:f[i-1] + 1。
我们可以用变量j来表示第i-1个数具体是哪个数。若如此做,j的范围:(0~i-1)
2.3 我们需要注意的是,上述的子集有可能不存在。比如,第i-1个数是序列中的第j个数。其中,j的范围:0~i-1。但是,如果a[j] > a[i] 的话就不构成上升子序列了。因此,如果某子集满足上述要求,那么不应该计算它。
根据上述内容,我们可以得出状态转移方程:
f[i] = max(a[j] + 1) 其中,j从0~i-1且a[j] < a[i]。
这道题的时间复杂度:由于i要遍历n个数,在遍历的过程中,还要将j遍历0~i-1次,因此时间复杂度为O(n²)。
如果这道题要输出最长上升子序列的话,我们可以开辟一个额外数组,来记录当前状态是由哪个状态转移过来的就行了。之后,倒序输出即可。
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 1010;
int n;
int a[N];
int 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; //序列里只有一个数的情况
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",res);
return 0;
}
// 输出最长上升子序列
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 1010;
int n;
int a[N];
int f[N],g[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; //序列里只有一个数的情况
g[i] = 0;
for(int j=1;j<i;j++){
//需要满足条件
if(a[j] < a[i]){
if(f[i] < f[j] + 1){
f[i] = f[j] + 1; //更新长度
g[i] = j; //记录状态
}
// f[i] = max(f[i],f[j] + 1);
}
}
}
int res = 0;
//确定倒序的入口k
int k;
for(int i=1;i<=n;i++){
if(res < f[i]){
res = f[i];
k = i;
}
}
//输出最长上升子序列,cnt为最长上升子序列的长度
int s[N],cnt = 0;
while(g[k]){
s[cnt++] = a[k];
k = g[k];
}
s[cnt++] = a[k];
for(int i=cnt-1;i>=0;i--){
printf("%d ",s[i]);
}
// printf("%d",res);
// printf("%d",cnt);
return 0;
}
4. 线性DP例题:最长公共子序列
https://www.acwing.com/activity/content/problem/content/1005/
上图是这个问题的简单举例。在这个案例中,最长公共子序列为:abd。
我们还是通过两种角度来阐述最长公共子序列问题:
1. 状态表示
由于题目中需要两个序列。因此,在这里我们用二维f[i,j]来表示状态。
1.1 集合
所有在第一个序列的前i个字母中出现,且在第二个序列的前j个字母中出现的子序列所构成的集合。
1.2 属性
集合中所有子序列长度的最大值。
2. 状态计算
对于状态f[i,j]我们如何来进行集合的划分呢?
我们可以以a[i],b[j]是否在子序列当中来将集合进行划分。我们可以得到四种情况(子集)。
2.1 子序列中既不含a[i]也不含b[j]。对于这种情况来讲,我们可以用状态f[i-1][j-1]来表示。
2.2 子序列中不含a[i],含b[j]。对于这种情况来讲,我们是不好进行表示的。但是,我们可以发现f[i-1][j]是包含这种情况的。然而,这道题的状态的属性是求最大值。因此,集合内部的元素(子序列)重复是没有问题的。因为,虽然用f[i-1][j]可能会导致元素的重复。但是,在求最值方面,元素的重复是不会造成影响的。
举个例子,假设班级中有一个人90分,另外一个人60分,那么最大值就是90分。即使又来了100个人还是60分,最大值仍是90分。再举一个例子,假设班级中有一个人60分,另外一个人30分,那么最大值就是60分。即使又来了100个人还是60分,最大值仍是60分。可见,元素的重复是不会影响最值的。上述的图中表明:当求ABC最大值的时候,即使B元素重复了2次,最值仍不会受到影响。
因此,我们在求解动态规划问题时,当确定集合的属性时,如果集合的属性是最值,那么在集合的划分中只需要保证元素不漏掉即可,重复是没有关系的。但是,如果集合的属性是个数,那么在集合的划分中既需要保证元素不漏掉,也要保证元素的不重复。(元素的重复性会对个数产生影响)
综上所述,我们可以用f[i-1][j]来替代这种情况。
2.3 子序列中含a[i],不含b[j]。根据上述的内容,我们可以用f[i][j-1]来替代这种情况。(具体细节跟上述内容几乎一致,这里不再赘述)
2.4 子序列中既含a[i],也含b[j]。对于这种情况来讲,由于这个集合中的每一个子序列中既包含a[i],也包含b[j],我们可以先去掉a[i]和b[j],那么集合的状态表示就是f[i-1][j-1]。之后我们将a[i]和b[j]这个元素加进去即可。综上所述,我们可以用f[i-1][j-1] + 1来进行表示(前提:a[i] == b[j])。如果a[i]!=b[j],那么这个子集就是空集。(在编程的时候,要避免这种情况发生)
根据上述内容,我们可以得出状态转移方程:
f[i][j] = Max(f[i-1][j-1],f[i-1][j],f[i][j-1],f[i-1][j-1] + 1);
由于元素的重复性不会影响最值问题。我们可以发现:f[i-1][j]和f[i][j-1]包含了f[i-1][j-1]这个集合。因此,上述的状态转移方程最终为:
f[i][j] = Max(f[i-1][j],f[i][j-1],f[i-1][j-1] + 1);
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 1010;
char a[N],b[N];
int f[N][N];
int n,m;
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",f[n][m]);
return 0;
}
作者:gao79138
链接:https://www.acwing.com/
来源:本博客中的截图、代码模板及题目地址均来自于Acwing。其余内容均为作者原创。
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具