动规训练之子数组和问题专题

本文介绍了使用动态规划解决最大子数组和问题,包括最大子数组和、环形子数组的最大和以及乘积为正数的最长子数组长度的算法原理、状态表示方程、状态转移方程、初始化步骤和代码实现。
摘要由CSDN通过智能技术生成

目录

一、最大子数组和

1、题目解析

2、算法原理

a状态表示方程

b状态转移方程

c初始化

d填表顺序

e返回值

3、代码编写

二、环形子数组的最大和

1、题目描述

​编辑

2、算法原理

a状态表示方程

b状态转移方程

c初始化

d填表顺序

e返回值

3、代码编写

三、乘积为正数的最长子数组长度

1、题目描述

2、算法原理

a状态表示方程

b状态转移方程

​编辑

​编辑

c初始化

d填表顺序

e返回值

3、代码编写


一、最大子数组和

1、题目解析

题目的意思就是在一个数组里找一个和为最大值的子数组,这个子数组第一个和最后一个元素的位置不确定,但是只需要返回最大值即可

2、算法原理

a状态表示方程

找到状态表达方程的小技巧:经验+题目要求

比如:dp[i]表示+巴拉巴拉

比如本题就是:dp[i]表示以i节点为结尾时可以得到的最大子数组和

b状态转移方程

想要找到状态转移方程,也有一个小技巧:根据最近的一步划分问题

就比如这个图中,i节点表示以i节点为结尾时最大的子数组和,i-1则表示以i-1节点为结尾时最大的子数组和。

那么我们就需要考虑dp[i]如何通过dp[i-1]得到,有两种情况:

dp[i]=max(dp[i-1]+nums[i],nums[i])

有些同学会有点疑惑,为什么dp[i-1]+nums[i]小于nums[i]的时候dp[i]的值是nums[i],因为我们的定义就是,以i为尾节点的时候,所以这个值无论如何都是要包括i节点的

c初始化

由于本题中出现了i-1,所以我们初始化需要考虑到 第一个节点时的边界问题

这道题我们通过设置一个节点在前面,为了不影响dp[1]的值,将其值设置为0,

这样我们的程序从1开始进入循环就不会出现越界问题了。

这种方法我称之为虚拟节点的初始化,不过这种方法有两点需要注意的点

1、给虚拟节点设置的初始值需要考虑到后续填表时取值的正确性

2、需要注意下标映射,因为我们的dp表比nums多了一个虚拟节点,我们对nums取值时需要下标-1

d填表顺序

从左到右

e返回值

在dp表中取出一个最大值

3、代码编写

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        //建表
        //初始化
        //填表
        //返回值
        int n=nums.size();
        vector<int> dp(n+1,0);
        for(int i=1;i<=n;i++){
            dp[i]=max(dp[i-1]+nums[i-1],nums[i-1]);
        }
        int Max=INT_MIN;
        for(int i=1;i<=n;i++){
            Max=max(dp[i],Max);
        }
        return Max;
    }
};

我记得之前在 leetcode 的题解上,看到⼀句⾮常骚的解释:
⾛完这⼀⽣:如果我和你在⼀起会变得更好,那我们就在⼀起,否则我就丢下你。我回顾我最光辉的时刻就是和不同⼈在⼀起,变得更好的最⻓连续时刻。

二、环形子数组的最大和

1、题目描述

2、算法原理

a状态表示方程

求子数组和的时候,会有两种情况:

1、子数组为中间的数组,这就和第一道一样的方法处理

2、子数组在头部和尾部

求出这种情况的最大子数组和就需要将整个数组的和sum减去中间子数组最小和即可,也就是sum-min

这样就将问题转换成我们熟悉的问题了。

状态表示方程:

f[i]表示以i节点为结尾时,最大子数组和

g[i]表示以i节点为结尾时,最小子数组和

b状态转移方程

根据最近的一步划分问题。

c初始化

d填表顺序

从左到右

e返回值

只需要将f的最大值和sum-g的最小值相比即可。

但是有一个特殊情况需要考虑,如果数组里面全是负数,我们只需要将最大的负数返回即可可。

3、代码编写

class Solution {
public:
    int maxSubarraySumCircular(vector<int>& nums) {
        int n=nums.size();
        vector<int> f(n+1,0);
        vector<int> g(n+1,0);
        int sum=0;
        for(int i=0;i<n;i++){
            sum+=nums[i];
        }
        for(int i=1;i<=n;i++){
            f[i]=max(f[i-1]+nums[i-1],nums[i-1]);
            g[i]=min(g[i-1]+nums[i-1],nums[i-1]);
        }
        int Min=INT_MAX;
        int Max=INT_MIN;
        for(int i=1;i<=n;i++)
        {
            Min=min(g[i],Min);
            Max=max(f[i],Max);
        }
        return sum==Min?Max:max(sum-Min,Max);
    }
};

三、乘积为正数的最长子数组长度

1、题目描述

2、算法原理

a状态表示方程

小技巧:经验+题目要求

