【动态规划】子数组、子串问题

【动态规划】子数组、子串问题

一、最大子数组和

题目描述:

在这里插入图片描述

思路讲解:
本道题需要我们找到数组中连续子数组的最大和,子数组需连续且至少包含一个元素,问题可拆解为:以第 i 个元素结尾的最大子数组和,要么是将第 i 个元素加入前一个子数组,要么是从第 i 个元素重新开始。以下是具体思路:

在这里插入图片描述
  1. 状态表示:dp[i] 表示以第 i 个元素为结尾的连续子数组的最大和
  2. 状态转移方程
    • dp[i] = max(dp[i-1] + nums[i], nums[i])
      • 若 dp[i-1] + nums[i] 更大,说明以第 i-1 个元素结尾的子数组可以延续到第 i 个元素,形成更长的子数组
      • 若 nums[i] 更大,说明从第 i 个元素重新开始的子数组和更大,放弃之前的子数组
  3. 初始化
    • 第 0 个元素:dp[0] = nums[0],即以第 0 个元素为结尾的子数组最大和就是其自身
  4. 填写 DP 表与结果保存
    • 按照从左到右的顺序,从第 1 个元素遍历到最后一个元素(i=1 到 i=n-1),基于前一个位置的 dp 值计算当前值
    • 同时维护一个变量 ret,实时记录遍历过程中出现的最大 dp 值(即所有以不同位置为结尾的子数组和的最大值)
  5. 结果返回
    • 遍历结束后,ret 中存储的就是整个数组中最大的连续子数组和,直接返回即可

编写代码:

classSolution{public:intmaxSubArray(vector<int>& nums){int n = nums.size(); vector<int>dp(n,0); dp[0]= nums[0];int ret = dp[0];for(int i =1; i < n ; i++){ dp[i]=max(dp[i-1]+ nums[i],nums[i]); ret =max(ret,dp[i]);}return ret;}};

二、环形子数组的最大和

题目描述:

在这里插入图片描述

思路讲解:
本道题需要我们计算环形子数组的最大和,数组呈环形,最大子数组可能是:

  • 普通的非环形子数组(不跨首尾)
    • 问题可拆解为:以第 i 个元素结尾的最大子数组和,要么是将第 i 个元素加入前一个子数组,要么是从第 i 个元素重新开始
  • 环形子数组(包含首尾元素,中间跳过一段最小和的子数组)
    • 问题可拆解为:以第 i 个元素结尾的最小子数组和,要么是将第 i 个元素加入前一个子数组,要么是从第 i 个元素重新开始,最后通过整个数组的和 - 最小数组和,得到最大数组和

通过计算两种情况的最大值,即可得到环形子数组的最大和。以下是具体思路:

在这里插入图片描述
  1. 状态表示
    • f[i] 表示以第 i 个元素为结尾的最大连续子数组和(非环形场景)
    • g[i] 表示以第 i 个元素为结尾的最小连续子数组和(用于计算环形场景)
  2. 状态转移方程
    • f[i] = max(nums[i], f[i-1] + nums[i])
      • 以 i 结尾的最大子数组,要么从 i 重新开始,要么延续 i-1 的最大子数组
    • g[i] = min(nums[i], g[i-1] + nums[i])
      • 以 i 结尾的最小子数组,要么从 i 重新开始,要么延续 i-1 的最小子数组
  3. 初始化
    • 第 0 个元素:f[0] = nums[0],即以第 0 个元素为结尾的子数组最大和就是其自身
    • 第 0 个元素:g[0] = nums[0],即以第 0 个元素为结尾的子数组最小和就是其自身
  4. 填写 DP 表
    • 按照从左到右的顺序,从第 1 个元素遍历到最后一个元素(i=1 到 i=n-1),基于前一个位置的 f,g 值计算当前值
    • 维护一个变量 fmax,实时记录遍历过程中出现的最大 f 值
    • 维护一个变量 gmin,实时记录遍历过程中出现的最小 g 值
    • 维护一个变量 sum,实时记录整个数组的和
  5. 结果返回
    • 若数组所有元素均为负数(sum == gmin,即最小子数组和等于总和),此时环形子数组会包含所有元素(和为负数),应返回非环形的最大子数组和 fmax
    • 否则,返回 max(fmax, sum - gmin)(普通最大子数组和与环形子数组和的最大值)

