1. 专题介绍
这一系列算法题都属于子数组专题:求数组中连续的一段。这是限制,也是抓手------通过对以该位置为结尾 / 开头设下限制,逐个推导。
Q:为什么能用?
A:
- 按
i结尾分类,n个位置刚好对应n类,互不重叠、合起来是全集 - 以
i结尾的子数组,往前只有一种延伸方式(把i-1结尾的某段接上),所以dp[i]只依赖dp[i-1] - 只依赖前一项 + 一维数组,仅需遍历一次,复杂度
O(N)
重点!解题流程:
- 状态表示:以 i 结尾的所有子数组 + 题目所求
- 状态转移:dpi 只有两种来源 ------ 自己 / 接上一项 dpi-1
- 初始化:不越界 + 保证后续填表的正确性
- 填表顺序:不确定,通常从左往右
- 返回值:不确定,通常要返回整张表的最值
创建多少张表,根据经验 + 题目要求确定。
2. 最大子数组和
为什么用动态规划:
最直接的做法是枚举左右端点,O(N²) 个区间挨个求和。问题在于这些区间大量重合:以 i-1 结尾的区间和,在算以 i 结尾时又被从头加了一遍。重复计算就是 dp 要消掉的东西。
换个枚举维度:不枚举"哪一段",而是枚举"结尾在哪一段"。答案一定以某个位置结尾,所以把以 i 结尾的最大和全部求出来取最大值即可。
而以i结尾的区间只有两种:只含 nums[i] 自己,或者由以 i-1 结尾的某段加一个 nums[i] 拼成。后者要挑最大的那段,那就是 dp[i-1]。于是:dp[i] = max(nums[i], dp[i-1] + nums[i])
HOW:
- 状态表示:
dp[i]表示以i位置结尾的所有子数组中的最大和 - 状态转移:
dp[i] = max(nums[i], dp[i-1] + nums[i]) - 初始化:最前面加一个辅助格子,
dp[0] = 0;下标映射为nums[i-1] - 填表顺序:从左往右
- 返回值:整张
dp表的最大值,不是dp[n]

复杂度 :时 O(N),空 O(N)
注意点:下表映射,分类讨论,状态转移方程推导,状态表示
cpp
class Solution {
public:
int getMaxLen(vector<int>& nums)
{
//创建dp表+初始化
int n = nums.size();
vector<int> f(n + 1, 0);
vector<int> g(n + 1, 0);
//填表
int ret = INT_MIN;
for(int i=1; i<=n; ++i)
{
if(nums[i-1] > 0)
{
f[i] = f[i-1] + 1;
g[i] = g[i-1] == 0 ? 0 : g[i-1]+1;
}
else if(nums[i-1] < 0)
{
f[i] = g[i-1] == 0 ? 0 : g[i-1]+1;
g[i] = f[i-1] + 1;
}
ret = max(ret, f[i]);
}
//返回值
return ret;
}
};
2. 环形子数组的最大和
- 状态表示:
f[i]表示以i结尾的所有子数组中的最大和,g[i]表示最小和 - 状态转移:
f[i] = max(x, x + f[i-1]),g[i] = min(x, x + g[i-1]) - 初始化:
f[0] = g[0] = 0 - 填表顺序:从左往右,两张表一起填
- 返回值:
sum == gmin ? fmax : max(fmax, sum - gmin)
注意点:
- 开两张表:将
环形数组分类讨论,转化为普通数组的题型(正+正难则反)

- 对状态转移分类讨论:(gi同理)

- 多画图!子数组问题都可以转化为
该元素 / 该元素之前的段两种状态,分类即可:

- 返回值的特殊性:数组全是负数,
gmin == sum,此时sum - gmin == 0。表示没有元素被正向选入,需要特殊判断
复杂度 :时 O(N),空 O(N)。
cpp
class Solution {
public:
int maxSubarraySumCircular(vector<int>& nums)
{
int n = nums.size();
vector<int> f(n + 1, 0);
vector<int> g(n + 1, 0);
int sum = 0;
int fmax = INT_MIN;
int gmin = INT_MAX;
for(int i=1; i<=n; ++i)
{
sum += nums[i - 1];
f[i] = max(f[i-1] + nums[i-1], nums[i - 1]);
g[i] = min(g[i-1] + nums[i-1], nums[i-1]);
fmax = max(f[i], fmax);
gmin = min(g[i], gmin);
}
return sum == gmin ? fmax : max(fmax, sum - gmin);
}
};
3. 乘积最大子数组
-
状态表示:
f[i]表示以i结尾的所有子数组的最大乘积,g[i]表示最小乘积 -
状态转移:(gi同理)

-
初始化:
f[0] = g[0] = 1,开n+1大小的数组 -
填表顺序:从左往右,两张表一起填
-
返回值:
f表的最大值
复杂度 :时 O(N),空 O(N)
注意点:
- 开两个数组的必要性:乘法存在正负,仅凭一张表视野有限(无法通过上一个位置推出该位置的状态),必须另外开一张表同步
负数的视野 - 初始化为1而不是0
cpp
class Solution {
public:
int maxProduct(vector<int>& nums)
{
//1.创建dp数组+初始化
int n = nums.size();
vector<int> f(n + 1, 1);
vector<int> g(n + 1, 1);
//2.填表
int ret = INT_MIN;
for(int i=1; i<=n; ++i)
{
f[i] = max({f[i-1]*nums[i-1], nums[i-1], g[i-1]*nums[i-1]});
g[i] = min({g[i-1]*nums[i-1], nums[i-1], f[i-1]*nums[i-1]});
ret = max(ret, f[i]);
}
//3.返回
return ret;
}
};
4. 乘积为正数的最长子数组
- 状态表示:
f[i]表示以i结尾、乘积为正的最长子数组长度,g[i]表示乘积为负的最长长度 - 状态转移:按
nums[i]的符号分类讨论:
- 初始化:
f[0] = g[0] = 0(存储长度) - 填表顺序:从左往右,两张表一起填
- 返回值:
f表的最大值
复杂度 :时 O(N),空 O(N)。
注意点:
- 表中存储的是
长度,注意区分 x == 0不需要在dp表中处理状态转移方程中三元运算符的处理原因- 分类讨论
cpp
class Solution {
public:
int getMaxLen(vector<int>& nums)
{
//创建dp表+初始化
int n = nums.size();
vector<int> f(n + 1, 0);
vector<int> g(n + 1, 0);
//填表
int ret = 0;
for(int i=1; i<=n; ++i)
{
if(nums[i-1] > 0)
{
f[i] = f[i-1] + 1;
g[i] = g[i-1] == 0 ? 0 : g[i-1]+1;
}
else if(nums[i-1] < 0)
{
f[i] = g[i-1] == 0 ? 0 : g[i-1]+1;
g[i] = f[i-1] + 1;
}
ret = max(ret, f[i]);
}
//返回值
return ret;
}
};