状态表⽰: f [i] 表⽰「所有以 i 结尾的⼦数组,乘积为正数的最⻓⼦数组的⻓度」。
思考状态转移:对于 i 位置上的 nums[i] ,我们可以分三种情况讨论:
i. 如果 nums[i] = 0 ,那么所有以 i 为结尾的⼦数组的乘积都不可能是正数,此时dp[i] = 0 ;
ii. 如果 nums[i] > 0 ,那么直接找到 f [i - 1] 的值(这⾥请再读⼀遍 f [i - 1] 代表的意义,并且考虑如果 f [i - 1] 的结值是 0 的话,影不影响结果),然后加
⼀即可,此时 f [i] = f[i - 1] + 1
iii. 如果 nums[i] < 0 ,这时候你该蛋疼了,因为在现有的条件下,你根本没办法得到此时的最⻓⻓度。因为乘法是存在「负负得正」的,单单靠⼀个 f[i - 1] ,我们⽆法推导出 f[i] 的值。
但是,如果我们知道以 i - 1 为结尾的所有⼦数组,乘积为负数的最⻓⼦数组的⻓度g[i - 1] ,那么此时的 f [i] 是不是就等于 g[i - 1] + 1 呢?
通过上⾯的分析,我们可以得出,需要两个 dp 表,才能推导出最终的结果。不仅需要⼀个乘积 为正数的最⻓⼦数组,还需要⼀个乘积为负数的最⻓⼦数组。

b状态转移方程

遍历每⼀个位置的时候,我们要同步更新两个 dp 数组的值。
对于 f[i] ,也就是以 i 为结尾的乘积为「正数」的最⻓⼦数组,根据 nums[i] 的值,可以分为三种情况:
i. nums[i] = 0 时,所有以 i 为结尾的⼦数组的乘积都不可能是正数,此时 f[i] =0 ;
ii. nums[i] > 0 时,那么直接找到 f[i - 1] 的值(这⾥请再读⼀遍 f[i - 1] 代表的意义,并且考虑如果 f[i - 1] 的结值是 0 的话,影不影响结果),然后加⼀即可,此时 f[i] = f[i - 1] + 1
iii. nums[i] < 0 时,此时我们要看 g[i - 1] 的值(这⾥请再读⼀遍 g[i - 1] 代表的意义。因为负负得正,如果我们知道以 i - 1 为结尾的乘积为负数的最⻓⼦数组的⻓度,加上 1 即可),根据 g[i - 1] 的值,⼜要分两种情况:
1. g[i - 1] = 0 ,说明以 i - 1 为结尾的乘积为负数的最⻓⼦数组是不存在的,⼜因为 nums[i] < 0 ,所以以 i 结尾的乘积为正数的最⻓⼦数组也是不存在的,此时 f[i] = 0
2. g[i - 1] != 0 ,说明以 i - 1 为结尾的乘积为负数的最⻓⼦数组是存在的,⼜因为 nums[i] < 0 ,所以以 i 结尾的乘积为正数的最⻓⼦数组就等于 g[i - 1] + 1 ;
综上所述, nums[i] < 0 时, f[i] = g[i - 1] == 0 ? 0 : g[i - 1] +
1;
对于 g[i] ,也就是以 i 为结尾的乘积为「负数」的最⻓⼦数组,根据 nums[i] 的值,可以分为三种情况:
i. nums[i] = 0 时,所有以 i 为结尾的⼦数组的乘积都不可能是负数,此时 g[i] = 0 ;
ii. nums[i] < 0 时,那么直接找到 f[i - 1] 的值(这⾥请再读⼀遍 f[i - 1] 代表的意义,并且考虑如果 f[i - 1] 的结值是 0 的话,影不影响结果),然后加⼀即可 (因为正数 * 负数 = 负数),此时 g[i] = f[i - 1] + 1
iii. nums[i] > 0 时,此时我们要看 g[i - 1] 的值(这⾥请再读⼀遍 g[i - 1] 代表的意义。因为正数 * 负数 = 负数),根据 g[i - 1] 的值,⼜要分两种情况:
1. g[i - 1] = 0 ,说明以 i - 1 为结尾的乘积为负数的最⻓⼦数组是不存在的,⼜因为 nums[i] > 0 ,所以以 i 结尾的乘积为负数的最⻓⼦数组也是不存在的,此
f[i] = 0
2. g[i - 1] != 0 ,说明以 i - 1 为结尾的乘积为负数的最⻓⼦数组是存在的,⼜因为 nums[i] > 0 ,所以以 i 结尾的乘积为正数的最⻓⼦数组就等于 g[i -1] + 1 ;
综上所述, nums[i] > 0 时, g[i] = g[i - 1] == 0 ? 0 : g[i - 1] +
1 ;
这⾥的推导⽐较绕,因为不断的出现「正数和负数」的分情况讨论,我们只需根据下⾯的规则,严
格找到此状态下需要的 dp 数组即可:
i. 正数 * 正数 = 正数
ii. 负数 * 负数 = 正数
iii. 负数 * 正数 = 正数 * 负数 = 负数

c初始化

为了防止i-1时下标越界:

我们通过虚拟节点进行初始化,给第一个节点设置的值为0 即可。

d填表顺序

从左到右,两个表一起填写

e返回值

返回f中最大值即可

3、代码编写

class Solution {
public:
    int getMaxLen(vector<int>& nums) {
        int n =nums.size();
        vector<int> f(n+1,0);
        vector<int> g(n+1,0);//以i为尾相乘为负数的最长位数
        for(int i=1;i<=n;i++){
            if(nums[i-1]>0){
                f[i]=f[i-1]+1;
                g[i]=g[i-1]==0?0:g[i-1]+1;
            }else if(nums[i-1]<0){
                f[i]=g[i-1]==0?0:g[i-1]+1;
                g[i]=f[i-1]+1;
            }
            else{
                f[i]=0;
                g[i]=0;
            }
        }
        int Max=INT_MIN;
        for(int i=1;i<=n;i++){
            Max=max(f[i],Max);
        }
        return Max;
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

一周学八天

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

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

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

打赏作者

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

抵扣说明:

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

余额充值