编写代码:

classSolution{public:intmaxSubarraySumCircular(vector<int>& nums){int n = nums.size(); vector<int>f(n,0),g(n,0); f[0]= g[0]= nums[0];int sum = nums[0];int fmax = f[0], gmin = g[0];for(int i =1; i < n ; i++){ f[i]=max(nums[i],f[i-1]+ nums[i]); g[i]=min(nums[i],g[i-1]+ nums[i]); fmax =max(fmax,f[i]); gmin =min(gmin,g[i]); sum += nums[i];}return sum == gmin ? fmax :max(fmax,sum - gmin);}};

三、乘积最大子数组

题目描述:

在这里插入图片描述

思路讲解:
本道题需要我们计算出数组中非空连续子数组的最大乘积,乘积最大子数组与最大子数组和的区别在于:负负相乘可能得正,因此需同时跟踪最大和最小乘积。以下是具体思路:

在这里插入图片描述
  1. 状态表示
    • f[i] 表示以第 i 个元素为结尾的连续子数组的最大乘积
    • g[i] 表示以第 i 个元素为结尾的连续子数组的最小乘积
  2. 状态转移方程
    • f[i] = max(nums[i], max(f[i-1] * nums[i], g[i-1] * nums[i]))
      • 以 i 结尾的最大乘积,可能是:当前元素本身、前一个最大乘积乘以当前元素、前一个最小乘积乘以当前元素(负负得正的情况),取三者最大值
    • g[i] = min(nums[i], min(g[i-1] * nums[i], f[i-1] * nums[i]))
      • 以 i 结尾的最小乘积,可能是:当前元素本身、前一个最小乘积乘以当前元素、前一个最大乘积乘以当前元素(正负得负的情况),取三者最小值
  3. 初始化
    • 第 0 个元素:f[0] = g[0] = nums[0],即以第 0 个元素为结尾的子数组,最大和最小乘积均为其自身
  4. 填写 DP 表与结果保存
    • 按照从左到右的顺序,从第 1 个元素遍历到最后一个元素(i=1 到 i=n-1),基于前一个位置的 f 和 g 值计算当前值
    • 同时维护变量 ret,实时记录遍历过程中出现的最大 f[i](即所有以不同位置为结尾的子数组的最大乘积)
  5. 结果返回
    • 遍历结束后,ret 中存储的就是整个数组中乘积最大的连续子数组的乘积,直接返回即可

编写代码:

classSolution{public:intmaxProduct(vector<int>& nums){int n = nums.size(); vector<int>f(n,0),g(n,0); f[0]= g[0]= nums[0];int ret = f[0];for(int i =1; i < n ; i++){ f[i]=max(nums[i],max(f[i-1]* nums[i],g[i-1]* nums[i])); g[i]=min(nums[i],min(g[i-1]* nums[i],f[i-1]* nums[i])); ret =max(ret,f[i]);}return ret;}};

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

题目描述:

在这里插入图片描述

思路讲解:
本道题需要我们找出数组中乘积为正数的最长子数组的长度,乘积为正数的子数组需满足:元素乘积为正(负数个数为偶数,且无 0),需要区分两种状态跟踪子数组长度。以下是具体思路:

