ACwing算法备战蓝桥杯——Day27——线性DP

文章介绍了动态规划(DP)在解决路径问题和序列问题中的应用,如数字三角形的最大路径和、最长上升子序列、最长公共子序列等。通过代码示例展示了如何使用DP进行状态表示和优化,并强调了状态表示的重要性以及如何根据问题特性选择合适的维度。
摘要由CSDN通过智能技术生成

DP问题可以用闫氏DP分析法来做,这个方法的核心思想是用集合的思想来做。

这就是为什么DP会比暴力搜索快,因为DP每次处理的是一个集合,暴力搜索每次求的是一种情况;

其中状态表示也(就是集合表示)很难想出,只能靠做题积累题型来总结得出;

数字三角形:

路径问题的状态用坐标来表示状态,本题中是二维的,所以用f[i][j]来表示;

给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。

        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;

int f[N][N],w[N][N];

int main(){
    int n;
    cin>>n;

    //路径DP的状态表示用坐标表示
    for(int i=1;i<=n;i++){
        for(int j=1;j<=i;j++){
            scanf("%d",&w[i][j]);
        }
    }

    for(int j=1;j<=n;j++) f[n][j] = w[n][j];

    for(int i=n-1;i>=1;i--){
        for(int j=1;j<=i;j++){
            f[i][j]=max(f[i+1][j],f[i+1][j+1])+w[i][j];
        }
    }

    printf("%d\n",f[1][1]);

    return 0;
}

//代码等价优化版
#include <iostream>
#include <algorithm>
using namespace std;

const int N=510;

int f[N][N];

int main(){
    int n;
    cin>>n;
    
    //路径DP的状态表示用坐标表示
    for(int i=1;i<=n;i++){
        for(int j=1;j<=i;j++){
            scanf("%d",&f[i][j]);
        }
    }
    for(int i=n-1;i>=1;i--){
        for(int j=1;j<=i;j++){
            f[i][j]=max(f[i+1][j],f[i+1][j+1])+f[i][j];
        }
    }
    
    printf("%d\n",f[1][1]);
    
    return 0;
}

这题可以从上往下计算,也可以从下往上计算,选择从下往上计算可以减少边界条件的判定;

因为每个数都有两个更低层的数得来,但不是每个数都有两个更高层的数;


最长上升子序列:

判断状态表示用几维,在能表示答案的基础上维度越低越好,因为维度越高循环层数就越多;

给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。

输入格式
第一行包含整数 N。

第二行包含 N 个整数,表示完整序列。

输出格式
输出一个整数,表示最大长度。

数据范围
1≤N≤1000,
−109≤数列中的数≤109
输入样例:
7
3 1 2 1 8 5 6
输出样例:
4


#include <iostream>
#include <algorithm>

using namespace std;

const int N=1010;

int f[N],w[N];

int main(){
    //读入数据
    int n;
    cin>>n;
    for(int i=1;i<=n;i++) cin>>w[i];
    
    //初始为1
    for(int i=1;i<=n;i++) f[i]=1;
    
    //状态计算,对于每个状态i,要得到f[i],都可以从f[0]~f[i-1],i个状态得来.也就是得到状态i的最后一步操作
    for(int i=2;i<=n;i++){
        for(int j=1;j<=i;j++){
            if(w[i]>w[j]) 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;
}

最长公共子序列:

给定两个长度分别为 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>
#include <string>
using namespace std;

const int N=1010;

int f[N][N];

string a,b;

int main(){
    int n,m;
    
    cin>>n>>m;
    
    cin>>a>>b;
    
    //当然可以从字符数组[1]的位置开始存储字符串,从而避免初始化
    /*
    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;
    }
    */
    //初始化
    bool flage=false;
    for(int i=0;i<n;i++){
        if(flage){
            f[i][0]=1;
            continue;
        }
        if(a[i]==b[0]&&!flage){
            flage=true;
            f[i][0]=1;
        }
           
    }
    flage=false;
    for(int j=0;j<m;j++){
        if(flage){
            f[0][j]=1;
            continue;
        }
        if(a[0]==b[j]&&!flage){
            flage=true;
            f[0][j]=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-1][j-1]+1,f[i][j]);
        }
    }
    
    cout<<f[n-1][m-1]<<endl;
    
    return 0;
}

最大连续子序列:

给定 K 个整数的序列 {N0,N1,…,NK−1},其任意连续子序列可表示为 {Ni,Ni+1,…,Nj},其中 0≤i≤j<K。

最大连续子序列是所有连续子序列中元素和最大的一个,例如给定序列 {−2,11,−4,13,−5,−2},其最大连续子序列为 {11,−4,13} ,最大和为 20。

