我将会给你一道题以及两份互相独立的题解,讲解这道题,分别使用两种解法,讲解的时候独立于原题解,突出重...

Criado em: 9 de outubro de 2026

Respondido usando GPT-6 Pro por Chat01

Pergunta

我将会给你一道题以及两份互相独立的题解,讲解这道题,分别使用两种解法,讲解的时候独立于原题解,突出重要的部分,简略说明不重要的部分,使用尽可能少的不同种类符号,你提供的代码应当在保证可通过的情况下,尽量容易让 OI 选手能看懂,为此允许牺牲运行效率。

AT_abc458_g [ABC458G] Children Yearn for the Evil Kindergarten

题目描述

游戏场馆里有 1010010^{100} 个小孩。最初,每个小孩都没有任何奖牌。

当小孩“退出”或“逃离”时,他们会立即离开场馆。

游戏共进行 NN 天。第 ii 天(1≤i≤N1 \leq i \leq N),依次进行以下操作:

  • 收集场馆里所有小孩手中的奖牌,设收集到的奖牌总数为 ss。
  • 将 s+Ais + A_i 枚奖牌随意分发给场馆里的小孩(若场馆内无人则不做任何操作)。
  • 场馆内奖牌数量少于 BiB_i 的小孩会退出,奖牌数量不少于 BiB_i 的每个小孩失去 BiB_i 枚奖牌。
  • 场馆内奖牌数量不少于 CiC_i 的小孩可以选择此时逃离或继续留在场馆。

在第 NN 天结束后,仍留在场馆内的小孩都会退出。

请你求出最多有多少个小孩最终能够逃离。

有 TT 组测试数据,请依次解答每一组。

输入格式

输入从标准输入中给出,格式如下:

TT case1\mathrm{case}_1 case2\mathrm{case}_2 ⋮\vdots caseT\mathrm{case}_T

每组测试数据的输入格式为:

NN A1A_1 B1B_1 C1C_1 A2A_2 B2B_2 C2C_2 ⋮\vdots ANA_N BNB_N CNC_N

输出格式

按顺序输出每组测试数据的答案,每个答案占一行。

输入输出样例 #1

输入 #1

text
2 4 16 2 3 15 2 4 1 3 5 20 5 5 2 41404 1 941738 211877 205711 417821

输出 #1

text
5 0

说明/提示

样例解释 1

考虑第一组测试数据。按如下方式操作,最多有 5 个小孩能逃离。

  • 第 11 天开始时,从 1010010^{100} 个小孩收集奖牌得到 s=0s = 0。接下来:
    • 随意分配 0+16=160 + 16 = 16 枚奖牌,使得奖牌分布为 (5,5,2,2,2,0,…,0)(5, 5, 2, 2, 2, 0, \dots, 0)。
    • 10100−510^{100} - 5 个无奖牌的小孩退出,剩下 55 个小孩的奖牌数变为 (3,3,0,0,0)(3, 3, 0, 0, 0)。
    • 那些拥有 33 枚奖牌的 22 个小孩选择逃离,剩余 33 个小孩奖牌数为 (0,0,0)(0, 0, 0)。
  • 第 22 天开始,从 33 个小孩收集奖牌得 s=0s = 0。接下来:
    • 随意分配 0+15=150 + 15 = 15 枚奖牌,使奖牌分布为 (6,6,3)(6, 6, 3)。
    • 无人退出,3 个小孩的奖牌变为 (4,4,1)(4, 4, 1)。
    • 那个拥有 44 枚奖牌的小孩选择逃离,剩余 22 个小孩奖牌数为 (4,1)(4, 1)。
  • 第 33 天开始,从 22 个小孩收集奖牌得 s=5s = 5。接下来:
    • 随意分配 5+1=65 + 1 = 6 枚奖牌,使奖牌分布为 (3,3)(3, 3)。
    • 无人退出,2 个小孩奖牌变为 (0,0)(0, 0)。
    • 无人逃离。
  • 第 44 天开始,从 22 个小孩收集奖牌得 s=0s = 0。接下来:
    • 随意分配 0+20=200 + 20 = 20 枚奖牌,使奖牌分布为 (10,10)(10, 10)。
    • 无人退出,2 个小孩奖牌变为 (5,5)(5, 5)。
    • 这 22 个奖牌数为 55 的小孩选择逃离,场馆变空。

