题解:洛谷 P2904 River Crossing

【题目来源】

洛谷:P2904 [USACO08MAR] River Crossing S

【题目描述】

农夫约翰以及他的 \(N(1 \le N \le 2500)\) 头奶牛打算过一条河,但他们所有的渡河工具,仅仅是一个木筏。

由于奶牛不会划船,在整个渡河过程中,约翰必须始终在木筏上。在这个基础上,木筏上的奶牛数目每增加 \(1\),FJ把木筏划到对岸就得花更多的时间。

当约翰一个人坐在木筏上,他把木筏划到对岸需要 \(M(1 \le M \le 1000)\) 分钟。当木筏搭载的奶牛数目从 \(i-1\) 增加到 \(i\) 时,约翰得多花 \(M_i(1 \le M_i \le 1000)\) 分钟才能把木筏划过河(也就是说,船上有 \(1\) 头奶牛时,约翰得花 \(M+M_1\) 分钟渡河;船上有 \(2\) 头奶牛时,时间就变成 \(M+M_1+M_2\) 分钟。后面的以此类推)。那么,约翰最少要花多少时间,才能把所有奶牛带到对岸呢?当然,这个时间得包括约翰一个人把木筏从对岸划回来接下一批的奶牛的时间。

【输入】

  • 第一行包含 \(2\) 个整数 \(N\)\(M\)
  • \(2\) 行至第 \(N + 1\) 行,其中第 \(i + 1\) 行包含 \(1\) 个整数 \(M_i\)

【输出】

  • 输出共 \(1\) 行,为农夫约翰让所有奶牛过河所需的最短时间。

【输入样例】

5 10
3
4
6
100
1

【输出样例】

50

【核心思想】

  1. 问题分析:给定 \(N\) 头奶牛和基础划船时间 \(M\),搭载第 \(i\) 头奶牛额外增加 \(M_i\) 时间。约翰需分批将奶牛运到对岸,每次运输后若还有奶牛需返回接(返回时间也为 \(M\))。求将所有奶牛运到对岸的最短时间。这是一个线性 DP 问题,核心在于将运输过程分解为"前 \(k\) 头已运完,本次运第 \(k+1\) 到第 \(i\) 头"的子问题。

  2. 算法选择

    • 一维线性 DP\(f[i]\) 表示将 \(i\) 头奶牛运到对岸的最短时间
    • 前缀和预处理\(sa[k] = \sum_{j=1}^{k} M_j\),表示搭载 \(k\) 头奶牛的总额外时间
    • 分治思想:前 \(k\) 头已用最短时间 \(f[k]\) 运到对岸,本次运剩余的 \(i-k\)
  3. 关键步骤

    • 初始化:读取 \(N\)\(M\)\(M_1\)\(M_N\),计算前缀和 \(sa[k] = \sum_{j=1}^{k} M_j\)
    • 边界\(f[0] = 0\)\(0\) 头奶牛无需时间),其余 \(f[i] = \infty\)
    • DP 递推\(i\)\(1\)\(N\)):
      • 枚举前 \(k\) 头已运完的奶牛数(\(k\)\(0\)\(i-1\)):
        • 子问题时间:\(f[k]\)(前 \(k\) 头已运完)
        • 本次运输时间:\(M + sa[i-k]\)(基础时间 \(+\) 本次运 \(i-k\) 头的额外时间)
        • 返回时间:若 \(k > 0\)(之前已运过奶牛,约翰需从对岸返回),加 \(M\)
        • 总时间:\(tim = f[k] + M + sa[i-k] + (k > 0 ? M : 0)\)
        • 更新:\(f[i] = \min(f[i], tim)\)
    • 输出答案\(f[N]\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N^2)\),外层 \(O(N)\),内层枚举 \(k\) 最多 \(O(N)\)
    • 空间复杂度:\(O(N)\)\(f\) 数组和前缀和数组
  5. 线性 DP 的核心思想

    • 前缀分解:将"运 \(i\) 头"分解为"前 \(k\) 头已最优" \(+\) "本次运剩余 \(i-k\) 头",满足最优子结构
    • 前缀和优化\(sa[i-k]\) 表示每次运输从 \(0\) 头开始计数的额外时间,因为每批运输的额外时间只与这批的数量有关
    • 返回时间条件判断:仅当 \(k > 0\)(之前已有奶牛在对岸)时才需约翰返回接下一批,避免重复计算
    • 枚举所有分割点:对每个 \(i\) 枚举所有可能的前缀分割 \(k\),取最小值保证全局最优
    • 适用于分批运输、资源分批消耗、带固定开销的分组优化类 DP 问题

【算法标签】

普及 #线性DP-一维

【代码详解】

#include <bits/stdc++.h>
using namespace std;
const int N = 2505; // 最大奶牛数量
int n, m; // n:奶牛总数, m:约翰独自划船的基础时间
int a[N], sa[N]; // a[i]:搭载第i头奶牛额外增加的时间, sa[i]:a的前缀和
int f[N]; // f[i]:将i头奶牛运到对岸所需的最短时间

int main()
{
    cin >> n >> m; // 读入奶牛数量和基础划船时间
    for (int i=1; i<=n; i++)
    {
        cin >> a[i]; // 读入每增加一头奶牛额外增加的时间
        sa[i] = sa[i-1] + a[i]; // 计算前缀和:sa[k]表示搭载k头奶牛的总额外时间
    }

    // 初始化DP数组为无穷大
    memset(f, 0x3f, sizeof(f));
    f[0] = 0; // 边界条件:0头奶牛过河时间为0

    for (int i=1; i<=n; i++) // 外层循环:枚举已经运到对岸的奶牛总数i
    {
        for (int k=0; k<i; k++) // 内层循环:枚举前k头奶牛已经运到对岸
        {
            // 本次运输(i-k)头奶牛(第k+1到第i头)的总时间由三部分组成:
            // 1. f[k](先将前k头奶牛运到对岸的最短时间,子问题)
            // 2. sa[i-k](本次运输的(i-k)头奶牛的额外时间总和:M_1+M_2+...+M_{i-k})
            //    注意:这里的sa前缀和表示"搭载第1到第(i-k)头"的额外时间
            //    因为每批运输都是从0头开始计数额外时间的
            // 3. m(基础时间:约翰独自划船去对岸的时间)
            int tim = f[k] + sa[i-k] + m;
            // 如果前k头不是0头(即之前已经运过奶牛),需要先从对岸返回
            // 返回时间也是m(约翰独自划船回来接下一批)
            if (k>0)
                tim += m;
            // 更新最小时间
            f[i] = min(f[i], tim);
        }
    }
    cout <<f[n] << endl; // 输出将n头奶牛全部运到对岸的最短时间
    return 0;
}

【运行结果】

5 10
3
4
6
100
1
50
posted @ 2026-08-11 11:27  团爸讲算法  阅读(14)  评论(0)    收藏  举报