C语言深入探究动态规划之线性DP

 更新时间:2022年04月12日 14:21:55   作者:小羊努力变强  
线性动态规划,是较常见的一类动态规划问题,其是在线性结构上进行状态转移,这类问题不像背包问题、区间DP等有固定的模板,线性动态规划的目标函数为特定变量的线性函数,约束是这些变量的线性不等式或等式,目的是求目标函数的最大值或最小值

写在前面

之前讲过背包问题,不知道大家忘了吗,如果忘了可以点这里,这次是线性DP

数字三角形

在这里插入图片描述

状态表示:f[i,j],到点i,j的最大路径

状态计算:f[i,j] = MAX(f[i-1,j-1]+a[i,j],f[i-1,j]+a[i,j])

在这里插入图片描述

看图,以例题为例,将它看成五行五列的三角形,每个点都可以用坐标表示。那么我们可以得知到一个数的最大路径要么来自左上,要么来自右上。左上的数用f[i-1,j-1]表示,右上的数f[i-1,j]表示,因此我们就有了状态转移公式:

f[i,j] = MAX(f[i-1,j-1]+a[i,j],f[i-1,j]+a[i,j])

所以就有了最终的代码:

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 510, INF = 1e9;

int n;
int a[N][N];
int f[N][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]);

    for (int i = 0; i <= n; i ++ )
        for (int j = 0; j <= i + 1; j ++ )//注意这里j从0到i+1,因为对于边界点,它的上一层只有一条路径通向它
            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\n", res);
    return 0;
}

最长上升子序列

在这里插入图片描述

状态表示:f[i]表示从第一个数字开始算,以w[i]结尾的最大的上升序列。(以w[i]结尾的所有上升序列中属性为最大值的那一个)

状态计算(集合划分):j∈(0,1,2,…,i-1), 在w[i] > w[j]时,

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

有一个边界,若前面没有比i小的,f[i]为1(自己为结尾)。

最后在找f[i]的最大值。

时间复杂度

O(n2) 状态数(n) * 转移数(n)

在这里插入图片描述

看图, 首先 f[i]f[i] 的含义是以 w[i]结尾的最长上升子序列的长度

初始值 f[i]=1,i∈[0,n−1],表示自己就是最长上升子序列,长度为 1

接下来考虑状态转移,把前 i−1个数字中所有满足条件 w[j]<w[i](因为要求是上升子序列) 的 j 找出来,那么 f[i] 就可以试着更新为以 w[j] 结尾的最长上升子序列的长度 再加上 自己的长度 1,但可能更新完的结果没有之前更新过的 f[i] 大,最后两者取一个 max,所以状态转移方程就是 f[i]=max(f[i],f[j]+1)

#include <iostream>

using namespace std;

const int N = 1010;

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

int main() {
    cin >> n;
    for (int i = 0; i < n; i++) cin >> w[i];

    int mx = 1;    // 找出所计算的f[i]之中的最大值,边算边找
    for (int i = 0; i < n; i++) {
        f[i] = 1;    // 设f[i]默认为1,找不到前面数字小于自己的时候就为1
        for (int j = 0; j < i; j++) {
            if (w[i] > w[j]) f[i] = max(f[i], f[j] + 1);    // 前一个小于自己的数结尾的最大上升子序列加上自己,即+1
        }
        mx = max(mx, f[i]);
    }

    cout << mx << endl;
    return 0;
}

最长上升子序列 II

在这里插入图片描述

会发现II的数据范围变了,那我们就得做优化,怎么优化呢?

状态表示:f[i]表示长度为i的最长上升子序列,末尾最小的数字。(长度为i的最长上升子序列所有结尾中,结尾最小min的) 即长度为i的子序列末尾最小元素是什么。

状态计算:对于每一个w[i], 如果大于f[cnt-1] (下标从0开始,cnt长度的最长上升子序列,末尾最小的数字),那就cnt+1,使得最长上升序列长度+1,当前末尾最小元素为w[i]。 若w[i]小于等于f[cnt-1],说明不会更新当前的长度,但之前末尾的最小元素要发生变化,找到第一个 大于或等于 (这里不能是大于) w[i],更新以那时候末尾的最小元素。

f[i]一定以一个单调递增的数组,所以可以用二分法来找第一个大于或等于w[i]的数字。

时间复杂度

O(nlogn)状态数(n) * 转移数(logn)

#include <iostream>

using namespace std;

const int N = 1010;
int n, cnt;
int w[N], f[N];

