动态规划之子数组系列问题

news2024/9/22 19:45:33

题型介绍

子数组系列动态规划问题长什么样

例题

力扣 53. 最大子数组和

解题步骤:

创建 dp 表以及确定 dp 表中所要填写位置的含义:

首先,根据写题经验,先确定出这道题应该使用的解题思路是 “以某一个位置为结尾进行分析”。

在这道题中,规定 dp[i] 表示以 i 位置为结尾的子数组中的最大和。

确定状态转移方程:

以 i 位置为结尾的子数组有两种情况。

① 子数组中只有自己,这时 dp[i] = nums[i]。

② 和前面的其他元素组成一个子数组,则可以理解为 以 i - 1 为结尾的最大子数组和 + nums[i] 本身。

所以,应该是上面两种情况的最大值。dp[i] = max(dp[i - 1] + nums[i], nums[i])。

初始化:

由于状态转移方程中,只涉及到 dp 表中的前一个位置,所以只需要将 dp[0] = nums[0],后续填表从 dp[1] 开始即可。

确定填表顺序:

根据状态转移方程可以看出应该从左向右依次填表。

确定返回值:

题目要求返回所有子数组中的最大和,所有的子数组可能以任何一个位置为结尾。

所以,可以在填表之后遍历 dp 表找出最大值,也可以在填表的过程中更新最大值,时间复杂度都是 O(N)。

代码:

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

练习题

力扣 918. 环形子数组的最大和

力扣 918. 环形子数组的最大和

重点:环形子数组,跟例题的区别就是,这个数组是环形的。

环形意味着,一个子数组可以在数组中间,也可以由数组的开头和结尾组成。

在中间的情况就和例题一模一样,在两边的可以换一种思路。

解题步骤:

创建 dp 表以及确定 dp 表中所要填写位置的含义:

首先,根据写题经验,先确定出这道题应该使用的解题思路是 “以某一个位置为结尾进行分析”。

在这道题中,规定 Max[i] 表示以 i 位置为结尾的所有子数组中的最大和。

规定 Min[i] 表示以 i 位置为结尾的所有子数组中的最小和。

确定状态转移方程:

以 i 位置为结尾的子数组有两种情况。

① 子数组中只有自己,这时 dp[i] = nums[i]。

② 和前面的其他元素组成一个子数组,则可以理解为 以 i - 1 为结尾的最大子数组和 + nums[i] 本身。

所以,应该是上面两种情况的最大值或最小值。

Max[i] = max(Max[i - 1] + nums[i], nums[i])。Min[i] = min(Min[i - 1] + nums[i], nums[i])。

初始化:

由于状态转移方程中,只涉及到 dp 表中的前一个位置,所以只需要将 Max[0] = Min[0] = nums[0],将 Max 表中的其他值初始化为 INT_MIN,将 Min 表中的其他值初始化为 INT_MAX,后续填表从 dp[1] 开始即可。

确定填表顺序:

根据状态转移方程可以看出应该从左向右依次填表。

确定返回值:

题目要求返回所有子数组中的最大和,所有的子数组可能以任何一个位置为结尾。

所以,应该找出 Max 中的最大值和 Min 中的最小值。然后返回 Max 中的最大值以及原数组和减去 Min 中的最小值中更大的那一个。

这里注意,如果给出的数组中全是负数,则数组总和会等于 Min 中的最小值,这时应该返回的是原数组中最小的那个元素,也就是 Max 中的最大值。

代码:

class Solution 
{
public:
    int maxSubarraySumCircular(vector<int>& nums) 
    {
        int n = nums.size(), sum = 0, big = nums[0], litter = nums[0];
        for (int num : nums) sum += num;
        // 创建 dp 表
        vector<int> Max(n, INT_MIN), Min(n, INT_MAX);
        // 初始化
        Max[0] = Min[0] = nums[0];
        // 填表
        for (int i = 1; i < n; i++)
        {
            Max[i] = max(Max[i - 1] + nums[i], nums[i]);
            Min[i] = min(Min[i - 1] + nums[i], nums[i]);
            big = max(big, Max[i]);
            litter = min(litter, Min[i]);
        }
        return sum == litter ? big : max(big, sum - litter);
    }
};

力扣 152. 乘积最大子数组

