记录93
cpp
#include<bits/stdc++.h>
using namespace std;
const int N=1010;
long long dp[N],a[N],s[N];
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
s[i]=s[i-1]+a[i];//前缀和
dp[i]=-1;//前i头牛最多能分成多少个合法小组,-1为不可能
}
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
if(s[i]-s[j-1]>=0&&dp[j-1]!=-1) dp[i]=max(dp[i],dp[j-1]+1);//前缀和的差就是第j到第i头牛的和
}//-1作用,如果前面都分不了,那加上这一段也没用
}//j-1头牛分成dp[j-1]组,把j到i作为新的一组更新值
if(dp[n]<0) cout<<"Impossible";
else cout<<dp[n];
return 0;
}
题目传送门
https://www.luogu.com.cn/problem/P1569
突破口
约翰家的 n 头奶牛聚集在一起,排成一列,正在进行一项抗议活动。第 i 头奶牛的理智度为 ai。
约翰希望奶牛在抗议时保持理性,为此,他打算将所有的奶牛隔离成若干个小组,每个小组内的奶牛的理智度总和都要不小于零。
由于奶牛是按直线排列的,所以一个小组内的奶牛位置必须是连续的。请帮助约翰计算一下,最多分成几组。
思路
题目核心理解
🎯 目标
将 排成一列的 n 头奶牛 分成 若干个连续小组,使得:
- 每个小组的 理智度总和 ≥ 0
- 小组必须是 连续子数组
- 要求 最多能分成多少组
✅ 这是一个 最大化分段数 的动态规划问题,带约束(每段和 ≥ 0)
❌ 无解情况
如果 整个序列无法被完全划分 (比如某部分无论如何都无法满足和 ≥ 0),则输出 "Impossible"。
🧠 二、解题思路:动态规划 + 前缀和优化判断
关键观察
- 因为小组必须连续,我们可以枚举 最后一段的起点 j(1 ≤ j ≤ i)
- 若子段
[j, i]的和 ≥ 0,且前j−1头牛能合法分组,则可更新dp[i]
状态定义
dp[i]:表示 前 i 头奶牛 最多能分成多少个合法小组- 若无法合法分组,则
dp[i] = -1(作为"不可达"标记)
前缀和预处理
s[i] = a[1] + a[2] + ... + a[i]- 子段
[j, i]的和 =s[i] - s[j-1]
状态转移
对每个 i(1 到 n):
cpp
for j in 1..i:
if (s[i] - s[j-1] >= 0) and (dp[j-1] != -1):
dp[i] = max(dp[i], dp[j-1] + 1)
💡 解释:
dp[j-1]是前j−1头牛的最大分组数- 把
[j, i]作为新的一组(需满足和 ≥ 0)- 总组数 =
dp[j-1] + 1
初始条件
dp[0] = 0:0 头牛,0 组(隐含,代码中通过s[0]=0和dp[j-1]当 j=1 时访问dp[0]实现)- 其他
dp[i]初始化为-1(不可达)
最终答案
- 若
dp[n] < 0→ 输出"Impossible" - 否则输出
dp[n]
代码分析
cpp
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
long long dp[N], a[N], s[N];
N=1010:因 n ≤ 1000,留余量a[i]:第 i 头奶牛的理智度s[i]:前缀和dp[i]:前 i 头牛最多分组数(-1 表示不可能)
cpp
int main(){
int n;
cin >> n;
for(int i = 1; i <= n; i++){
cin >> a[i];
s[i] = s[i-1] + a[i]; // 计算前缀和
dp[i] = -1; // 初始化为不可达
}
- 读入数据,同时计算前缀和
s[0] = 0(全局变量默认初始化为 0)dp[i] = -1表示初始时前 i 头牛无法分组
⚠️ 注意:
dp[0]未显式赋值,但作为全局变量,dp[0] = 0(因为未初始化的全局long long为 0)这正是我们需要的:前 0 头牛分 0 组
🔁 动态规划主循环
cpp
for(int i = 1; i <= n; i++){
for(int j = 1; j <= i; j++){
- 枚举当前考虑前
i头牛 - 枚举最后一段的起始位置
j(从 1 到 i)
cpp
if(s[i] - s[j-1] >= 0 && dp[j-1] != -1)
dp[i] = max(dp[i], dp[j-1] + 1);
s[i] - s[j-1]= 第 j 到第 i 头牛的和- 若该和 ≥ 0,且前
j−1头牛可分组(dp[j-1] != -1) - 则尝试用
dp[j-1] + 1更新dp[i]
💡 举例:当 j=1 时,检查整个
[1,i]是否 ≥0,且dp[0]=0合法 → 可分 1 组
cpp
}
}
🔚 输出结果
cpp
if(dp[n] < 0) cout << "Impossible";
else cout << dp[n];
return 0;
}
- 若
dp[n]仍为 -1 → 无法分组 - 否则输出最大组数
🧪 样例验证:n=4, a = [2, 3, -3, 1]
前缀和 s = [0, 2, 5, 2, 3]
计算 dp:
dp[0] = 0i=1: j=1 → sum=2≥0, dp[0]=0 → dp[1]=1i=2:- j=1: sum=5≥0 → dp[0]+1=1
- j=2: sum=3≥0, dp[1]=1 → 1+1=2 → dp[2]=2
i=3:- j=1: sum=2≥0 → 0+1=1
- j=2: sum=0≥0, dp[1]=1 → 2
- j=3: sum=-3<0 → 跳过 → dp[3]=2
i=4:- j=1: sum=3≥0 → 1
- j=2: sum=1≥0, dp[1]=1 → 2
- j=3: sum=-2<0 → 跳过
- j=4: sum=1≥0, dp[3]=2 → 3 → dp[4]=3 ✅
输出 3,符合样例!
⚠️ 复杂度与适用性
- 时间复杂度:O(n²) → n=1000 时约 1e6 次操作,可接受
- 空间复杂度:O(n)
💡 对于更大 n(如 1e5),需用单调队列优化,但本题 n≤1000,O(n²) 足够
总结
| 要点 | 说明 |
|---|---|
| 问题类型 | 最大化连续分段数,每段和 ≥ 0 |
| DP 状态 | dp[i] = 前 i 头牛最多分组数 |
| 转移关键 | 枚举最后一段起点 j,用前缀和快速判断合法性 |
| 边界处理 | dp[0]=0 是合法起点 |
| 无解判断 | dp[n] == -1 |