题解:P14535 [OII 2025] 木材运输 / Trasporto tronchi

· · 题解

[OII 2025] 木材运输 / Trasporto tronchi

题目分析

题意概述

关键洞察

  1. 顺序不变性:树无法交叉穿过,初始位置递增意味着移动过程中始终保持 A_0 < A_1 < \dots < A_{N-1} 的相对顺序。
  2. 修剪有效性:仅连续修剪的树段能产生滚动收益(非连续修剪无法形成滚动链,修剪成本无意义)。
  3. 代价计算核心
    • 未修剪树的移动代价 = 自身初始位置(需移动 A_i 步到 0)。
    • 连续修剪树段 [i..j] 的移动代价 = A_j(整个段可整体滚动,总步数等价于最右侧树的位置)。
    • 修剪收益 = (原移动成本总和 - 修剪后移动成本) - 修剪成本。

算法推导

1. 基础代价计算

所有树均不修剪时,总移动代价为 S = \sum_{i=0}^{N-1} A_i(每棵树独立移动 A_i 步)。

2. 修剪收益公式

对于连续修剪段 [i..j]

3. 前缀和优化

用前缀和数组简化求和:

代入收益公式得:

\text{gain}(i,j) = (\text{prefix}[j] - \text{prefix}[i]) - K \times (j - i + 1)

4. 收益最大化转化

拆分公式为:

\text{gain}(i,j) = \left( \text{prefix}[j] - K \times (j + 1) \right) - \left( \text{prefix}[i] - K \times i \right)

定义:

\text{gain}(i,j) = B[j] - C[i]。要最大化收益,需对每个 j 找到最小的 C[i]i \leq j),遍历过程中维护 \min_C 即可。

5. 算法步骤

  1. 计算所有树不修剪的总代价 S = \sum A_i
  2. 遍历数组,实时维护:
    • 前缀和 \text{prefix}_j(避免存储完整前缀和数组,节省空间)。
    • 最小 C[i]\min_C)。
    • 最大收益 \max_{\text{gain}}
  3. 最终最小代价 = S - \max(\max_{\text{gain}}, 0)(收益为负时不修剪,取 0)。

Code

#include <iostream>
#include <vector>

using namespace std;

long long carica(int N, int K, vector<int> A) {
    long long sum_A = 0;
    for (int x : A) {
        sum_A += x;
    }
    if (N == 0) {
        return 0;
    }
    long long prefix_j = 0;
    long long min_C = 0; // C[0] = prefix[0] - K*0 = 0
    long long max_gain = -1LL * K; // B[0] - C[0] = (0 - K*1) - 0 = -K(修剪第0棵树的收益)
    for (int j = 1; j < N; ++j) {
        prefix_j += A[j-1];
        long long current_C = prefix_j - 1LL * K * j;
        if (current_C < min_C) {
            min_C = current_C;
        }
        long long current_B = prefix_j - 1LL * K * (j + 1);
        long long current_gain = current_B - min_C;
        if (current_gain > max_gain) {
            max_gain = current_gain;
        }
    }
    max_gain = max(max_gain, 0LL); // 收益为负时不修剪
    return sum_A - max_gain;
}

// GRADER DI ESEMPIO, NON MODIFICARE

#ifndef ONLINE_JUDGE

int main() {
    int N, K;
    cin >> N >> K;

    vector<int> A(N);
    for (int &a: A) cin >> a;

    cout << carica(N, K, A) << endl;

    return 0;
}

#endif

反正我也是在附件代码上写了个函数

代码解释

核心函数 carica

  1. 基础求和:计算所有树不修剪时的总移动代价 sum_A
  2. 初始化
    • prefix_j:初始为 0(对应 prefix[0]),实时累计前 j 棵树的和。
    • min_C:初始为 C[0] = 0i=0 时的 C[i] 值)。
    • max_gain:初始为 -K(修剪单棵树 [0..0] 的收益,避免遗漏单棵树修剪的情况)。
  3. 遍历优化
    • 对每个 j(从 1 到 N-1),更新前缀和 prefix_j
    • 计算当前 C[j] 并更新 min_C(确保始终保存 i \leq j 中最小的 C[i])。
    • 计算当前 B[j] 和对应的最大收益,更新 max_gain
  4. 结果计算:收益为负时不修剪(取 max(max_gain, 0)),总代价 = 基础代价 - 最大收益。

主函数 main

复杂度分析

该算法可高效处理 N \leq 5 \times 10^5 的数据规模,无超时风险,且正确通过所有样例和测试点。