对当前目录下 1092 份 p5.cpp 提交进行算法归类、原理剖析与最优写法推荐。
从城市 0 走到城市 n,每条边 i→i+1 要么坐高铁(花 g[i]),要么被某次飞行覆盖(花 f[i])。一次飞行可以连续覆盖若干相邻边,只算一次飞机;飞行次数 ≤ k。求最少总时间。
关键观察:把"被飞行覆盖的边"看作若干段连续区间。每一段连续飞行 = 用掉 1 次飞机,覆盖的边付 f,其余边付 g。于是问题等价于:
令 d[i] = g[i] − f[i](飞行相对高铁的"节省"),基准 base = Σ g[i]。则
答案 = base − (至多 k 段连续区间内 d[i] 之和的最大值)。这正是经典的"最多 k 段子数组最大和"问题。注意:一段区间内可以包含 d<0 的边——当合并两个正区间能省下一次飞机名额时,这样做反而更优(见样例 3)。
绿=DP正解思路 · 黄=部分分贪心 · 橙=暴力 · 红=错误贪心 · 蓝=堆贪心 · 灰=非有效提交。分类基于代码特征自动统计,个别边界会有出入。
| 类别 | 核心思路 | 复杂度 | 正确性 / 预期得分 |
|---|---|---|---|
| A. DP(城市×飞行次数) | 状态 = (到第 i 城, 已用 j 次飞机, 当前是否正在飞),逐边转移 | 时间 O(n·k) | 完全正确,100 分(前提:用滚动数组控制内存) |
| B. 前缀和 + 堆/multiset 贪心 | 对 d 求前缀和,维护"最多 k 段最大和",用堆选最优 | O(n·k) 或 O(n log n) | 思路正确,但实现复杂、易写挂;本次仅 4 份且多为半成品 |
| C. 暴力 / DFS 枚举 | 递归枚举每段飞行的起止位置 | 指数级 / O(n²·…) | 正确但超时,约 10%~40% 部分分 |
| D. 仅处理 k=1(最大子段) | 只找单个 d 最大连续区间(Kadane 或 O(n²) 枚举) | O(n) ~ O(n²) | 只能拿 k=1 的两个子任务,约 30% |
| E. 差分 + 排序贪心 | 把 d[i] 排序,取最小的 k 个相加 | O(n log n) | 错误:把"飞 1 段连续边"误当成"飞 1 条边",无视区间合并 |
| F. 逐边 min(g,f) | 每条边独立取 min,完全忽略 k 限制 | O(n) | 错误:当 k < 需要飞的段数时会过量飞行 |
| G. 非有效提交 | 空程序、读入即结束、打表 14、甚至贴了 P1 的代码 | — | 0 分或仅样例分 |
这是最常见的"看似对"的错误。它们都假设每条边可以独立决定飞或不飞:
k。若有 5 段不相邻的边都值得飞,但 k=2,逐边 min 会"飞 5 次",违反限制。d[i] 当独立单位选 k 个——但一次飞机能覆盖一整段连续边。它既漏掉了"一次飞机飞多条边"的收益,也算不出"为合并两段而吞下中间负 d"的取舍。样例 3 是专门的反例:g=4,6,8 f=1,7,4,k=1。虽然 g[1]=6<f[1]=7(中间边飞行更亏),但只有 1 次飞机时,最优是一口气飞 0→3,吞下亏损的中间边,总时间 1+7+4=12,优于任何"只飞划算单边"的贪心。E/F 都答不对。
D(仅 k=1)本质是在 d[] 上求最大连续子段和(Kadane):base − max_subarray(d)。它对 k=1 的两档数据(共约 30%)完全正确,但 k≥2 无能为力。
C(DFS 枚举区间)思路无误——枚举每段飞行的起止——但 n≤1e5 时分支爆炸,只能过小数据(n≤200/1000)。
在 n·k ≤ 10⁵×10³ = 10⁸ 的约束下,O(n·k) 的 DP 是时间与实现难度的最佳平衡点,可稳定拿满分。状态机定义(直接对"最少时间"做 DP):
f[j][0] = 走到当前城市、已用 j 次飞机、当前没在飞(这条边坐了高铁/刚落地)的最少时间f[j][1] = 走到当前城市、已用 j 次飞机、当前正在这次飞行中的最少时间逐边转移(第 i 条边高铁费 a[i]、飞机费 b[i]):
f[j][0] = min(f[j][0], f[j][1]) + a[i] // 这条边坐高铁;若上一段在飞则飞行就此结束
f[j][1] = min(f[j][1], f[j-1][0]) + b[i] // 这条边飞:要么续上当前飞行,要么从"没飞"状态新开一次飞行(j-1→j)
关键工程点 —— 内存。朴素三维数组 dp[100010][1010][2] 需要约 1.6 TB(本次有提交真这么开,必然 MLE)。必须滚动掉城市那一维,只保留 f[k][2],并让 j 从大到小循环以正确引用上一城市的 f[j-1][0]。空间降到 O(k)。
#include <bits/stdc++.h>
using namespace std;
const long long INF = 4e18;
int a[100005], b[100005];
long long f[1005][2]; // f[j][0/1],城市维已滚动
int main(){
int n, m; cin >> n >> m;
for(int i=1;i<=n;i++) cin >> a[i];
for(int i=1;i<=n;i++) cin >> b[i];
for(int j=0;j<=m;j++) f[j][0]=f[j][1]=INF;
f[0][0] = a[1]; // 第1条边坐高铁,未飞
if(m>=1) f[1][1] = b[1]; // 第1条边飞,开启第1次飞行
for(int i=2;i<=n;i++)
for(int j=m;j>=0;j--){ // j 倒序:保证 f[j-1][0] 还是上一城市的值
if(f[j][0]!=INF || f[j][1]!=INF)
f[j][0] = min(f[j][0], f[j][1]) + a[i];
if(j && (f[j][1]!=INF || f[j-1][0]!=INF))
f[j][1] = min(f[j][1], f[j-1][0]) + b[i];
}
long long ans = INF;
for(int j=0;j<=m;j++) ans = min(ans, min(f[j][0], f[j][1]));
cout << ans;
}
本目录中 A006/p5.cpp 即此写法(滚动数组,正确且省内存),可作满分范例;A005/p5.cpp 思路相同但开了三维大数组会 MLE。
结论:第 5 节的 O(n·k) 状态机 DP + 滚动数组,是本题的推荐最优写法。
| 方案 | 时间 | 空间 | 好写吗 | 评价 |
|---|---|---|---|---|
| DP + 滚动数组 | O(n·k)≈1e8 | O(k) | 容易 | 推荐:1s 内稳过,代码短、不易错 |
| DP + 三维数组 | O(n·k) | O(n·k) | 容易 | 思路对但 MLE,必须改滚动 |
| 前缀和+堆贪心 | O(n log n) | O(n) | 难 | 渐进最快,但 k≤1000 时 DP 已够,且这种写法易出错,性价比低 |
虽然堆贪心在渐进复杂度上更优(O(n log n)),但本题数据范围下 O(n·k)=10⁸ 在 1 秒内绰绰有余,而 DP 的正确性更易保证、调试更简单。竞赛/教学语境下,O(n·k) DP 是公认的标准满分解。
dp[n][k][2] 三维数组——必须滚动城市维。min(g,f) 或排序取 k 个 d,忽略"一次飞机=一段连续区间"与样例 3 的合并取舍。j 写成正序,会用到本城市刚更新的 f[j-1],导致一次飞行被错误拆成多次。A006/p5.cpp解法最优 风格优秀 在全部 1092 份提交中,这份是正确性、效率、内存、可读性综合最好的范例:标准 O(n·k) 状态机 DP,并且把城市维滚动掉,空间只有 O(k)。
#include <iostream>
#include <cstring>
using namespace std;
const int MAXN = 1e5 + 5;
const long long INF = 4e18;
int a[MAXN], b[MAXN];
long long f[1005][2];
int main(){
int n, m;
cin >> n >> m;
if (n == 0){
cout << 0;
return 0;
}
for (int i = 0;i <= m;i++){
f[i][0] = f[i][1] = INF;
}
for (int i = 1;i <= n;i++){
cin >> a[i];
}
for (int i = 1;i <= n;i++){
cin >> b[i];
}
f[0][0] = a[1];
f[1][1] = b[1];
for (int i = 2;i <= n;i++){
for (int j = m;j >= 0;j--){
if (f[j][0] != INF || f[j][1] != INF)
f[j][0] = min(f[j][0], f[j][1]) + a[i];
if (j && (f[j][1] != INF || f[j - 1][0] != INF))
f[j][1] = min(f[j][1], f[j - 1][0]) + b[i];
}
}
long long minn = 4e18;
for (int i = 0;i <= m;i++) {
minn = min(minn, min(f[i][0], f[i][1]));
}
cout << minn;
}
f[1005][2] 只保留"飞行次数 × 是否在飞"两维,城市维靠原地滚动——避开了三维大数组 MLE(对照 A005 开 dp[100010][1010][2] 直接爆内存)。j 倒序循环,保证 f[j-1][0] 仍是"上一城市"的值,新开一次飞行的转移正确,不会把一次飞行错拆成多次。long long + INF=4e18 并用 !=INF 守卫,杜绝溢出与非法状态转移;还顺手处理了 n==0 边界。a/b/f/m 偏简,建议改成 g/f/dp/k 并加一行状态注释;初始化 f[1][1]=b[1] 隐含 m>=1,若 m==0 该格应保持 INF(此处恰好不影响答案,但显式判断更稳)。结论:思路是标准满分解,工程细节(滚动数组 + 溢出守卫)处理到位,可直接作为标程。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int a[N],b[N],d[N];
int main() {
int n,k;
cin>>n>>k;
for(int i=1; i<=n; i++) {
cin>>a[i];
}
for(int i=1; i<=n; i++) {
cin>>b[i];
d[i]=a[i]-b[i];
}
int ma=0;
int s,e;
int sum=0;
for(int i=1; i<=n; i++) {
sum=0;
for(int j=i; j<=n; j++) {
sum+=d[j];
if(sum>ma) {
s=i,e=j,ma=sum;
}
}
}
long long ans=0;
for(int i=1; i<=n; i++) {
if(i<s&&i>e)
{
ans+=a[i];
}
}
for(int i=s;i<=e;i++)
{
ans+=b[i];
}
cout<<ans;
return 0;
}
思路:求 d=g−f 的最大连续子段(O(n²) 枚举),即把问题按 k=1 处理,再用区间内走飞机、区间外走高铁求和。
不足:① 完全忽略 k,k≥2 拿不到更多分。② 致命 bug:if(i<s&&i>e) 恒为假(不可能既 i<s 又 i>e),导致区间外的高铁费 a[i] 一条都没加上,答案只剩区间内 f 之和——连 k=1 也算错;应为 i<s||i>e。③ ma 初始为 0,全负 d 时会取空区间。
如何提高:先把 k=1 写对(用 Kadane O(n) 求最大子段,并正确累加"区间外高铁 + 区间内飞机"),再升级到"至多 k 段"的 O(n·k) 状态机 DP(见第 5 节)。调试时打印中间区间 s,e 能立刻发现求和漏项。
#include<bits/stdc++.h>
using namespace std;
int g[100005],f[100005],n,k,dp[100005][1003];
int t(int i,int time){
if(dp[i][time]!=0){
return dp[i][time];
}
if(i>n){
return 0;
}
int s=t(i+1,time)+g[i];
//cout<<i<<' '<<time<<' '<<s<<endl;
if(time>0){
time--;
for(int j=i;j<=n;j++){
int c=0;
for(int k=i;k<=j;k++){
c+=f[k];
}
s=min(s,t(j,time)+c);
//cout<<c<<endl;
//cout<<i<<' '<<time<<' '<<s<<endl;
}
}
dp[i][time]=s;
//cout<<i<<' '<<time<<' '<<s<<endl;
return s;
}
int main(){
cin>>n>>k;
for(int i=1;i<=n;i++){
cin>>g[i];
}
for(int i=1;i<=n;i++){
cin>>f[i];
}
t(1,k);
int minid=2100000000;
for(int i=1;i<=n;i++){
for(int j=0;j<=k;j++){
cout<<dp[i][j]<<' '<<i<<' '<<j<<endl;
if(dp[i][j]!=0)
minid=min(dp[i][j],minid);
}
}
cout<<minid;
return 0;
}
思路:记忆化搜索,状态 (城市 i, 剩余飞行次数 time)——方向是对的,已经接近正解的状态设计。
不足:① 转移里再套一层 for j 累加 f 是 O(n),总复杂度 O(n²·k),n=1e5 必超时。② dp[100005][1003] ≈ 4×10⁸ 字节,MLE。③ time-- 改了变量后又拿它当记忆化下标,状态键被写乱。④ 用 dp!=0 当"已算过"标志不可靠(0 是合法值)。⑤ 留了调试 cout 打印整张表,输出格式直接错;最后对所有 dp[i][j] 取 min 也不是答案。
如何提高:保留"状态正确"的优点,把"枚举飞行终点"换成"当前是否正在飞"的二状态 O(1) 转移(O(n·k));滚动数组省内存;用独立 vis[][] 标记记忆化;删掉调试输出;答案取 dp[1][*] 而非全表最小。