编写程序得到其中最大子序列的和并输出该子序列的第一个和最后一个元素的下标。

输入格式
输入包含多组测试数据。

每组数据占 2 行,第 1 行给出正整数 K。

第 2 行给出 K 个整数 N1,…,NK。

输出格式
每组数据输出一行结果,包含最大子序列的和以及子序列的第一个下标 i 和最后一个元素的下标 j。

所有元素下标为 0∼K−1。

如果最大子序列不唯一,则选择 i 最小的那个子序列,如果仍不唯一,则选择 i 最小的子序列中 j 最小的那个子序列。

若所有 K 个元素都是负数,则定义其最大和为 0,输出 0 0 0。

数据范围
1≤K≤105,
−10000≤Ni≤10000,
输入最多包含 10 组数据。

输入样例:
8
6 -2 11 -4 13 -5 -2 10
5
10 -10 10 -10 10
8
-1 -5 -2 3 -1 0 -2 0
4 
-1 -2 -4 -3
输出样例:
27 0 7
10 0 0
3 3 3
0 0 0

#include <iostream>
#include <algorithm>
using namespace std;
const int N=1e5+10;

int f[N],w[N],l[N],r[N];

int main(){
    int n;
    while(cin>>n){
        
        bool flag=false;//判断是否全为负数
        for(int i=1;i<=n;i++){
            scanf("%d",&w[i]);
            if(w[i]>=0) flag=true;
        }
        
        if(!flag){
            puts("0 0 0");
            continue;
        }
        
        for(int i=1;i<=n;i++) l[i]=r[i]=i;//初始化坐标
        
        f[0]=-10010;
        for(int i=1;i<=n;i++){
            
            f[i]=max(f[i-1],0)+w[i];
            
            if(!f[i-1]||f[i]!=w[i]){
                l[i]=l[i-1];
            }
        }
        
        int res=-1e9-10,loc=0;
        for(int i=1;i<=n;i++){
            if(f[i]>res){
                res=f[i];
                loc=i;
            }
        }
        
        printf("%d %d %d\n",f[loc],l[loc]-1,r[loc]-1);
    }
    
    return 0;
}

最大的和:

这题是在最大连续子序列的拓展版:

需要进行两次DP,分别对前缀和后缀DP。

对于给定的整数序列 A={a1,a2,…,an},找出两个不重合连续子段,使得两子段中所有数字的和最大。

我们如下定义函数 d(A):

d(A)=max1≤s1≤t1<s2≤t2≤n{∑i=s1t1ai+∑j=s2t2aj}
我们的目标就是求出 d(A)。

输入格式
第一行是一个整数 T,代表一共有多少组数据。

接下来是 T 组数据。

每组数据的第一行是一个整数,代表数据个数据 n,第二行是 n 个整数 a1,a2,…,an。

输出格式
每组数据输出一个整数,占一行,就是 d(A) 的值。

数据范围
1≤T≤30,
2≤n≤50000,
|ai|≤10000
输入样例:
1
10
1 -1 2 2 3 -3 4 -4 5 -5
输出样例:
13
样例解释
在样例中,我们取{2,2,3,-3,4}和{5}两个子段,即可得到答案。

#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>

using namespace std;

const int N=50010;

int f[N],rf[N],w[N],g[N],h[N],INT=1e9;

int main(){
    int t;
    scanf("%d",&t);
    
    while(t--){
        int n;
        scanf("%d",&n);
        
        for(int i=1;i<=n;i++) scanf("%d",&w[i]);
        
        f[0]=-INT;
        for(int i=1;i<=n;i++) f[i]=max(0,f[i-1])+w[i];//在1~i里以i结尾的最大不重复连续子序列    
        
        rf[n+1]=-INT;
        for(int i=n;i>=1;i--) rf[i]=max(0,rf[i+1])+w[i];//在i~n里以i结尾的最大不重复连续子序列
        
        g[0]=-INT;
        for(int i=1;i<=n;i++){//在区间1~i里,最大连续不重复子序列
            g[i]=max(g[i-1],f[i]);
        }
        
        h[n+1]=-INT;
        for(int i=n;i>=1;i--){//区间i~n里,最大连续不重复子序列
            h[i]=max(h[i+1],rf[i]);
        }
        
        int res=-INT;
        for(int i=1;i<=n-1;i++){
            res=max(g[i]+h[i+1],res);
        }
        printf("%d\n",res);
    }
    
    return 0;
}
当然,可以进行代码优化。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

切勿踌躇不前

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值