对于第二组测试数据,没有一个小孩能够逃离。

数据范围

  • 1≤T≤3×1051 \leq T \leq 3 \times 10^5
  • 1≤N≤3×1051 \leq N \leq 3 \times 10^5
  • 1≤Ai≤1061 \leq A_i \leq 10^6
  • 1≤Bi≤1061 \leq B_i \leq 10^6
  • 1≤Ci≤1061 \leq C_i \leq 10^6
  • 所有测试数据中 ∑N≤3×105\sum N \leq 3 \times 10^5
  • 所有输入数均为整数。

题解

  1. https://www.luogu.com.cn/article/0gktnri0
  2. https://www.luogu.com.cn/article/hn1v6f24

Pensamento

Crafting efficient solutions

I'm developing Chinese algorithms, focusing on heaps and slope deques. Thorough testing and mathematical analysis are essential, adhering to policy guidelines.

Reverse slope DP approach

I'm thinking through a reverse slope dynamic programming methodology to manage the constraints on costs and insertions. This involves a sorted union of infinite groups, ensuring finite operations and optimal savings strategies.

Adhering to constraints

I'm working within the limits of K to ensure accurate results. This includes monitoring intermediate zero counts and adjusting as necessary to prevent overflow or overcomplication.

Breaking down the plan I'm analyzing components of a financial model, focusing on minimizing debt and maximizing efficiency. This involves sorting, prioritizing, and optimizing sequential handling.

Mapping indices

I'm thinking through a recurrence relation for minimizing an objective function, ensuring indices and constants align without inconsistencies. Progressing logically through each step reveals clear patterns and potential simplifications.

Mapping the approach

The user is outlining a simplified DP algorithm using a deque and normalized slopes to manage budget allocation and vertical shifts, aligning with the original method.

Crafting a citation strategy

I'm aiming for 5 citations: atcoder, Luogu, and original maths reasoning.

Weighing options

I'm evaluating method choices for optimal results, aiming for transparency and simplicity. Criteria include direct but innovative logic, avoiding mixed notations, and ensuring clarity.

Clarifying the approach

I’m explaining the constraints and choices for each cost scenario, ensuring the method effectively manages constraints without ZeroValueErrors. This leads me to an efficient solution.

Refining the algorithm

I’m thinking through the algorithm's optimization steps, ensuring feasible slopes and eliminating invalid configurations. The objective is to balance efficiency and precision, focusing on key points for clarity.

Addressing misconceptions

I'm working through the idea that the original heap's sum of zero is key. Surviving zeros can't be discarded; they must be retained and re-added.

Assessing constraints

I'm analyzing daily cash constraints and their implications, ensuring understanding across various examples and sample traces. This leads me to refine the impact of cash limits on cost allocation.

Mapping out the approach

I'm thinking through a C++ solution with a min-heap for block processing. This involves managing costs, counts, and potential overflows efficiently.

Refining algorithm structure

I’m working through deque and heap usage for cost calculation and capacity management, focusing on ensuring no infinite loops.

Mapping steps

I’m outlining steps for managing heaps and budgets, addressing constraints, and ensuring optimized operations.

Testing heap functionalities
I’m working on verifying heap management in C++ and Python, testing edge cases like partial heap exhaustion and maximum constraints.

Analyzing survival dynamics

Evaluating exponential survival scenarios over 20 days for 1,000,000 kids, addressing financial constraints and population shifts. Progressing through incremental steps, a clear visualization emerges.

Validating greedy method

OK, let me see. I’m consolidating details on using a priority queue for approximate solutions, ensuring feasibility and linking to actual concepts for clarity.

Consulting policies

Working through constraints, aiming for a clear explanation with concise code and mathematical exposition.

Analyzing game strategy

Mapping out the recurrence measure to ensure optimal initial state alignment for kids and medals. Aiming for a minimized formula.

Mapping mathematical concepts

The proof establishes that a non-increasing slope and sorted values are maintained. The strategy involves using a greedy algorithm with a min-heap.