在这里插入图片描述
  1. 状态表示
    • f[i] 表示以第 i 个元素为结尾的乘积为正数的最长子数组长度
    • g[i] 表示以第 i 个元素为结尾的乘积为负数的最长子数组长度
  2. 状态转移方程
    • 根据当前元素 nums[i] 的值分三种情况:
      • 当前元素为 0:
        • 子数组包含 0 时乘积为 0,故 f[i] = 0,g[i] = 0(无法形成正 / 负乘积子数组)
      • 当前元素为正数:
        • 正乘积子数组可延续前一个正乘积子数组(正数 × 正数仍为正):f[i] = f[i-1] + 1
        • 负乘积子数组需延续前一个负乘积子数组(负数 × 正数仍为负),若前无负乘积子数组则为 0:g[i] = g[i-1] == 0 ? 0 : g[i-1] + 1
      • 当前元素为负数:
        • 正乘积子数组需延续前一个负乘积子数组(负数 × 负数为正),若前无负乘积子数组则为 0:f[i] = g[i-1] == 0 ? 0 : g[i-1] + 1
        • 负乘积子数组可延续前一个正乘积子数组(正数 × 负数为负):g[i] = f[i-1] + 1
  3. 初始化
    • 第 0 个元素:
      • 若 nums[0] > 0:f[0] = 1(自身形成正乘积子数组),g[0] = 0(无负乘积子数组)
      • 若 nums[0] < 0:f[0] = 0(无正乘积子数组),g[0] = 1(自身形成负乘积子数组)
      • 若 nums[0] = 0:f[0] = g[0] = 0
  4. 填写 DP 表与结果保存
    • 按照从左到右的顺序,从第 1 个元素遍历到最后一个元素(i=1 到 i=n-1),基于前一个位置的 f 和 g 值计算当前值
    • 维护变量 ret,实时记录遍历过程中出现的最大 f[i](即最长正乘积子数组长度)
  5. 结果返回
    • 遍历结束后,ret 中存储的就是乘积为正数的最长子数组长度,直接返回即可

编写代码:

classSolution{public:intgetMaxLen(vector<int>& nums){int n = nums.size(); vector<int>f(n,0),g(n,0); f[0]= nums[0]>0?1:0; g[0]= nums[0]<0?1:0;int ret = f[0];for(int i =1; i < n ; i++){if(nums[i]==0){ f[i]=0; g[i]=0;}elseif(nums[i]>0){ f[i]= f[i-1]+1; g[i]= g[i-1]==0?0: g[i-1]+1;}else{ f[i]= g[i-1]==0?0: g[i-1]+1; g[i]= f[i-1]+1;} ret =max(ret,f[i]);}return ret;}};

五、等差数列划分

题目描述:

在这里插入图片描述

思路讲解:
本道题需要我们找出数组中子数组为等差数列的个数,等差数列子数组需满足:长度至少为 3,且任意相邻元素差相等,问题可拆解为:若当前位置与前两个位置构成等差数列,则以当前位置为结尾的新等差数列子数组个数 = 以前一个位置为结尾的个数 + 1。以下是具体思路:

在这里插入图片描述
  1. 状态表示:dp[i] 表示以第 i 个元素为结尾的等差数列子数组的个数
  2. 状态转移方程
    • 若 nums[i] - nums[i-1] == nums[i-1] - nums[i-2](即三个连续元素构成等差数列),则 dp[i] = dp[i-1] + 1
    • 否则,dp[i] = 0(当前位置无法构成以其为结尾的等差数列子数组)
    • 当第 i 个元素能与前两个元素形成等差数列时,会新增一个包含 i 的子数组,同时继承之前所有以 i-1 为结尾的子数组(将 i 加入后仍为等差数列)
  3. 初始化
    • 数组长度小于 3 时,不存在等差数列子数组,返回 0
    • dp[0] = dp[1] = 0:前两个元素无法构成长度 >= 3 的子数组,故初始化为 0
  4. 填写 DP 表
    • 按照从左到右的顺序,从第 2 个元素遍历到最后一个元素(i=2 到 i=n-1),判断是否构成等差数列并更新 dp[i]
  5. 结果返回
    • 累加 dp 数组后得到的总和,即为数组中所有等差数列子数组的总个数

编写代码:

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

六、最长湍流子数组

题目描述:

在这里插入图片描述

