子数组问题——动态规划

个人主页:敲上瘾-CSDN博客

动态规划

目录

一、解题技巧

二、最大子数组和

三、乘积最大子数组

四、最长湍流子数组

五、单词拆分


一、解题技巧

区分子数组(子串)与子序列:

  • 子数组(子串):在数列中的一段连续的元素组成的新数列,中间不可间断。
  • 子序列:在数列中从左往右依次挑选出元素组成的新数列,或者说在数列中随意删除一些元素后,剩下的元素组成的新数列。

用动态规划做子数组类的题时,对于状态表示我们可以直接设:

  • dpi为以i元素结尾的子数组的... ...

后面就根据具体的题目要求填写,可能是子数组的和或者子数组的积等等,无论如何都可以以i元素结尾的子数组 ++为研究对象++去思考问题,如果解决不了就尝试增加状态,但++研究对象不要改变++。如果还解决不了那么再考虑改变或增加研究对象。

二、最大子数组和

状态表示

如上技巧所述,我们直接设状态转移方程:

  • dpi表示:以i位置结尾的子数组的最大和。

接下来只需要去尝试是否能写出正确的状态转移方程,如果能那么状态表示就是对的。

状态转移方程:

以i位置结尾的子数组我们可以分为两种:

  1. numsi单独构成一个子数组
  2. numsi以i-1结尾的最大和子数组组合成的子数组。

那么这个以i-1结尾的最大子数组的值就是一个重复子问题,我们假设在前面已经计算过了,即dpi-1,然后需要注意两种情况只能取一种。那么:

  • dpi = max(numsi+dpi-1,numsi)

初始化

初始化的目的主要有两个:

  • 保证填表的时候不越界。
  • 保证填表的正确性。

因为这里有i-1,所以如果从0开始填表可能会越界,通常有两种解决方案:

方法一:把dp0初始化(即dp0=nums0),然后从dp1位置开始填表(即从nums1位置开始记录)。

方法二:开辟一个n+1的空间(n=nums.size()),让dp0=0(需要根据具体情况具体分析),然后从dp1位置开始填写,而dp1记录的是nums0的情况,也就是错开一位进行记录,所以需要注意映射关系。

这题看似方法一更简洁,但对于其他题可能需要做更复杂的边界判断。所以在做动规题时更推荐使用方法二来解决边界问题。

填表顺序

从左往右。

返回值

dp表中的最大值。

代码示例:

cpp 复制代码
class Solution 
{
public:
    int maxSubArray(vector<int>& nums)
    {
        int n=nums.size(),ret=INT_MIN;
        vector<int> dp(n+1);
        for(int i=1;i<n+1;i++)
        {
            dp[i]=max(nums[i-1],nums[i-1]+dp[i-1]);
            ret=max(ret,dp[i]);
        }    
        return ret;
    }
};

三、乘积最大子数组

状态表示

同样的我们假设状态表示为:

dpi表示:以i位置结尾的子数组的最大乘积。

那么状态转移方程dpi=max(dpi-1*numsi,numsi),我们想一想这样对吗?比如dpi-1*numsi,numsi乘以一个最大积的子数组就是最大吗?

如果nums是一个负数不就变成最小乘积了吗,反之numsi小于0时乘以一个最小的数才能成为最大积。

所以当numsi小于0时我们需要知道以i-1结尾的子数组的最小积。

所以状态表示为:

  • fi表示:以i位置结尾的子数组的最大乘积。
  • gi表示:以i位置结尾的子数组的最小乘积。

状态转移方程

  • numsi>=0:
    • fi = max(fi-1*numsi,numsi)
    • gi = min(gi-1*numsi,numsi)
  • numsi < 0:
    • fi = max(gi-1*numsi,numsi)
    • gi = min(fi-1*numsi,numsi)

或:

  • fi=max(numsi,max(numsi*fi-1,numsi*gi-1));
  • gi=min(numsi,min(numsi*fi-1,numsi*gi-1));

初始化

与上一题相同,为防止越界我们给两个dp表都多开辟一个空间,映射关系错开一位。

然后把两个dp表都初始化为1,因为这里是乘法,如果使用默认的0值那么这个结果都是0。

注:dp0是我们为防止越界添加上的虚拟位置,它的值需要使得后面的填表正确。

填表顺序

从左往右,f表和g表一起填。

返回值

f表中的最大值。

代码示例:

cpp 复制代码
class Solution {
public:
    int maxProduct(vector<int>& nums)
    {
        int n=nums.size(), ret=INT_MIN;;
        vector<int> f(n+1,1),g(n+1,1);
        for(int i=1;i<=n;i++)
        {
            f[i]=max(nums[i-1],max(nums[i-1]*f[i-1],nums[i-1]*g[i-1]));
            g[i]=min(nums[i-1],min(nums[i-1]*f[i-1],nums[i-1]*g[i-1]));
            ret=max(ret,f[i]);
        }
        return ret;
    }
};

四、最长湍流子数组

题目的核心就一句话:比较符号在子数组中的每个相邻元素对之间翻转。

然后找到满足这样的条件的最长子数组。

状态表示