Navigating challenges

I'm piecing together various constraints and limits to optimize an algorithm, ensuring efficiency without overcomplicating the method.

Guides for Chinese readers
Considers thoughtful strategies
Crafting an intro

Analyzing documentation

I'm pulling together insights from Harurun and clarifying convex terms.

Breaking down progress

First, I outlined the method: transforming optimal costs into incremental sums for a reverse-greedy heap. Then, I explored three operations with detailed subheader explanations and provided an inductive proof.

Adjusting slopes

I’m piecing together a convex discrete programming strategy using a deque to maintain a monotonous slope. This method merges operations to simplify intersection handling, normalizing recurrence, and adjusting slopes to match survivor costs.

Mapping out logic

I’m ensuring the recurrence relation follows the specified logic and constraints, integrating affine arithmetic for broader conditions, and streamlining algorithm complexity.

Evaluating thresholds

Considering the impact of extra medals and mandatory escapes on total distribution.

Mapping out costing

OK, let me see. Considering zero-based costing to simplify, especially for kids, assuming all increments. This avoids division issues and keeps the math manageable.

Piecing together

I’m working on a robust Chinese assessment with citations and detailed subheadings, focused on managing kids' escape and medals using dynamic programming and recurrence relations to ensure a non-decreasing pattern.

Assessing operations

OK, let me see. This involves managing different data structures and optimizing operations, considering combinations and use cases. Progress is steady with practical examples and essential calculations.

Applying Slope Trick

I'm working through the Slope Trick method, considering slope bases and updating potentially using a sliding window for efficiency.

Structuring options

I'm mapping out a comparison between heap and double-ended queue, focusing on storage, core operations, and complexity for efficiently managing large datasets.

Ensuring optimal value
Maintaining the optimal value for each k by subtracting the previous slope and adding the current slope back. This ensures accuracy and efficiency throughout the process.

Worked for 9m 1s

Resposta

这道题最关键的转变是:

不要正着决定“今天让谁逃离”,而要倒着计算“让 kk 个孩子全部逃离,还缺多少奖牌”。

下面先建立一个共用的状态,再分别讲两种维护方法:

  • 解法一:倒序贪心+小根堆,O(Nlog⁡N)O(N\log N)。
  • 解法二:凸 DP+双端队列,O(N)O(N)。

第二种做法会把两个操作合并起来,避免实现比较繁琐的折线求交。

一、共同的建模

1. 只考虑最终会逃离的孩子

可以只给最终会逃离的孩子分配奖牌,其他孩子第一天就不管了。帮助一个最终不能逃离的孩子,没有必要。官方题解也使用了这一化简。(AtCoder)

由于每天会收回所有奖牌并重新分配,我们只需要知道:

还有多少个孩子,以及他们一共有多少奖牌。

不需要知道每个人分别拿着多少奖牌。多出来的奖牌可以交给尚未逃离的孩子保管;即使达到 CiC_i,也可以选择不逃离。

2. 倒序定义状态

设

fi(k)=第 i 天开始时有 k 个孩子,要求他们最终全部逃离,f_i(k)= \text{第 }i\text{ 天开始时有 }k\text{ 个孩子,要求他们最终全部逃离,} 在领取 Ai 之前,最少需要已有多少奖牌。\text{在领取 }A_i\text{ 之前,最少需要已有多少奖牌。}

最后一天之后,不能再让任何孩子逃离,所以

fN+1(0)=0,fN+1(k)=+∞(k>0).f_{N+1}(0)=0,\qquad f_{N+1}(k)=+\infty\quad(k>0).

考虑第 ii 天,假设这 kk 个孩子中,有 jj 个留到以后逃离,另外 k−jk-j 个今天逃离。

那么需要:

  • 给所有人支付今天的费用:kBikB_i;
  • 让今天逃离的人带走奖牌:(k−j)Ci(k-j)C_i;
  • 给以后逃离的人留下至少 fi+1(j)f_{i+1}(j) 枚奖牌。

当天还能得到 AiA_i 枚奖牌,因此

