动态规划-线性DP

动态规划-线性DP

1. 线性DP的定义

    所谓线性DP,实际上就是:这类问题的状态转移方程满足一定的线性关系。即,状态递推的顺序是线性的,我们把这类DP问题称为线性DP问题。

2. 线性DP例题:数字三角形

https://www.acwing.com/problem/content/900/

img

    我们首先给出一个例子,来帮助理解题意。上图中红色路径的值为:20。我们需要找出总和最大的路径。

img
img
img
img

    首先,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/

img

    上图是对本题样例的解释。

img
img

    本题也是按照两种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/

img

    上图是这个问题的简单举例。在这个案例中,最长公共子序列为:abd。

img
img
img

    我们还是通过两种角度来阐述最长公共子序列问题:
        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); 

img

    由于元素的重复性不会影响最值问题。我们可以发现: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。其余内容均为作者原创。
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
posted @   夏目^_^  阅读(153)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示