跳到主要内容动态规划:子数组与子串问题详解 | 极客日志C++算法
动态规划:子数组与子串问题详解
动态规划解决子数组与子串问题的核心在于状态定义与转移。本文涵盖最大子数组和、环形子数组、乘积最大子数组、正数乘积最长长度、等差数列划分、湍流子数组、单词拆分及环绕字符串子串统计八类典型题型。通过维护以当前位置结尾的状态,结合贪心或哈希优化,可高效求解各类连续子序列最值与存在性问题。
指针猎手1 浏览 一、最大子数组和
题目要求找到数组中连续子数组的最大和。关键在于理解'以第 i 个元素结尾'这个状态。对于每个位置,我们有两种选择:要么把当前元素接在前一个子数组后面,要么从当前元素重新开始。
核心逻辑如下:
- 状态定义:
dp[i] 表示以第 i 个元素结尾的连续子数组的最大和。
- 状态转移:
dp[i] = max(dp[i-1] + nums[i], nums[i])。如果前一个子数组的和是正的,加上它更有利;否则不如直接抛弃前面的,从当前开始。
- 初始化与结果:
dp[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;
}
};
二、环形子数组的最大和
数组呈环形时,最大子数组可能不跨首尾,也可能跨越首尾(即包含首尾元素)。后者等价于用总和减去中间一段最小子数组的和。
我们需要同时计算普通情况下的最大子数组和,以及环形情况下的最大和(总和 - 最小子数组和)。
- 状态定义:
f[i] 表示以第 i 个元素为结尾的最大连续子数组和。
g[i] 表示以第 i 个元素为结尾的最小连续子数组和。
- 状态转移:
f[i] = max(nums[i], f[i-1] + nums[i])
g[i] = min(nums[i], g[i-1] + nums[i])
特殊情况:如果所有元素都是负数,总和等于最小子数组和,此时应返回非环形的最大值,避免取空集。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] 表示以第 i 个元素为结尾的连续子数组的最大乘积。
g[i] 表示以第 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]))
- 注意这里需要比较三个值:当前元素本身、前一个最大乘积、前一个最小乘积。
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。
我们需要区分两种状态:以当前位置结尾的正数乘积子数组长度,和负数乘积子数组长度。
- 状态定义:
f[i] 表示以第 i 个元素为结尾的乘积为正数的最长子数组长度。
g[i] 表示以第 i 个元素为结尾的乘积为负数的最长子数组长度。
- 状态转移:
- 若
nums[i] > 0:正数延续正数,负数延续负数。
- 若
nums[i] < 0:正数需由之前的负数转换而来,负数由之前的正数转换而来。
- 若
nums[i] == 0:两者都重置为 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 结尾的所有等差数列都可以延伸到 i。
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;
}
};
六、最长湍流子数组
湍流子数组要求相邻元素的比较符号交替翻转(如 <, >, < 或 >, <, >)。
我们可以定义两个状态,分别记录以当前位置结尾的最后一次比较是上升还是下降时的最长长度。
- 状态定义:
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;
}
};
七、单词拆分
判断字符串能否被字典中的单词拼接而成。这是一个典型的线性 DP 问题。
dp[i] 表示前 i 个字符能否被拆分。对于每个位置 i,我们尝试所有可能的分割点 j,检查 dp[j-1] 是否为真且 s[j...i] 是否在字典中。
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 中是连续的(如 'a'->'b' 或 'z'->'a'),则可以延长当前连续子串的长度。
由于需要去重,我们记录以每个字符结尾的最长连续子串长度,最终累加这些长度即可得到唯一子串总数。
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;
}
};
相关免费在线工具
- 加密/解密文本
使用加密算法(如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