思路讲解:
本道题需要我们找出最长湍流子数组的长度,湍流子数组要求相邻元素的比较符号交替翻转(如 <,>,< 或 >,<,>),所以我们需要定义两个状态数组分别记录两种交替模式下以每个位置为结尾的最长湍流子数组长度。以下是具体思路:

在这里插入图片描述
  1. 状态表示
    • f[i] 表示以第 i 个位置为结尾中所有子数组,最后呈现为“上升”趋势的最长湍流子数组的长度
    • g[i] 表示以第 i 个位置为结尾中所有子数组,最后呈现为“下降”趋势的最长湍流子数组的长度
  2. 状态转移方程
    • 若 arr[i] > arr[i-1](当前为上升):
      • 前一个比较必须为下降才能形成交替,故 f[i] = g[i-1] + 1(延续前一个下降模式并加 1)
    • 若 arr[i] < arr[i-1](当前为下降):
      • 前一个比较必须为上升才能形成交替,故 g[i] = f[i-1] + 1(延续前一个上升模式并加 1)
    • 若 arr[i] == arr[i-1]:
      • 无法形成湍流(比较符号不翻转),故 f[i] = g[i] = 1(重置为 1,仅含当前元素)
  3. 初始化
    • 所有位置初始值为 1:f[i] = g[i] = 1(单个元素本身是长度为 1 的湍流子数组)
  4. 填写 DP 表与结果保存
    • 采用自底向上顺序,从第 1 个元素遍历到最后一个元素(i=1 到 i=n-1),根据相邻元素的大小关系更新 f[i] 和 g[i]
    • 维护变量 ret,实时记录遍历过程中 f[i] 和 g[i] 的最大值(即最长湍流子数组长度)
  5. 结果返回
    • 遍历结束后,ret 中存储的就是最长湍流子数组的长度,直接返回即可

编写代码:

classSolution{public:intmaxTurbulenceSize(vector<int>& arr){int n = arr.size(); vector<int>f(n,1),g(n,1);int ret = f[0];for(int i =1; i < n ; i++){if(arr[i]> arr[i-1]) f[i]= g[i-1]+1;if(arr[i]< arr[i-1]) g[i]= f[i-1]+1; ret =max(ret,f[i]); ret =max(ret,g[i]);}return ret;}};

七、单词拆分

题目描述:

在这里插入图片描述

思路讲解:
本道题需要我们判断字符串 s 能否被拆分为字典 wordDict 中的单词组合,可重复使用字单词,问题可拆解为:前 i 个字符能否拆分,取决于是否存在某个 j < i,使得前 j 个字符可拆分且 j 到 i 的子串在字典中。以下是具体思路:

在这里插入图片描述
  1. 状态表示:dp[i] 表示字符串 s 的前 i 个字符(即 s [0…i-1])能否被字典中的单词拼接而成(true表示可以,false表示不可以)
  2. 状态转移方程
    • 对于 i ≥ 1(前 i 个字符):
      • 若存在 j(1 ≤ j ≤ i),使得:
        • dp[j-1] 为 true(前 j-1 个字符可拆分)
        • 子串 s[j-1…i-1](即从第 j 个字符到第 i 个字符)存在于字典中
        • 则 dp[i] = true(前 i 个字符可拆分)
  3. 初始化
    • dp[0] = true:空字符串可被拆分(作为拆分的起点)
    • 为方便截取子串,将原字符串 s 前加一个空格得到news,使下标从 1 开始对应原字符串的第 0 个字符
  4. 填写 DP 表与字典优化
    • 按照从左到右的顺序,外层循环遍历前 i 个字符(i=1 到 i=n),内层循环遍历可能的拆分点 j(1 ≤ j ≤ i)
    • 将字典转为哈希集合hash,实现 O (1) 时间复杂度的单词存在性判断
  5. 结果返回
    • 最终 dp[n] 表示整个字符串 s(前 n 个字符)能否被拆分,返回该值即可

编写代码:

classSolution{public:boolwordBreak(string s, vector<string>& wordDict){int n = s.size(); unordered_set<string>hash(wordDict.begin(),wordDict.end()); vector<bool>dp(n +1,false); dp[0]=true; string news =" "+ s;for(int i =1; i <= n ; i++){for(int j =1; j <= i ; j++){ string sub = news.substr(j,i-j+1);if(dp[j-1]&& hash.count(sub)){ dp[i]=true;}}}return dp[n];}};

八、环绕字符串中唯一的子字符串

题目描述:

在这里插入图片描述

思路讲解:
本道题需要我们统计s中有多少不同非空子串也在 base 中出现,base 是 “abcdefghijklmnopqrstuvwxyz” 的无限循环,因此其中的子串需满足:每个相邻字符是连续的(如 “abc”),或 ‘z’ 后接 ‘a’(如 “zab”),以某个字符结尾的符合条件的子串,其最长长度决定了该字符贡献的独特子串数量。以下是本道题的具体思路:

在这里插入图片描述
  1. 状态定义
    • dp[i] 表示以 s 中第 i 个字符为结尾的最长连续符合 base 规则的子串长度(连续指符合 base 的字符顺序)
  2. 状态转移方程
    • 若当前字符与前一个字符符合 base 的连续规则(s[i] - s[i-1] == 1 或 s[i-1] == 'z’且s[i] == ‘a’),则 dp[i] = dp[i-1] + 1(延续前一个字符的连续子串)
    • 否则,dp[i] = 1(当前字符自身形成长度为 1 的子串)
  3. 初始化
    • dp数组长度为s的长度n,所有元素初始化为1
      • 单个字符自身是长度为 1 的符合条件的子串(如s[0]本身一定在base中)
    • 辅助数组arr长度为 26(对应 26 个小写字母),初始化为0,用于记录每个字符结尾的最长符合条件子串长度
  4. 填写 DP 表
    • 按照从左到右的顺序,遍历s从第 1 个字符到最后一个字符(i = 1到i = n-1),根据字符关系更新dp[i]
  5. 去重统计
    • 由于不同位置的相同字符可能贡献重复子串(如 “aba” 中两个 ‘a’ 的子串有重叠),需按字符去重:
    • 遍历dp数组,更新arr:arr[s[i]-‘a’] = max(arr[s[i]-‘a’], dp[i])
  6. 结果计算
    • 所有字符贡献的子串数量之和即为答案,即累加arr数组的所有元素(每个arr[c]的值等于以 c 结尾的独特子串数量)

编写代码:

classSolution{public:intfindSubstringInWraproundString(string s){int n = s.size(); 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];}} vector<int>arr(26,0);for(int i =0; i < n ; i++){ arr[s[i]-'a']=max(arr[s[i]-'a'],dp[i]);}int ret =0;for(int i =0; i <26; i++){ ret += arr[i];}return ret;}};

结尾

如果有什么建议和疑问,或是有什么错误,大家可以在评论区中提出。
希望大家以后也能和我一起进步!!🌹🌹
如果这篇文章对你有用的话,希望大家给一个三连支持一下!!🌹🌹

Read more

Java SpringBoot框架Web开发实战04--使用Lockback技术进行日志管理

Java SpringBoot框架Web开发实战04--使用Lockback技术进行日志管理

日志是每个网页所必须的东西,当网站出错时,日志就可以帮我们快速查看错误,传统的systemout进行输出日志有很多缺陷。一是如果后来某一大板块不需要日志进行输出,这时候你想要修改代码要把所有的输出代码全部删除,二是这种方式不会保留日志文件,如果出现闪退现象你无法得知是哪里的错误,但是使用lockback技术就可以避免这些问题。 下面是一段完整的logback.xml代码。 <?xml version="1.0" encoding="UTF-8"?> <configuration> <!-- 控制台输出 --> <appender name="STDOUT"> <encoder> <!--格式化输出:%d 表示日期,%thread 表示线程名,%-5level表示级别从左显示5个字符宽度,%logger显示日志记录器的名称, %msg表示日志消息,