力扣 152. 乘积最大子数组

这道题跟上面那道题有个不同之处,这道题用乘法,上面那道题用加法。

乘法就涉及到某一个位置的元素是正数、负数还是 0 的情况,三种情况得到的结果是不一样的。

如果某个位置的元素为 0,不管子数组中只有自己还是包括其他元素,乘积都是 0。

如果某个位置的元素为正数,那么它乘以一个负数就会变小,乘以一个正数就会变大。

如果某个位置的元素为负数,那么它乘以一个正数就会变小,乘以一个负数就会变大。

所以,针对一个正数,当子数组中还包含其他数的时候,应该用它乘以以前一个位置为结尾的所有子数组中乘积的最大值。

针对一个负数,当子数组中还包含其他数的时候,应该用它乘以以前一个位置为结尾的所有子数组中乘积的最小值。

所以需要两个 dp 表,因为一个 dp 表不足以表示出所有的状态。

big[i] 表示以 i 位置为结尾(包含 nums[i])的所有乘积为正数的子数组中的最大乘积。

litter[i] 表示以 i 位置为结尾(包含 nums[i])的所有乘积为负数的子数组中的最小乘积。

当 nums[i] > 0 时,乘以一个最大的正数得到的就是以 i 位置为结尾的子数组中的最大乘积,或者子数组中只有自己。

当 nums[i] < 0 时,乘以一个最小的负数得到的就是以 i 位置为结尾的子数组中的最大乘积,或者子数组中只有自己。

当 nums[i] > 0 时,乘以一个最小的负数得到的就是以 i 位置为结尾的子数组中的最小乘积,或者子数组中只有自己。

当 nums[i] < 0 时,乘以一个最大的正数得到的就是以 i 位置为结尾的子数组中的最小乘积,或者子数组中只有自己。

以上情况在 nums[i] = 0 的时候也是成立的,所以不用单独拿出来讨论。

代码:

class Solution 
{
public:
    int maxProduct(vector<int>& nums) 
    {
        // 创建 dp 表 + 初始化
        int n = nums.size();
        vector<long long> big(n, INT_MIN), litter(n, INT_MAX);
        big[0] = litter[0] = nums[0];
        // 填表
        for (int i = 1; i < n; i++)
        {
            big[i] = max(nums[i], max(nums[i] * big[i - 1], nums[i] * litter[i - 1]));
            litter[i] = min(nums[i], min(nums[i] * litter[i - 1], nums[i] * big[i - 1]));
        }
        return *max_element(big.begin(), big.end());
    }
};

力扣 1567. 乘积为正数的最长子数组长度

力扣 1567. 乘积为正数的最长子数组长度

这道题也需要分为乘积为正数和负数两个 dp 表。

f[i] 表示以 i 位置为结尾的,乘积为正数的,最长子数组的长度。

g[i] 表示以 i 位置为结尾的,乘积为负数的,最长子数组的长度。

所以,当 nums[i] > 0 时,f[i] = f[i - 1] + 1。因为如果前一个也是正数,则再多一个正数,长度就 +1。

g[i] = g[i - 1] == 0 ? 0 : g[i - 1] + 1。因为如果前一个为负数,则正数乘以负数还是负数,所以 +1,但要注意以前一个位置为结尾的子数组中是否有乘积为负数的子数组,所以加了一个判断。

当 nums[i] < 0 时,f[i] = g[i - 1] == 0 ? 0 : g[i - 1] + 1。因为负数只有乘以负数才是正数,同样也需要注意以前一个位置为结尾的子数组中是否有乘积为负数的子数组。

g[i] = f[i - 1] + 1。因为负数乘以正数还是负数,因为 dp 表初始化为 0,所以不用判断。

代码:

class Solution 
{
public:
    int getMaxLen(vector<int>& nums) 
    {
        // 创建 dp 表
        int n = nums.size();
        vector<int> f(n), g(n);
        // 初始化
        if (nums[0] > 0) f[0] = 1;
        else if (nums[0] < 0) g[0] = 1;
        // 填表
        for (int i = 1; i < n; i++)
        {
            if (nums[i] > 0)
            {
                f[i] = f[i - 1] + 1;
                g[i] = g[i - 1] == 0 ? 0 : g[i - 1] + 1;
            }
            else if (nums[i] < 0)
            {
                f[i] = g[i - 1] == 0 ? 0 : g[i - 1] + 1;
                g[i] = f[i - 1] + 1;
            }
        }
        return *max_element(f.begin(), f.end());
    }
};

