P3009 USACO11JAN Profits S 题解复盘
模块:线性 DP
目标:求连续子数组的最大和(最大子段和)
基本信息
| 项目 | 内容 |
|---|---|
| 题目编号、来源 | P3009 洛谷 / USACO11JAN Profits |
| 训练层级 | A 模板题 |
| 知识版块 | 线性 DP、最大子段和 |
解题前 · 关键信号识别
| 维度 | 分析 |
|---|---|
| 目标、约束、底层结构 | 目标 :求一个连续区间,使区间和最大。 约束 :必须是连续区间。 底层结构:当前位置的最优答案只依赖于前一个位置,因此属于线性 DP。 |
| 数据规模 | N≤10^5,必须使用 O(n) 算法。 |
| 候选算法和依据 | 算法:线性 DP(最大子段和)。 依据:当前位置只有两种选择:重新开始一个新区间,或者接到前面的连续区间后面。 |
| 复杂度预判 | 时间复杂度 O(n),空间复杂度 O(n)(可优化为 O(1))。 |
解题后 · 外化复盘
| 维度 | 内容 |
|---|---|
| 实现结构 / 核心思路 | 定义 dp[i] 表示以第 i 个数结尾 的最大连续子段和。当前位置可以重新开始,也可以接在前面的连续区间之后,因此:dp[i]=max(v[i],dp[i-1]+v[i])。最后遍历 dp 取最大值即可。 |
| 错因回溯 | 1. 一开始误认为是区间 DP,实际上与区间合并没有关系。 2. ans 初始化成 0,导致全负数数据出错。 3. 没有意识到状态必须是"以 i 结尾",而不是任意区间。 |
| 边界和易错点 | 1. dp[1]=v[1]。 2. ans 初始化为 dp[1] 或一个极小值。 3. 全负数时答案仍然是最大的那个负数,而不是 0。 |
| 下次看到什么信号,我应该想到这个方法 | 看到"连续区间""最大和""最大利润""最大收益",立即想到:最大子段和(Kadane)+ 线性 DP。 |
AC 完整代码
cpp
#include<iostream>
#include<algorithm>
using namespace std;
int dp[100005];
int main()
{
int n;
cin>>n;
int v[100005];
for(int i=1;i<=n;i++)
cin>>v[i];
dp[1]=v[1];
int ans=dp[1];
for(int i=2;i<=n;i++)
{
dp[i]=max(dp[i-1]+v[i],v[i]);
ans=max(ans,dp[i]);
}
cout<<ans;
return 0;
}
本题知识点总结
1. 状态定义
cpp
dp[i]
表示:
以第 i 个元素结尾的最大连续子段和。
2. 状态转移
当前位置有两种选择:
重新开始:
cpp
v[i]
或者:
接到前面的连续区间:
cpp
dp[i-1]+v[i]
所以:
cpp
dp[i]=max(v[i],dp[i-1]+v[i]);
3. 为什么必须是"以 i 结尾"?
因为只有这样:
下一状态:
cpp
dp[i+1]
才能直接利用:
cpp
dp[i]
如果状态定义成:
text
前 i 个数中的最大子段和
就无法完成转移。
4. 为什么不是区间 DP?
区间 DP 一般具有:
text
区间合并
例如:
cpp
dp[l][r]
而最大子段和:
每个状态只依赖:
cpp
dp[i-1]
属于:
text
线性 DP
5. 空间优化
由于:
cpp
dp[i]
只依赖:
cpp
dp[i-1]
因此可以压缩成:
cpp
int cur=v[1];
空间优化到:
text
O(1)
一句话总结
看到"连续区间最大和",立即想到:
dp[i]表示以第i个元素结尾的最大连续子段和,转移:dp[i]=max(v[i],dp[i-1]+v[i])。