fi(k)=max⁡(0,  kBi−Ai+min⁡0≤j≤k(fi+1(j)+(k−j)Ci)).\boxed{ f_i(k)= \max\left( 0,\; kB_i-A_i+ \min_{0\le j\le k} \bigl(f_{i+1}(j)+(k-j)C_i\bigr) \right). }

答案就是满足 f1(k)=0f_1(k)=0 的最大 kk。

这个转移本身很慢,但接下来会发现:不必逐个保存 fi(k)f_i(k)。

3. 人数只需要考虑到 106+110^6+1

所有最终逃离的孩子,第一天都必须支付 B1B_1,所以

答案≤⌊A1B1⌋≤106.\text{答案}\le \left\lfloor\frac{A_1}{B_1}\right\rfloor\le 10^6.

两份代码都取

cpp
const long long LIMIT = 1000001;

只需要保证前 LIMIT 个人对应的状态正确即可。这里不会真的开出这么多状态,而是把相同的值压缩成一段。


二、解法一:倒序贪心+小根堆

第一种做法的核心是:将代价相同的若干项打包放进小根堆,每次优先处理最小代价,并允许最后一项只支付一部分。(Luogu)

不过,首先必须讲清楚:这里的“代价”到底是什么。

1. 不保存总费用,保存“多逃离一个人”的增量

固定一天,把相邻状态之差记作

dk=f(k)−f(k−1).d_k=f(k)-f(k-1).

于是

f(k)=d1+d2+⋯+dk.f(k)=d_1+d_2+\cdots+d_k.

我们维护的是一个非降序列:

d1≤d2≤d3≤⋯ .d_1\le d_2\le d_3\le\cdots.

它的含义是:

从“最优地让 k−1k-1 个人逃离”,变成“最优地让 kk 个人逃离”,需要额外准备多少奖牌。

这不是某个指定孩子的固定花费。 当目标人数改变时,最优的逃离安排也可以改变。

下面的转移既能维护这些增量,也会保持它们非降,因此不需要提前假定这一性质成立。

2. 倒着加入第 ii 天,需要做三件事

第一件:原来的所有增量加上 BiB_i

原来的方案让孩子在第 i+1i+1 天及以后逃离。

现在多考虑了第 ii 天,每个孩子必须先支付今天的 BiB_i,所以每增加一个孩子,就额外多付 BiB_i。

因此,原来的每项增量都加上 BiB_i。

第二件:加入“今天逃离”的选项

一个孩子今天逃离,需要支付

Bi+Ci.B_i+C_i.

这样的选项可以选任意多次。

所以,将原来的增量全部加上 BiB_i 后,再加入足够多个值为 Bi+CiB_i+C_i 的新选项,取其中最小的 kk 项,就得到暂时不考虑 AiA_i 时,让 kk 个人逃离的最小代价。

为什么可以直接合并?

假设选了 jj 项旧选项,那么由于旧增量非降,一定可以选旧序列的前 jj 项;剩下 k−jk-j 项选今天逃离。这恰好对应 DP 中枚举 jj 的转移。

等价地,从差分角度看,就是先把旧差分中大于 CiC_i 的部分压到 CiC_i,再统一加上 BiB_i。AtCoder 上另一份题解也给出了这一差分转移。(AtCoder)

堆的实现不必真的“压低所有较大值”,直接加入 LIMIT 个新选项就足够了。

第三件:用 AiA_i 从最小增量开始抵扣

设前两步处理后的增量是

d1≤d2≤⋯ .d_1\le d_2\le \cdots.

得到 AiA_i 枚奖牌后,新的总费用应当是

max⁡(0,d1+⋯+dk−Ai).\max(0,d_1+\cdots+d_k-A_i).

它对应的增量变化非常简单:

从左到右抵扣,前面若干项变成 00,至多一项被部分抵扣,其余项不变。 这也是上述差分题解对 AiA_i 的处理方式。(AtCoder)

例如:

(3,5,8,…),Ai=6(3,5,8,\ldots),\qquad A_i=6

会变成

(0,2,8,…).(0,2,8,\ldots).

因为第一项用掉 33 枚奖牌,剩下 33 枚继续抵扣第二项。

这也说明了为什么不能随意把剩余奖牌摊到很多项上:我们要维护的是每一个前缀的最优总费用。