假设状态表示为:

dpi表示:以i结尾的最长湍流子数组。

我们把数据的大小波动抽象成一条折线,如下把示例1化为折线图:

结果取该段:

也就是子数组要满足前一个元素是上升趋势那么下一个元素必须是下降,如果前一个元素是下降趋势那么下一个元素必须是上升。

我们在做状态转移方程中主要是考虑两种情况,

  1. numsi单独构成一个子数组
  2. numsi接到前一个元素结尾构成的子数组中。

第2种情况又需要分情况讨论,

  • numsi < numsi-1:只有前面的子数组最终状态是呈现上升趋势时numsi才能接上。
  • numsi > numsi-1:只有前面的子数组最终状态是呈现下降趋势时numsi才能接上。
  • numsi==numsi-1:不能接入前面子数组。

所以我们需要把状态转移细分为两种状态:

  • fi表示:以i结尾并且最后一个元素呈上升趋势的最长湍流子数组的长度**。**
  • gi表示:以i结尾并且最后一个元素呈下降趋势的最长湍流子数组的长度**。**

状态转移方程

  • numsi < numsi-1
    • fi=1
    • gi=fi-1+1
  • numsi > numsi-1
    • fi=gi-1+1
    • gi=1
  • numsi==numsi-1
    • fi=1
    • gi=1

因为任意一个子数组,最小的长度都是1,所以可以把两个dp表都初始化为1,那么状态转移方程可简化为:

  • numsi < numsi-1:gi+=fi-1
  • numsi > numsi-1:fi+=gi-1

初始化

为防止越界我们给两个dp表都多开辟一个空间,映射关系错开一位。

然后把两个dp表都初始化为1。

填表顺序

从左往右,f表和g表同时填写。

返回值

f表和g表中的最大那个元素

代码示例:

cpp 复制代码
class Solution {
public:
    int maxTurbulenceSize(vector<int>& arr)
    {
        int n=arr.size(),ret=1;
        vector<int> f(n,1),g(n,1);
        for(int i=1;i<n;i++)
        {
            if(arr[i]<arr[i-1]) g[i]+=f[i-1];
            if(arr[i]>arr[i-1]) f[i]+=g[i-1];
            ret=max(ret,max(f[i],g[i]));
        }    
        return ret;
    }
};

五、单词拆分

状态表示

根据经验直接设状态表示:

  • dpi表示:从0到i位置结尾的字符串是否能被字典中的单词表示(bool类型)。

状态转移方程

因为在填写i时以前的每个子串是否能由字典表示已经知道,储存在dp表中。那么我们只需要找到任意一个j(0<=j<i)使得dpj=true,并且子字符串j+1,i能用字典表示,那么dpi=true,否则dpi=false。

所以状态转移方程:

  • dpi = (dpi-1&&si,i能用字典表示) || (dpi-2&&si-1,i能用字典表示) || ... ... ||(dp0&&s1,i能用字典表示)

注:si-1,i表示字符串中i-1到i这个子串。

初始化

为了让第一个字符元素也讨论进来,我们创建n+1的dp表,并把dp0初始化为true。

填表顺序

从左往右

返回值

return dpn

代码示例:

cpp 复制代码
class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict)
    {
        int n=s.size();
        unordered_set<string> st(wordDict.begin(),wordDict.end());
        vector<bool> dp(n+1);
        dp[0]=true;
        for(int i=1;i<=n;i++)
        {
            for(int j=0;j<i;j++)
            {
                if(dp[j]==false) continue;
                if(st.count(string(s.begin()+j,s.begin()+i)))
                {
                    dp[i]=true;
                    break;
                }
            }
        }   
        return dp[n];
    }
};

好题推荐:

非常感谢您能耐心读完这篇文章。倘若您从中有所收获,还望多多支持呀!

相关推荐
To_OC5 小时前
LC 49 字母异位词分组:想到哈希表很简单,选对 key 才是精髓
javascript·算法·leetcode
小bo波9 小时前
使用Thread子类创建线程 VS 使用Runnable接口创建线程的区别
java·多线程·thread·并发编程·runnable
SamDeepThinking10 小时前
高并发场景下,CompletableFuture与ForkJoinPool该如何取舍?
java·后端·面试
用户9385156350710 小时前
从 O(n²) 到 O(nlogn):一文读懂快速排序的“快”与“妙”
javascript·算法
To_OC11 小时前
手写快排次次翻车?别死背快排模板了,这才是面试官想听的底层逻辑
javascript·算法·排序算法
饼干哥哥12 小时前
Reddit VOC调研太慢?搭一个AI专家团队半小时洞察任何品类|以猫用饮水机为例
人工智能·算法·ai编程
张不才13 小时前
CPU 100% 了怎么办?Java 性能排障的标准化操作
java·后端
地平线开发者13 小时前
Transformer模型部署之性能优化指南
算法
地平线开发者14 小时前
人在途中:从“编译失败”到“模型可落地”——CUDA 自定义算子
算法·自动驾驶
shepherd11114 小时前
吞吐量提升 10 倍:高并发大批量数据处理任务的架构演进与性能调优
java·后端·架构