目录
暴力先行
题目比较长,先写个暴力模拟来理解题目(10分钟)
#include <iostream>
#include <vector>
#include <algorithm> // 用于 max_element
#include <cmath> // 用于 floor
using namespace std;
int main() {
// 优化一下输入输出,虽然暴力算法本身慢,但输入习惯要好
ios::sync_with_stdio(false);
cin.tie(NULL);
int n, m, q, u, v, t;
cin >> n >> m >> q >> u >> v >> t;
// 使用 vector 存储当前所有的蚯蚓长度
vector<int> worms;
for (int i = 0; i < n; i++) {
int temp;
cin >> temp;
worms.push_back(temp);
}
// --- 模拟开始,循环 m 秒 ---
for (int i = 1; i <= m; i++) {
// [步骤 1: 找出最大的蚯蚓]
// max_element 会遍历整个 vector 找到最大值的迭代器
auto max_it = max_element(worms.begin(), worms.end());
int max_val = *max_it; // 取出最大值的数值
// [步骤 2: 将其从队伍中取出/移除]
// 这一步非常慢,因为 vector 删除元素需要移动后面的所有数据
worms.erase(max_it);
// 按题目要求,每隔 t 秒输出一次被切断蚯蚓的长度
if (i % t == 0) {
cout << max_val << " ";
}
// [步骤 3: 这里的代码完成了 "其余蚯蚓增加 q"]
// 注意:此时最大的那只已经被 erase 移除了,新的两只还没放进去。
// 所以现在 vector 里剩下的,就是所有 "其余的蚯蚓"。
for (int &w : worms) {
w += q;
}
// [步骤 4: 切割产生两只新蚯蚓]
// 计算长度,注意整数运算
// 题目公式:长度1 = floor(x * u / v)
long long len1_long = (long long)max_val * u / v;
int len1 = (int)len1_long;
int len2 = max_val - len1;
// [步骤 5: 将新蚯蚓放回队伍]
// 题目说:新产生的两只蚯蚓这一秒不增加长度,所以它们是在上面的 for 循环之后加入的
worms.push_back(len1);
worms.push_back(len2);
}
cout << endl; // 第一行输出结束
// [步骤 6: 输出最后的结果]
// 题目要求输出第 m 秒结束后,所有蚯蚓长度(从大到小)
// 暴力解法直接排序即可
sort(worms.begin(), worms.end(), greater<int>());
for (int i = 0; i < worms.size(); i++) {
if ((i + 1) % t == 0) {
cout << worms[i] << " ";
}
}
cout << endl;
return 0;
}
题目解析
这是一道非常经典的NOIP提高组题目: [NOIP 2016 提高组] 蚯蚓,它考察了对数据结构的优化以及对题目性质(单调性)的挖掘。
1. 朴素思路(只拿部分分)
题目要求每次取出最大的蚯蚓进行切割,并将其他蚯蚓长度增加
最直观的想法是使用 优先队列(大根堆)。
- 每次从堆顶取出最大值
。 - 切割成
和 。 - 关于“其余蚯蚓增加
”的处理: - 如果每次都遍历堆中所有元素加
,时间复杂度爆炸。 - 我们可以维护一个全局变量
delta,表示累加增长量。 - 放入堆时,将数值减去当前的
delta;取出时,将数值加上delta还原为真实长度。
- 如果每次都遍历堆中所有元素加
- 复杂度分析:
- 操作次数
最大为 。 - 堆的插入删除是
。总复杂度 。 - 算一下:
。 - 对于 1秒的时限来说,这个计算量非常危险,可能会 TLE(超时),只能拿 60~85 分左右。
- 操作次数
2. 正解思路(利用单调性)
为了拿到 100 分,我们需要
关键性质:
如果在
如果在
那么一定满足:先切出来的部分
(即
推导结论:
因为切分出来的两段长度随时间推移是单调递减的(相对于产生的时刻),我们可以不用堆,而是用 3个普通的队列 来维护:
:存储初始给定的 条蚯蚓(先从大到小排好序)。 :存储切分出来的左半段长度( )。 :存储切分出来的右半段长度( )。
由于单调性,
每次最大的蚯蚓,一定就在这三个队列的队头产生。
3. 算法流程
- 将初始蚯蚓排序,放入
。 - 循环
次(时间 ): - 比较
三个队列的队头元素,取出最大的那个(记为 )。 - 还原真实长度:
。 - 按要求输出。
- 计算切分长度
, 。 - 将
和 减去 当前的总增长量 ,分别放入 和 的队尾(为了统一处理增长,新来的要“亏”掉之前的增长)。
- 比较
- 循环结束后,将三个队列剩余的元素合并,排序(或者直接利用归并排序的思想输出),得到最终结果。
易错点解析
- 数据类型:题目中
最大 ,经过 秒,每秒长 200,最大长度可能达到 。虽然 int范围是勉强够用,但在计算 real_len * u时,如果很大, 必定溢出 int,所以中间计算必须强制转为long long。 - 相对长度技巧:这是处理“全员加减”的经典技巧。记录一个偏移量
,入队时存 val - offset,出队时用val + offset。 - 空行输出:题目要求“即使某一行没有任何数需要输出,你也应输出一个空行”。
cout << "\n"必须要有,否则可能格式错误。 - 手动队列:由于
很大,STL 的 queue涉及大量的动态内存分配,可能导致常数过大超时或内存碎片。使用数组模拟队列q[head], q[tail]是最稳妥的。
这张图展示了三个队列如何像三条传送带一样,每次只需看三个“出口”处的最大值,即可保证全局最大,这就是
我的思考🤔
上面是解析,下面是我的思考
我觉得这个题目 考查点,或者,思考点
根据写题目的经验,这个题目
- 本质是一个枚举优化,优化点有一下几个
- 每个时刻长度都会增加 q, 不能每个蚯蚓枚举得去增加
- 快速的找到集合里面的最值
- 方法1: 堆
- 方法2: 单调性: 多路归并
核心: 数学思路
- 每个时刻长度都会增加 q 的问题: 利用 加法 的负元性质, 去除 deta 的影响, 这个你来写一个详细的数学证明
- 先切出来的部分
后切出来的部分, 这个不容易想到,你来写一下, 普通人如何思考(观察,思维跳跃)到这里. 加上 详细的数学证明
上面的两个思路, 都用到了重要的数学思路: 数学归纳法(递归), 只要证明 t 到 t+1 的时刻是对的,任意相邻的时刻是对,则在整个时刻都是对的
证明
一、 “全局加 q”的数学证明:利用加法的逆元性质
核心思想:我们利用了相对论的思想。如果每个人都长高了,等于没人长高而坐标系降低了。
1. 问题定义
我们需要维护一个集合
- Extract: 取出最大值
。 - Global Add: 集合中所有剩下的元素
变为 。
2. 朴素操作 vs. 优化操作
假设第
对于一个在时刻
也就是:
3. 变换公式(利用逆元)
我们展开上面的公式:
这里我们发现了一个变量分离的技巧:
:这一部分只和进入集合的时间有关,进入后是一个常数。 :这一部分只和当前的时刻有关,对集合里所有元素都是一样的。
结论:
我们不需要更新集合里的每一个数。
- 入队时:存入
。 - 出队时:读出
。
这在数学群论中,利用了加法群的逆元性质:为了抵消未来的增长,我们先减去当前的累积量(预支亏损),未来再加上累积量时,差值正好就是这段时间的增长。
二、 单调性是如何被“普通人”发现的?(观察与思维跳跃)
你问到普通人如何从朴素思路跳跃到“单调性”?通常这不是灵光一闪,而是通过小数据模拟和极端情况分析得到的。
1. 直觉来源:通胀模型
想象“长度”是钱,“增长
- 早切(早分家):你手里的大钱马上变成了两笔小钱。但是,这两笔小钱从下一秒开始,每一笔都能领到完整的福利
。 - 晚切(晚分家):你死守着大钱,虽然大钱本身领了一份福利
,但当你晚一点被“切”的时候,这一份福利 也被切成了两半(比如乘上 )。
直观感受:早切的人,虽然本金变小了,但享受福利的“份数”变多了(一份变两份,每份都拿
2. 观察步骤(模拟)
假设
- A蚯蚓(长度 100):现在切
变成 50, 50。 - 过 1 秒后,它们变成:60, 60。
- B蚯蚓(长度 98):现在不切,过 1 秒切。
- 过 1 秒后,B 先长到 98+10 = 108。
- 然后切
变成 54, 54。
发现规律:
虽然 A (100) 和 B (98) 只差 2,但 A 早切了一秒,结果 A 的后代 (60) 比 B 的后代 (54) 大了很多。
这就验证了猜想:先切出来的,一定比后切出来的长。
直觉模型:分家产模型
早分家 vs 晚分家
-
早分家(切
): 大家立刻独立。你分到的小家产,下一秒能完整领到一份新的补助
。 收益 = 完整补助
-
晚分家(切
): 大家先不分,作为大家庭领了一份补助
。 下一秒分家时,这一份补助也被劈成了两半(乘
)。你只能分到残缺的补助 。 收益 = 打折补助
结论:早分家的人,领到的补助多。
极简算式推导流
设定:当前最大值
已知:
路径 A:早切
-
切:变
-
长:变
结果 A
路径 B:晚切
-
长:变
-
切:变
结果 B
PK (做差法)
最终判决
证毕。
三、 单调性的严格数学证明
这里我们需要证明:对于从队列中先后取出的两个最大值
设定变量
时刻,取出最大值 。 时刻,取出剩下的最大值 (注意:这个 在 时刻就在队伍里,或者它也是刚被切出来的,但由于我们前提是“有序队列”,我们总能保证 。如果是 在 时刻还没生成,那它自然更小,不用证)。 - 切割比例参数为
( )。 - 每秒增长量为
。
证明目标
我们需要证明切出来的较长那一段满足单调性(较短那一段同理)。
-
的轨迹: 在
时刻被切,较长一段长度为 。 到了
时刻,它长了 ,长度变为: -
的轨迹: 在
时刻没被切,先长了 ,长度变成 。 在
时刻被切,较长一段长度变为:
求证:
证明过程
利用取整函数的性质:
我们比较不等式左右两边:
左边
右边
步骤 1:去除取整符号进行放缩(最坏情况分析)
为了证明
考虑
由于
我们展开右边:
因为
让我们做一下差值分析:
利用性质
现在比较
我们要证:
分析这一步:
:因为 且 ,所以这一项 。 :因为 且 (切割比例通常小于1),所以这一项 。
结论:
这意味着
关于取整符号的微小扰动:
虽然核心值满足
但在本题(蚯蚓)中,由于
即便在极端情况下(例如
由于
一个数加上
只有当
四、 总结
你提到了数学归纳法,这正是这道题解法成立的基石:
- 基础步 (Base Case):初始状态下,原始数组
是有序的(我们排了序)。 - 归纳步 (Inductive Step):
- 假设在
时刻, 都是有序的。 - 我们证明了:在
时刻从任意队列头取出的最大值 ,经过切割运算后,生成的新值 ,一定会小于等于之前时刻放入 的队尾元素(因为之前放入的元素是由更大的“祖先”生成的,且经历了更长时间的 )。 - 同时,这一刻生成的
会大于等于未来 时刻生成的 (刚刚证明的单调性)。 - 因此,
和 始终保持单调递减。
- 假设在
这就是为什么我们可以把优先队列
希望这个深度的数学解析能满足你的好奇心!继续保持这种对“数学本质”的探究,这对学习算法非常重要。