力扣 413. 等差数列划分

力扣 413. 等差数列划分

这道题算是比较简单的,只需要从前往后判断,相邻的三个元素能不能构成等差数组即可。

但是返回值应该是,以每一个位置为结尾的所有等差子数组的总数的和。

代码:

class Solution 
{
public:
    int numberOfArithmeticSlices(vector<int>& nums) 
    {
        int n = nums.size(), ret = 0;
        vector<int> dp(n);
        for (int i = 2; i < n; i++)
        {
            dp[i] = nums[i] - nums[i - 1] == nums[i - 1] - nums[i - 2] ? dp[i - 1] + 1 : 0;
            ret += dp[i];
        }
        return ret;
    }
};

力扣 978. 最长湍流子数组

力扣 978. 最长湍流子数组

这道题其实是跟上一道题一样的。

只是需要注意,每一个单个的元素都是一个长度为一的湍流子数组。

任意两个相邻的且不相等的元素,都可以构成一个长度为二的湍流子数组。

如果相邻的三个元素可以构成湍流子数组,则只需要考虑前一个位置湍流子数组中元素的个数,然后 +1 即可。

代码:

class Solution 
{
public:
    int maxTurbulenceSize(vector<int>& arr) 
    {
        int n = arr.size();
        if (n == 1) return 1;
        vector<int> dp(n);
        dp[0] = 1;
        dp[1] = arr[1] == arr[0] ? 1 : 2;
        int ret = dp[1];
        for (int i = 2; i < n; i++)
        {
            if (arr[i] == arr[i - 1]) dp[i] = 1;
            else if ((arr[i] > arr[i  -1] && arr[i - 1] < arr[i - 2]) ||
                     (arr[i] < arr[i - 1] && arr[i - 1] > arr[i - 2]))
                dp[i] = dp[i - 1] + 1;
            else dp[i] = 2;
            ret = max(ret, dp[i]);
        }
        return ret;
    }
};

力扣 139. 单词拆分

力扣 139. 单词拆分

这道题仍然可以用 dp[i] 来表示字符串 s 中,以 i 位置为结尾的字符串,是否满足可以由 wordDict 中的单词组成的条件,即 dp[i] 中是一个 bool 值。

但是,由 dp[i] 却无法推出一个可以正确表示 dp[i + 1] 的状态转移方程。

所以,可以换一种思路。

所以,对于以 i 位置为结尾的字符串,还需要多加一层循环,用来找到合适的 j 以使 0~j 和 j~i 这两段子字符串都满足条件。

代码:

class Solution 
{
public:
    bool wordBreak(string s, vector<string>& wordDict) 
    {
        unordered_set<string> hash;
        for (string word : wordDict) hash.insert(word);
        int n = s.size();
        vector<bool> dp(n + 1);
        dp[0] = true;
        s = ' ' + s;
        for (int i = 1; i <= n; i++)
        {
            for (int j = i; j >= 1; j--)
            {
                if (dp[j - 1] && hash.count(s.substr(j, i - j + 1)))
                {
                    dp[i] = true;
                    break;
                }
            }
        } 
        return dp[n];
    }
};

力扣 467. 环绕字符串中唯一的子字符串

力扣 467. 环绕字符串中唯一的子字符串

这道题用一层循环足以。

只需从前往后依次判断,两个相邻的字符相差是不是 1,或者前一个是 z,后一个是 a 也满足条件。

如果满足条件,则 dp[i] = dp[i - 1] + 1。dp[i] 表示以 i 位置为结尾的子字符串中满足条件的个数。

但是,有一个问题,题目要求返回的应该是所有符合条件的子字符串的和,如果直接将 dp 表中的每一个位置相加,结果会有重复。

所以,可以借助一个哈希表来记录以每一个字符为结尾的,满足条件的子字符串的个数。

最后,再依次相加即可。

代码:

class Solution 
{
public:
    int findSubstringInWraproundString(string s) 
    {
        int n = s.size(), ret = 0;
        vector<int> dp(n, 1);
        for (int i = 1; i < n; i++)
        {
            if (s[i] - s[i - 1] == 1 || (s[i - 1] == 'z' && s[i] == 'a'))
                dp[i] = dp[i - 1] + 1;    
        }
        vector<int> hash(26);
        for (int i = 0; i < n; i++) hash[s[i] - 'a'] = max(hash[s[i] - 'a'], dp[i]);
        for (int num : hash) ret += num;
        return ret;
    }
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1973917.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【编码心得】单元测试的那些事

【编码心得】单元测试的那些事 文章目录 单元测试定义&#xff1f;为什么需要单元测试&#xff1f;为重构保驾护航提高代码质量减少 bug快速定位 bug持续集成依赖单元测试团队编码规范要求大牛都写单元测试保住面子 TDD 测试驱动开发何谓 TDD&#xff1f;TDD的基本流程TDD 优缺…

全球轻型汽车市场规划预测:2030年市场规模将接近2502亿元,未来六年CAGR为2.8%

一、引言 随着全球经济的发展和消费者出行需求的增加&#xff0c;轻型汽车作为汽车市场中的重要组成部分&#xff0c;其市场重要性日益凸显。本文旨在探索轻型汽车行业的发展趋势、潜在商机及其未来展望。 二、市场趋势 全球轻型汽车市场的增长主要受全球经济发展、消费者对出…

海风小店微信商城小程序附后端一款免费开源的小程序源码

该商城小程序服务端api基于node.jsThinkJSMySQL&#xff0c;如果对这个不大熟悉的人&#xff0c; 可能有那么一点难度&#xff0c;但是如果只是搭建的话&#xff0c;作者的教程还是比较详细的&#xff0c;而且搭建步骤比较简单&#xff0c; 应该很容易上手&#xff0c;如果你…

【KAN】【API教程】索引

简单来说就是确定激活函数的坐标 from kan import *model = KAN(width=[2,3,2,1]) x = torch.normal(0,1,size=(100,2)) model(x); beta = 100 model.plot(beta=beta) # [2,3,2,1] means 2 input nodes # 3 neurons in the first hidden layer, # 2 neurons in the second hid…

知识图谱开启了一个可以理解的人工智能未来

概述 本文是对利用知识图谱&#xff08;KG&#xff09;的综合人工智能&#xff08;CAI&#xff09;的全面调查研究&#xff0c;其中 CAI 被定义为可解释人工智能&#xff08;XAI&#xff09;和可解释机器学习&#xff08;IML&#xff09;的超集。 首先&#xff0c;本文澄清了…

【Linux学习】实现一个简单版的Shell

&#x1f351;个人主页&#xff1a;Jupiter. &#x1f680; 所属专栏&#xff1a;Linux从入门到进阶 欢迎大家点赞收藏评论&#x1f60a; 目录 &#x1f4d5;前言&#x1f351;shell&#x1f4da;Shell的工作原理&#x1f512;Shell的高级功能 &#x1f680;shell的代码实现&am…

Mybatis实战:#{} 和 ${}的使用区别和数据库连接池

一.#{} 和 ${} #{} 和 ${} 在MyBatis框架中都是用于SQL语句中参数替换的标记&#xff0c;但它们在使用方式和处理参数值上存在一些显著的区别。 #{}的作用&#xff1a; #{} 是MyBatis中用于预编译SQL语句的参数占位符。它会将参数值放入一个预编译的PreparedStatement中&am…

JavaScript ES6语法详解(下)

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;我是码喽的自我修养&#xff01;今天给大家分享JavaScript ES6语法详解(下)&#xff01;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01;创作不易&#xff0c;如果能帮助到带大家&#xff0c;欢迎收藏关注…

信创企业级即时通讯发展趋势,私有化安全沟通

信创&#xff08;创新型科技公司&#xff09;在当今的商业环境中发挥着重要作用&#xff0c;因此&#xff0c;他们对于私有化安全沟通的需求日益增加。企业级即时通讯软件是为满足企业内部沟通和协作需求而设计的重要工具。在信创企业中&#xff0c;采用私有化安全沟通解决方案…

乐乐音乐Kotlin版

简介 乐乐音乐Kotlin版&#xff0c;主要是基于ExoPlayer框架开发的Android音乐播放器&#xff0c;它支持lrc歌词和动感歌词(ksc歌词、krc歌词、trc歌词、zrce歌词和hrc歌词等)、多种格式歌词转换器及制作动感歌词、翻译歌词和音译歌词。 编译环境 Android Studio Jellyfish | …

计算机毕业设计Python+Tensorflow股票推荐系统 股票预测系统 股票可视化 股票数据分析 量化交易系统 股票爬虫 股票K线图 大数据毕业设计 AI

1、用pycharm打开项目&#xff0c;一定要打开包含manage.py文件所在文件夹 2、配置解释器&#xff1a;建议使用Anaconda(Python 3.8(base))&#xff0c;低于3.8版本的&#xff0c;页面会不兼容 3、安装依赖库&#xff1a;打开pycharm的终端&#xff0c;输入&#xff1a; pip in…

第TR3周:Pytorch复现Transformer

本文为365天深度学习训练营 中的学习记录博客 原作者&#xff1a;K同学啊 任务详情&#xff1a; ●1. 从整体上把握Transformer模型&#xff0c;明白它是个什么东西&#xff0c;可以干嘛 ●2. 读懂Transformer的复现代码&#xff08;暂时不要过于纠结于某一个点&#xff0c;后面…

重生之我们在ES顶端相遇第9 章- 搜索框最常用的功能 - 搜索建议

文章目录 1 前言2 Term Suggester2.1 基本介绍2.2 使用 demo2.3 常用参数2.3.1 suggest_mode2.3.2 max_edits2.3.3 prefix_length2.3.4 min_word_length 3 Completion Suggester3.1 基本描述3.2 基本使用3.3 查询参数3.3.1 size3.3.2 skip_duplicates3.3.3 fuzzy queries(模糊查…

【WPF开发】安装环境、新建工程

一、安装环境 在安装VS时候&#xff0c;勾选安装开发环境 如果已安装VS&#xff0c;可以到工具中查看是否有相应环境 二、新建工程 点击“创建新项目” 通过顶部过滤&#xff0c;C#&#xff0c;选择“WPF应用&#xff08;NET.framework&#xff09;”&#xff0c;并点击“下一…

通过 ACM 论文模版学习 LaTeX 语法 【三、格式】

文章目录 一、LaTeX 简介二、ACM 论文模版三、格式3.1 文章格式3.1.1 注释3.1.2 空格3.1.3 换行 3.2 字体3.2.1 字体样式3.2.2 字体大小2.2.3 字体颜色 一、LaTeX 简介 通过 ACM 论文模版学习 LaTeX 语法 【一、LaTeX简介和安装】 二、ACM 论文模版 通过 ACM 论文模版学习 L…

一款免费开源绿色免安装的透明锁屏工具

一款免费开源绿色免安装的透明锁屏工具 这个工具的特点就是电脑锁屏的时候&#xff0c;仍然显示原桌面&#xff0c;但是无法操作&#xff0c;需要输入密码才可以解锁。输入密码界面也是隐藏的需要按键才能显示输入密码框。 电脑★★★★★透明锁屏工具&#xff1a;https://pa…

canvas-视频绘制

通过Canvas元素来实时绘制一个视频帧&#xff0c;并在视频帧上叠加一个图片的功能可以当作水印。 获取Canvas元素&#xff1a; let canvas document.getElementById(canvas) 通过getElementById函数获取页面中ID为canvas的Canvas元素&#xff0c;并将其存储在变量canvas中。 …

快速将网站从HTTP升级为HTTPS

在当今数字化的世界中&#xff0c;网络安全变的越来越重要&#xff0c;HTTPS&#xff08;超文本传输安全协议&#xff09;不仅能够提供加密的数据传输&#xff0c;还能增强用户信任度&#xff0c;提升搜索引擎排名&#xff0c;为网站带来多重益处。所以将网站从HTTP升级到HTTPS…

达利欧对话施一公:如何应对快速变化的世界?

本篇是对达利欧对话施一公&#xff1a;如何应对快速变化的世界&#xff1f;&#xff5c;凤凰《封面》这一视频的翻译与整理, 过程中为符合中文惯用表达有适当删改, 版权归原作者所有. 达利欧&#xff1a;我很兴奋&#xff0c;施教授和我有很多共同点&#xff0c;即使我们来自不…