线性DP

原题目均来自:https://www.acwing.com/about/

数字三角形

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

        7
      3   8
    8   1   0
  2   7   4   4
4   5   2   6   5

输入格式
第一行包含整数n,表示数字三角形的层数。

接下来n行,每行包含若干整数,其中第 i 行表示数字三角形第 i 层包含的整数。

输出格式
输出一个整数,表示最大的路径数字和。

数据范围
1n500
1000010000
输入样例:

5
7
3 8
8 1 0 
2 7 4 4
4 5 2 6 5

输出样例:

30

思路:

个人感觉这应该是DP里边最简单的一道题了,这里表示的f[i][j]就表示以当前点为结尾的所有路径之和集合,而我们需要的集合属性是MAX,而f[i][j]是从它的左上角和右上角转移而来,即f[i1][j1], f[i1][j],状态转移方程:

f[i][j]=max(f[i1][j1]+1,f[i1][j]+1)

  • 不要忘了初始化
  • 最后对最后一行取max,得到需要的最大值

代码:

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 510, INF = 1e9;
int a[N][N];
int f[N][N];
int n;

int main()
{
    ios::sync_with_stdio(false), cin.tie(0);
    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 << endl;
    return 0;
}

最长上升子序列I

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

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

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

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

数据范围
1N1000
109109
输入样例:

7
3 1 2 1 8 5 6

输出样例:

4 

思路:

f[i]表示以当前数字结尾的所有子序列长度的集合,集合的属性就是MAX,怎么进行状态计算呢?
对于f[i],以最长子序列的倒数第二个数分类,而最长子序列的倒数第二个数的下标可以是从0i1中任何一个,用f[j]表示,因为在f[i]之前的f[j]都已经被同样的方式更新过了,所以对于每一个i我们从小到大枚举j,如果a[j]<a[i]那么f[i]就可以被f[j]更新,因为f[j]表示的是以下标j的数字结尾的最长上升子序列长度,所以状态转移方程:

f[i]=max(f[i],f[j]+1)

  • f[i]的初始值是,因为它最少就只有它自己一个数字
  • 最后再枚举一遍f[], 取max
    时间复杂度O(N2)

代码:

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;
int n, a[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;
        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;
}

如果想要记录出最长上升子序列是啥,就只需要记录出每一步转移是从什么时候转移的就可以了,最后倒序输出,有点类似与迷宫的BFS记录路径。

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;
int n, a[N], f[N], g[N]; //g数组保存每个转移是怎么做出来的

int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> 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; //记录一下当前i是由哪个j转移过来的
                }
            }
        }
    }
    int k = 1;
    for (int i = 1; i <= n; i++) {
        if (f[k] < f[i]) {
            k = i; //找到最长子序列的末尾
        }
    }
    //倒序输出
    for (int i = 0, len = f[k]; i < len; i++) { //f[k] 存的是最长子序列的长度
        cout << a[k] << endl;
        k = g[k]; //因为k记录了i的转移过程
    }
    return 0;
}

最长公共子序列

给定两个长度分别为NM的字符串AB,求既是A的子序列又是B的子序列的字符串长度最长是多少。

输入格式
第一行包含两个整数NM

第二行包含一个长度为N的字符串,表示字符串A

第三行包含一个长度为M的字符串,表示字符串B

字符串均由小写字母构成。

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

数据范围
1N,M1000
输入样例:

4 5
acbd
abedc

输出样例:

3

思路:

f[i][j]表示所有在第一个序列的前i个字母中出现,且在第二个序列中的前j个字母中出现的子序列,集合属性MAX,那怎么进行状态计算呢?
我们可以把集合分成四类:

  1. 不选a[i], b[j],即表示成状态就是f[i1][j1],即从第一个序列的前i1,第二个序列中前j1个字母出现的公共子序列。
  2. a[i], b[j],即f[i1][j1]+1,前提是当前a[i]=b[j],需要特殊判断

还剩下两种情况分别为:不选a[i], 选b[j],和,选a[i], 不选b[j],这两种情况怎么表示呢,它们等价于f[i1][j], f[i][j1]吗,看定义:

  1. f[i1][j]表示的是所有第一个序列的前i1个字母出现,且在第二个序列中的前j个字母中出现的子序列,所以包含了这里的不选a[i], 选b[j]的这种情况,但是好在因为是求最大值,所以我们完全可以用f[i1][j]来涵盖这种不选a[i], 选b[j]的情况,用10分别表示选和不选即:a[i]=0,b[j]=1f[i1][j], 那么整个max(a[i]=0,b[j]=1)max(f[i1][j])
  2. f[i][j1]表示与上同理。

要注意的是:
(f[i1][j1](f[i1][j1]+1)f[i1][j]f[i][j1])!=,即它们之间是有交集的,但是MAX只有一个,因此并不影响,还有就是既然有交集,那么可以看出,f[i1][j1]f[i1][j], 因此在写代码的时候完全可以把f[i1][j1]省略掉啦!

时间复杂度:O(N2)

代码:

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010;
char a[N], b[N];
int f[N][N]; //i表示从a串中前i个字母中选的集合, j表示从b串中前j个字母中选的集合
int n, m;

int main() {
    cin >> n >> m;
    cin >> a >> b;
    
    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]) { //特别判断a[i] == b[j]才会有f[i-1][j-1] + 1
                f[i][j] = max(f[i][j], f[i-1][j-1] + 1);
            }
        }
    }
    cout << f[n][m] << endl;
    return 0;
}
posted @   Xxaj5  阅读(77)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端
点击右上角即可分享
微信分享提示