By Ne0inhk
用AI科研绘图全新升级!借助谷歌新发布模型Nano Banana pro实测一键绘制机制图与技术路线图,直接告别文字乱码与低画质

用AI科研绘图全新升级!借助谷歌新发布模型Nano Banana pro实测一键绘制机制图与技术路线图,直接告别文字乱码与低画质

之前有同仁反馈,用Nano Banana画科研插图的时候,要么文字部分容易乱码,要么抓不住关键内容,画出来的效果反复修改后,还是达不到理想的效果。 终于在Gemini 3发布没几天,万众期待的大香蕉模型也成功升级到了Pro版本,官方名称叫“Gemini 3 Pro Image”,实际上也就是我们说的Nano Banana 2。 这次的Nano Banana Pro模型,不仅可以直出4K高清图,还能自定义比例,推理能力和中文文字的稳定性也得到了提升。 七哥实测用Nano Banana Pro模型来绘制科研插图,效果非常好,接下来我来进行实操,分别用它来画机制图和技术路线图。 开始之前我们需要打开Gemini 3Pro的Thinking模式,然后点击工具中的“Creat Image”,这样才算是调用Nano Banana Pro模型。 1、机制图 机制图的核心要求是科学准确,逻辑、符号和关键信息绝对不能出错,格式方面要统一,配色需符合特定期刊要求。

By Ne0inhk
个人健康中枢的多元化AI硬件革新与精准健康路径探析

个人健康中枢的多元化AI硬件革新与精准健康路径探析

在医疗信息化领域,个人健康中枢正经历着一场由硬件技术革新驱动的深刻变革。随着可穿戴设备、传感器技术和人工智能算法的快速发展,新一代健康监测硬件能够采集前所未有的多维度生物数据,并通过智能分析提供精准的健康建议。本文将深入探讨构成个人健康中枢的最新硬件技术,分析它们如何采集和处理多维生物数据,以及这些数据如何转化为个性化的健康指导方案,最终实现从被动治疗到主动预防的健康管理模式转变。 多维度生物数据采集的最新硬件技术 个人健康中枢的构建离不开先进的数据采集硬件,近年来,各类创新设备在生物信号采集能力上取得了显著突破,能够从生理、心理及行为等多个维度获取健康相关数据。 智能穿戴设备已从简单的步数计数器进化为精密的生物传感器网络。现代智能手表和手环不仅能够持续监测心率、血氧饱和度、血压等传统生理指标,还整合了心电图(ECG)和连续血糖监测(CGM)功能,实现了对心血管系统和代谢系统的高精度追踪[0

By Ne0inhk
OpenClaw 到底是什么?一篇讲清能动手干活的 AI 智能体

OpenClaw 到底是什么?一篇讲清能动手干活的 AI 智能体

最近AI圈最火的开源项目,非OpenClaw莫属。有人称它是“能动手干活的数字员工”,有人说它是个人专属“贾维斯”,也有小白疑惑它和ChatGPT、豆包这类AI到底有啥区别。今天这篇博文,不玩专业术语堆砌,从定位、功能、原理、实操到优缺点,全方位拆解OpenClaw,让你看完就懂它是什么、能做什么、怎么用,彻底搞懂这款“打破AI只说不做”的神器。 先给大家一个最通俗的定义:OpenClaw不是单纯的对话AI,而是一款基于MIT开源协议、本地优先部署的AI智能体执行网关,核心是“能听懂指令、能动手执行”——它就像一个不知疲倦的专属助手,不用你每一步手动操作,只要你用自然语言下达命令,它就能直接操控你的电脑、调用各类工具,把重复、繁琐的任务从头到尾做完,真正实现“指令一出,万事落地”。 很多人会把OpenClaw和传统AI搞混,这里用一组对比,一秒分清核心差异,看完你就明白它的独特价值: **传统AI(ChatGPT/豆包/Kimi等):**相当于“只会回答问题的秘书”,你问它答,只能输出文字、

By Ne0inhk