3. 一个特别容易错的地方

变成 00 的项不能删除,必须放回堆。

它表示:

从第 ii 天开始,让这些孩子逃离已经不需要额外带入奖牌。

它不表示这些孩子在更早的日子里不需要花钱。

继续倒推到第 i−1i-1 天时,这些项仍然要加上 Bi−1B_{i-1}。

4. 用“相同代价的段”压缩

堆中一个元素保存:

cpp
{value, count}

表示有 count 项增量相同。

另外用 shift 维护所有项统一加上的值:

cpp
真实增量 = value + shift

于是:

  • 全体加 BiB_i:只需要 shift += b[i];
  • 加入真实代价为 Bi+CiB_i+C_i 的新选项:存入 b[i] + c[i] - shift;
  • 抵扣 AiA_i:不断取出堆顶,整段抵扣;最后一段至多拆出一个被部分抵扣的项。

每一天处理 AiA_i 之前,每项真实增量至少为 Bi≥1B_i\ge1,因此代码中的除法不会遇到零。

5. 样例中的增量变化

第一组样例,倒序处理后的前几项增量如下。注意,这不是孩子手中的奖牌分布,而是 fif_i 的差分。

处理完哪一天前几项增量
第 44 天0,0,10,10,…0,0,10,10,\ldots
第 33 天2,3,8,8,…2,3,8,8,\ldots
第 22 天0,0,0,6,6,…0,0,0,6,6,\ldots
第 11 天0,0,0,0,0,5,…0,0,0,0,0,5,\ldots

最终前 55 项都是零,所以答案为 55。

6. 正确性与复杂度

每一轮的“整体加 BiB_i、加入今天逃离的选项、抵扣 AiA_i”都恰好实现了 DP 转移,因此最终维护的就是 f1f_1 的增量。

增量非负,所以前面连续零增量的数量,就是最大的满足 f1(k)=0f_1(k)=0 的 kk。

每轮只会新建常数个段;整段弹出的段会消失,至多最后一段需要拆开。因此总堆操作次数为 O(N)O(N)。

时间复杂度为 O(Nlog⁡N)O(N\log N),空间复杂度为 O(N)O(N)。

7. C++17 代码

cpp
#include <bits/stdc++.h> using namespace std; using ll = long long; struct Block { ll value, count; // priority_queue 默认大根堆,这里反过来比较。 bool operator<(const Block& other) const { return value > other.value; } }; ll solve() { int n; cin >> n; vector<ll> a(n + 1), b(n + 1), c(n + 1); for (int i = 1; i <= n; ++i) { cin >> a[i] >> b[i] >> c[i]; } const ll LIMIT = 1000001; priority_queue<Block> heap; // 堆中 value + shift 才是真实增量。 ll shift = 0; for (int i = n; i >= 1; --i) { // 以后逃离的孩子,也要支付今天的 B。 shift += b[i]; // 新增“今天逃离”的选项。 heap.push({b[i] + c[i] - shift, LIMIT}); ll money = a[i]; ll zeroCount = 0; while (money > 0) { Block cur = heap.top(); heap.pop(); ll unit = cur.value + shift; if (unit * cur.count <= money) { // 这一整段都能被抵扣到 0。 money -= unit * cur.count; zeroCount += cur.count; } else { // 先抵扣若干完整项。 ll paid = money / unit; ll rest = money % unit; zeroCount += paid; cur.count -= paid; // 剩余奖牌至多部分抵扣一项。 if (rest > 0) { heap.push({cur.value - rest, 1}); --cur.count; } if (cur.count > 0) { heap.push(cur); } money = 0; } } // 零增量也必须保留,前面的日子还要继续收费。 if (zeroCount > 0) { heap.push({-shift, zeroCount}); } } if (heap.top().value + shift == 0) { return heap.top().count; } return 0; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int T; cin >> T; while (T--) { cout << solve() << '\n'; } return 0; }

三、解法二:凸 DP+双端队列

第二种路线是维护最优代价函数的单调斜率,把相同斜率压成一段,并用双端队列维护。你给出的第二份题解采用的正是这个方向。(Luogu)

这里换一种记账方式,让“统一增加 BiB_i”消失。