int main() {
    cin >> n;
    for (int i = 0 ; i < n; i++) cin >> w[i];

    f[cnt++] = w[0];
    for (int i = 1; i < n; i++) {
        if (w[i] > f[cnt-1]) f[cnt++] = w[i];
        else {
            int l = 0, r = cnt - 1;
            while (l < r) {
                int mid = (l + r) >> 1;
                if (f[mid] >= w[i]) r = mid;
                else l = mid + 1;
            }
            f[r] = w[i];
        }
    }
    cout << cnt << endl;
    return 0;
}

最长公共子序列

在这里插入图片描述

在这里插入图片描述

集合表示:f[i][j]表示a的前i个字母,和b的前j个字母的最长公共子序列长度

集合划分:以a[i],b[j]是否包含在子序列当中为依据,因此可以分成四类:

  • ①a[i]不在,b[j]不在

max=f[i−1][j−1]

  • ②a[i]a[i]不在,b[j]b[j]在

看似是max=f[i−1][j] , 实际上无法用f[i−1][j]表示,因为f[i−1][j]表示的是在a的前i-1个字母中出现,并且在b的前j个字母中出现,此时b[j]不一定出现,这与条件不完全相等,条件给定是a[i]一定不在子序列中,b[j]一定在子序列当中,但仍可以用f[i−1][j]来表示,原因就在于条件给定的情况被包含在f[i−1][j]中,即条件的情况是f[i−1][j]的子集,而求的是max,所以对结果不影响。

例如:要求a,b,c的最大值可以这样求:max(max(a,b),max(b,c))虽然b被重复使用,但仍能求出max,求max只要保证不漏即可。

  • ③a[i],b[j]不在 原理同②
  • ④a[i]在,b[j]在 max=f[i−1][j−1]+1

实际上,在计算时,①包含在②和③的情况中,所以①不用考虑

#include <iostream>

using namespace std;

const int N = 1010;

int n , m;
char a[N] , b[N];
int f[N][N];
int main()
{
    cin >> n >> m;
    cin >> 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]);//2和3的情况一定存在,所以可以无条件优先判断
                if(a[i] == b[j]) f[i][j] = max(f[i][j] , f[i - 1][j - 1] + 1);
            }                                                       

    cout << f[n][m] << endl;
    return 0;
}

到此这篇关于C语言深入探究动态规划之线性DP的文章就介绍到这了,更多相关C语言 线性DP内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 深入理解goto语句的替代实现方式分析

    深入理解goto语句的替代实现方式分析

    本篇文章是对goto语句的替代实现方式进行了详细的分析介绍,需要的朋友参考下
    2013-05-05
  • C语言自动生成enum值和名字映射代码

    C语言自动生成enum值和名字映射代码

    这篇文章主要介绍了C语言自动生成enum值和名字映射代码的相关资料,需要的朋友可以参考下
    2015-12-12
  • 详解C++中常用的四种类型转换方式

    详解C++中常用的四种类型转换方式

    这篇文章主要为大家详细介绍了C++中常用的四种类型转换方式:static_cast<Type>、dynamic_cast<Type>、const_case<Type>和reinterpret_cast,感兴趣的可以了解一下
    2022-08-08
  • C语言字符串的模式匹配之BF与KMP

    C语言字符串的模式匹配之BF与KMP

    这篇文章记录一下串里面的模式匹配,模式匹配,顾名思义就是给定一个被匹配的字符串,然后用一个字符串模式(模型)去匹配上面说的字符串,看后者是否在前者里面出现。常用的有2种算法可以实现,下面我们来具体探讨下
    2021-09-09
  • C语言实现数独游戏的求解

    C语言实现数独游戏的求解

    这篇文章主要为大家详细介绍了C语言实现数独游戏的求解,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-01-01
  • C++ 命名空间--namespace总结

    C++ 命名空间--namespace总结

    namespace中文意思是命名空间或者叫名字空间,下面这篇文章主要给大家介绍了关于C++中名称空间namespace使用的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起看看吧
    2021-09-09
  • C++实现猜数游戏

    C++实现猜数游戏

    这篇文章主要为大家详细介绍了C++实现猜数游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-05-05
  • C++派生访问说明符小记(推荐)

    C++派生访问说明符小记(推荐)

    下面小编就为大家带来一篇C++派生访问说明符小记(推荐)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-01-01
  • C++实现“隐藏实现,开放接口”的方案

    C++实现“隐藏实现,开放接口”的方案

    本文从一个实例讲解了C++实现“隐藏实现,开放接口”的方案,文章条理清新,内容充实,需要的朋友可以参考下
    2015-07-07
  • 使用C++实现Excel文件与CSV之间的相互转换

    使用C++实现Excel文件与CSV之间的相互转换

    这篇文章主要为大家详细介绍了如何使用C++实现Excel文件与CSV之间的相互转换,文中的示例代码讲解详细,感兴趣的小伙伴可以了解一下
    2023-06-06

最新评论