跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

动态规划:子数组与子串问题实战

动态规划在子数组与子串问题中的应用广泛,涵盖最大和、环形结构、乘积最值及最长长度等场景。核心在于状态定义与转移方程的构建,需区分正负数影响、连续性约束及字典匹配逻辑。C++ 代码示例展示了从基础 DP 到空间优化的实现细节,帮助理解连续子序列问题的通用解法。

锁机制发布于 2026/3/15更新于 2026/9/1161 浏览
动态规划:子数组与子串问题实战

一、最大子数组和

题目要求找到数组中连续子数组的最大和,子数组需连续且至少包含一个元素。核心思路是拆解为:以第 i 个元素结尾的最大子数组和,要么是将第 i 个元素加入前一个子数组,要么是从第 i 个元素重新开始。

最大子数组和状态转移图

我们定义 dp[i] 表示以第 i 个元素为结尾的连续子数组的最大和。状态转移方程为 dp[i] = max(dp[i-1] + nums[i], nums[i])。如果 dp[i-1] + nums[i] 更大,说明延续之前的子数组更优;反之则从当前元素重新开始。初始化时 dp[0] = nums[0],遍历时维护一个变量记录全局最大值即可。

class Solution {
public:
    int maxSubArray(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;
    }
};

二、环形子数组的最大和

对于环形数组,最大子数组可能不跨首尾(普通情况),也可能跨越首尾(环形情况)。跨越首尾的情况等价于总和减去中间某段最小子数组的和。

环形子数组状态转移图

我们需要同时计算非环形的最大子数组和 fmax,以及最小子数组和 gmin。若所有元素均为负数,直接返回 fmax;否则比较 fmax 与 sum - gmin 取较大值。

class Solution {
public:
    int maxSubarraySumCircular(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);
    }
};

三、乘积最大子数组

乘积问题比求和复杂,因为负负得正。因此需要同时跟踪以当前位置结尾的最大乘积 f[i] 和最小乘积 g[i]。

乘积最大子数组状态转移图

当遇到负数时,最大乘积可能变成最小,最小乘积可能变成最大。所以转移方程中要同时考虑 f[i-1] * nums[i] 和 g[i-1] * nums[i]。

class Solution {
public:
    int maxProduct(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 时重置长度;遇到正数时延续对应符号的长度;遇到负数时交换正负长度的逻辑(正变负,负变正)。

class Solution {
public:
    int getMaxLen(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;
            } else if (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 及以上的等差子数组个数。如果 nums[i] - nums[i-1] == nums[i-1] - nums[i-2],则以 i 结尾的新增等差子数组数量等于以 i-1 结尾的数量加 1。

等差数列划分状态转移图

注意初始化时前两个元素无法构成长度>=3的子数组,所以 dp[0] 和 dp[1] 为 0。最终结果是 dp 数组的累加和。

class Solution {
public:
    int numberOfArithmeticSlices(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;
    }
};

六、最长湍流子数组

湍流子数组要求相邻元素的比较符号交替翻转(如 <, >, <)。我们需要定义两个状态:以 i 结尾最后上升的长度 f[i] 和最后下降的长度 g[i]。

最长湍流子数组状态转移图

若 arr[i] > arr[i-1],则 f[i] = g[i-1] + 1;若 arr[i] < arr[i-1],则 g[i] = f[i-1] + 1;相等则重置为 1。

class Solution {
public:
    int maxTurbulenceSize(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 中的单词拼接而成。dp[i] 表示前 i 个字符能否拆分。若存在 j < i 使得 dp[j] 为 true 且 s[j...i-1] 在字典中,则 dp[i] 为 true。

单词拆分状态转移图

使用哈希集合优化字典查找,时间复杂度可降至 O(n^2)。

class Solution {
public:
    bool wordBreak(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 是字母表的无限循环。关键在于以某个字符结尾的最长连续符合 base 规则的子串长度决定了该字符贡献的独特子串数量。

环绕字符串唯一子串状态转移图

遍历 s,若当前字符与前一个字符连续(或 z->a),则长度加 1,否则重置为 1。最后按字符去重累加最大长度。

class Solution {
public:
    int findSubstringInWraproundString(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;
    }
};

目录

  1. 一、最大子数组和
  2. 二、环形子数组的最大和
  3. 三、乘积最大子数组
  4. 四、乘积为正数的最长子数组长度
  5. 五、等差数列划分
  6. 六、最长湍流子数组
  7. 七、单词拆分
  8. 八、环绕字符串中唯一的子字符串

更多推荐文章

查看全部
  • 12 款主流在线代码编辑器对比:谁比 GitHub Codespaces 更香?
  • OpenClaw 本地 AI 助手部署与飞书对接指南
  • 六轴机械臂正运动学建模与 Python 实现
  • 前端水印技术与反爬策略实现方案
  • Moon VR Video Player 使用教程:支持 8K/12K 多音轨及外挂字幕
  • C++ 函数重载:核心规则、常见陷阱与实战
  • 大模型 API 注册与调用实战指南
  • OpenClaw.ai:Agentic AI 时代的 Spring Framework 时刻
  • MySQL JDBC 编程基础
  • Go 命令行 AI 对话客户端:环境部署与核心实现
  • DeerFlow 2.0 开源:字节跳动超级智能体框架技术解析
  • C++ 二叉搜索树原理与高效实现
  • Python 30 分钟构建简易记事本应用
  • GESP 2024 年 6 月 C++ 二级认证判断题解析(1-10)
  • 滑动窗口算法核心思路与四道经典题解
  • Git Push 失败?配置 SSH Key 实现代码推送
  • 前端开发一天通常能完成多少个页面?
  • AXURE 11 结合 AI 的智能原型设计体验
  • Android 工程师面试准备指南:核心知识点与实战技巧
  • GitHub 热门 LLM 公开资料与大模型入门教程

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online