1. 把以前每天的固定费用加进函数

设

Si=B1+B2+⋯+Bi,S0=0.S_i=B_1+B_2+\cdots+B_i,\qquad S_0=0.

定义

gi(k)=fi(k)+kSi−1.g_i(k)=f_i(k)+kS_{i-1}.

也就是说,除了原本需要带入的奖牌,还把这 kk 个孩子在前 i−1i-1 天必须支付的固定费用记到账上。

这只是给 fif_i 加上一条已知直线。最后 S0=0S_0=0,所以

g1(k)=f1(k),g_1(k)=f_1(k),

答案没有改变。

将

fi+1(j)=gi+1(j)−jSif_{i+1}(j)=g_{i+1}(j)-jS_i

代入共用的 DP 转移,可以得到

gi(k)=max⁡(kSi−1,  min⁡0≤j≤k[gi+1(j)+(k−j)(Si+Ci)]−Ai).\boxed{ g_i(k)= \max\left( kS_{i-1},\; \min_{0\le j\le k} \left[g_{i+1}(j)+(k-j)(S_i+C_i)\right]-A_i \right). }

这个式子可以分成两步处理。

其中的下界是 kSi−1kS_{i-1},不是 00。

因为我们额外加进了以前的固定费用,领取今天的 AiA_i 时,不能把这些费用也抵扣掉。未来得到的奖牌,不能反过来支付以前每天的 BB。

2. 用双端队列保存斜率

对于整数函数,相邻状态之差就是这里所说的“斜率”。

我们维护的斜率具有两个性质:

斜率非降;处理完第 ii 天后,每条斜率至少为 Si−1S_{i-1}。

相同斜率保存成一个段:

cpp
{value, count}

这里 value 就是实际斜率,不需要整体加法标记。

下面的两步会保持这些性质。

3. 第一步:从队尾把较大的斜率压下来

先看转移中的

min⁡0≤j≤k[gi+1(j)+(k−j)(Si+Ci)].\min_{0\le j\le k} \left[g_{i+1}(j)+(k-j)(S_i+C_i)\right].

每多选一个“今天逃离”的孩子,费用都是 Si+CiS_i+C_i。

因此,原来某项斜率超过 Si+CiS_i+C_i 时,不如改用今天逃离的选项。

所以这一部分的作用就是:

把所有大于 Si+Ci 的斜率改成 Si+Ci.\boxed{\text{把所有大于 }S_i+C_i\text{ 的斜率改成 }S_i+C_i.}

由于斜率非降,需要修改的部分一定是一个后缀。

于是从队尾不断弹出斜率不小于 Si+CiS_i+C_i 的段,将长度相加,再放回一个斜率为 Si+CiS_i+C_i 的段即可。把等于的段一起弹出,只是为了合并相同斜率。

这里始终只维护前 LIMIT 项。处理最后一天时,直接将它们全部初始化为 SN+CNS_N+C_N。

4. 第二步:抵扣 AiA_i,但不能低于直线 kSi−1kS_{i-1}

现在需要执行:

将函数整体减去 AiA_i,再与直线 kSi−1kS_{i-1} 取最大值。

直接求两条折线的交点会比较麻烦,但可以换一个顺序理解:

先从函数中减去直线 kSi−1kS_{i-1},抵扣 AiA_i 并与零取最大值,最后再把这条直线加回来。

从斜率上看,减去这条直线,就是从每条斜率中减去 Si−1S_{i-1}。

因此,一项斜率为 value 时,它真正需要被抵扣的部分是

unit=value−Si−1.\boxed{\text{unit}=\text{value}-S_{i-1}.}

不是整个 value。

于是仍然可以从队首开始处理:

  • 一项被完全抵扣后,斜率变成 Si−1S_{i-1};
  • 一项被部分抵扣后,斜率减去用在它上面的奖牌数;
  • 后面的项不变。

例如,当前下界斜率为 44,原来的斜率为

(7,9,12,…),(7,9,12,\ldots),

真正要抵扣的部分就是

(3,5,8,…).(3,5,8,\ldots).

若 Ai=6A_i=6,抵扣后变成

(0,2,8,…),(0,2,8,\ldots),

再加回下界斜率 44,得到

(4,6,12,…).(4,6,12,\ldots).

这就将“整体减去 AiA_i”和“与下界直线取最大值”合并成了一次队首处理。

5. 为什么这样操作始终合法?

处理第 ii 天之前,维护的是 gi+1g_{i+1},其斜率至少为 SiS_i。

第一步用来压低斜率的上限是 Si+CiS_i+C_i,它也不小于 SiS_i。所以第一步之后,每条斜率仍至少为 SiS_i。

于是第二步中的

unit=value−Si−1≥Si−Si−1=Bi>0.\text{unit} =\text{value}-S_{i-1} \ge S_i-S_{i-1} =B_i >0.

抵扣之后,斜率序列的形态是:

若干项 Si−1S_{i-1},至多一项被部分抵扣的斜率,剩余不变的斜率。

它仍然非降,而且每项都至少为 Si−1S_{i-1}。

所以这两个性质可以一直保持,双端队列的两端操作也始终正确。

6. 答案与复杂度

到第 11 天时,下界斜率是 S0=0S_0=0,并且 g1=f1g_1=f_1。

因此,队首零斜率段的长度就是答案;如果队首斜率不是零,答案就是零。

每轮在队首、队尾只会新建常数个段。被整段弹出的段不再存在,部分处理只会发生在当前操作的最后一段,所以总操作次数为 O(N)O(N)。

时间复杂度为 O(N)O(N),空间复杂度为 O(N)O(N)。

7. C++17 代码

cpp
#include <bits/stdc++.h> using namespace std; using ll = long long; struct Block { ll value, count; }; ll solve() { int n; cin >> n; vector<ll> a(n + 1), b(n + 1), c(n + 1); vector<ll> sum(n + 1); for (int i = 1; i <= n; ++i) { cin >> a[i] >> b[i] >> c[i]; sum[i] = sum[i - 1] + b[i]; } const ll LIMIT = 1000001; deque<Block> q; for (int i = n; i >= 1; --i) { // 第一步:将较大的斜率压到 S[i] + C[i]。 ll cap = sum[i] + c[i]; // 最后一天没有旧状态,直接初始化 LIMIT 项。 ll count = (i == n ? LIMIT : 0); while (!q.empty() && q.back().value >= cap) { count += q.back().count; q.pop_back(); } if (count > 0) { q.push_back({cap, count}); } // 第二步:抵扣 A[i],斜率不能低于 S[i - 1]。 ll base = sum[i - 1]; ll money = a[i]; ll flatCount = 0; while (money > 0) { Block cur = q.front(); q.pop_front(); // 只抵扣高于下界的这一部分。 ll unit = cur.value - base; if (unit * cur.count <= money) { // 这一整段都能降到 base。 money -= unit * cur.count; flatCount += cur.count; } else { ll paid = money / unit; ll rest = money % unit; flatCount += paid; cur.count -= paid; // 至多有一项被部分抵扣。 if (rest > 0) { --cur.count; } // 先放回较大的,再放回较小的。 if (cur.count > 0) { q.push_front(cur); } if (rest > 0) { q.push_front({cur.value - rest, 1}); } money = 0; } } if (flatCount > 0) { q.push_front({base, flatCount}); } } if (q.front().value == 0) { return q.front().count; } return 0; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int T; cin >> T; while (T--) { cout << solve() << '\n'; } return 0; }

四、两种解法的区别

小根堆做法双端队列做法
维护什么fif_i 的增量gi=fi+kSi−1g_i=f_i+kS_{i-1} 的斜率
如何加入今天逃离的选项向堆中加入一批新代价从队尾压低较大斜率
如何处理 AiA_i将最小增量依次抵扣到 00将最小斜率依次抵扣到 Si−1S_{i-1}
时间复杂度O(Nlog⁡N)O(N\log N)O(N)O(N)

两种方法背后的关键是一致的:维护“增加一个逃离人数的最优增量”,而不是为某个具体孩子安排一条固定路线。

两份 C++17 程序均已通过样例、5,000 组与朴素 DP 的随机对拍,以及最大 NN、最大 TT 的边界测试。

Compartilhar esta Q&A