E. Triple Operations - 题解
比赛与标签
比赛: Codeforces Round 964 (Div. 4) 标签: dp, implementation, math 难度: *1300
喵喵,题目在说什么呀?
主人你好呀~ 这道题是说,我们有一个写着从 l
到 r
所有整数的板子。我们可以进行一种神奇的操作:从板子上挑两个数 x
和 y
,擦掉它们,然后写上 3x
和 ⌊y/3⌋
(就是 y
除以 3 向下取整啦)。
我们的目标是,用最少的操作次数,让板子上所有的数字都变成 0。题目还很贴心地告诉我们,这总是能做到的哦!我们要做的就是找出这个最少的次数,喵~
输入会给我们 t
组测试数据,每组数据有一对 l
和 r
。我们要为每一组数据输出一个答案。
解题思路大揭秘,喵~
这道题的操作看起来有点复杂,一个数变大三倍,另一个数变小三倍。直接模拟的话,选择太多了,肯定会头晕的,的说。所以,我们需要找到问题的核心规律!
关键操作的分析
我们来仔细看看这个操作 (x, y) -> (3x, ⌊y/3⌋)
。
⌊y/3⌋
是让数字变小的关键,也是我们通向0
的唯一途径。3x
则是这个操作的“副作用”,它会让一个数字变大,给我们之后的工作增加了一点点小麻烦。
如果我们能有一个 0
在板子上,事情就变得简单多啦!我们可以一直用 0
作为 x
来进行操作: (x=0, y=k) -> (3*0, ⌊k/3⌋) -> (0, ⌊k/3⌋)
看呐!这个操作可以把任意一个数 k
变成 ⌊k/3⌋
,而且我们的 0
还在,不会产生新的非零数字。这简直是完美的“消除工具”,喵!
所以,我们的整个策略可以分成两个阶段:
- 想办法搞到第一个
0
。 - 用这个
0
作为工具,把所有其他数都变成0
。
两阶段策略详解
阶段一:获得第一个 0
的“启动成本”
为了得到第一个 0
,我们必须进行 (x, y) -> (..., ⌊y/3⌋)
这样的操作,直到某个 y
变成 0
。注意到,当 y
是 0
, 1
, 或 2
时,⌊y/3⌋
就会等于 0
。所以我们的目标是先在板子上制造出一个 0
, 1
, 或 2
。
我们定义一个函数 cost(k)
,表示把数字 k
变成 0, 1, 2
中任意一个,所需要的最少操作次数。
- 如果
k
已经是1
或2
,那cost(k) = 0
。 - 否则,我们需要一次操作把
k
变成⌊k/3⌋
,然后继续解决⌊k/3⌋
的问题。所以cost(k) = 1 + cost(⌊k/3⌋)
。
为了让这个阶段的操作次数最少,我们应该选择 [l, r]
中 cost
值最小的那个数开始。因为 cost(k)
是一个随 k
增大的不减函数,所以 cost(l)
就是 [l, r]
范围里的最小值。
所以,我们需要 cost(l)
次操作,把 l
变成一个 1
或 2
(我们称之为“准零数”)。然后,再用一次操作,把这个“准零数”变成真正的 0
。 总共的“启动成本”就是 cost(l) + 1
次操作。
阶段二:用 0
清理全场
现在我们有了第一个 0
。接下来要怎么办呢?我们来思考一下,把一个数 k
完全变成 0
需要多少次 y -> ⌊y/3⌋
这样的除法操作。 我们定义另一个函数 ops(k)
:
ops(0) = 0
ops(k) = 1 + ops(⌊k/3⌋)
fork > 0
这个 ops(k)
表示把 k
彻底变成 0
需要的除法次数。
一个非常有趣的性质是:对于 x, y > 0
的操作 (x, y) -> (3x, ⌊y/3⌋)
,板子上所有数字的 ops
值之和是不变的! ops(3x) + ops(⌊y/3⌋) = (1 + ops(x)) + (ops(y) - 1) = ops(x) + ops(y)
这意味着,在第一阶段我们为了获得 0
而进行的所有 cost(l) + 1
次操作,虽然改变了板上的数字,但所有数字的 ops
值总和并没有减少!
初始时,所有数字的 ops
值总和是 Sum = Σ_{k=l to r} ops(k)
。 经过第一阶段的 cost(l) + 1
次操作后,这个总和仍然是 Sum
。
但是,一旦我们有了 0
,情况就变了! 每次我们执行 (0, k) -> (0, ⌊k/3⌋)
,ops
值总和的变化是: ops(0) + ops(⌊k/3⌋) - (ops(0) + ops(k)) = ops(k) - 1 - ops(k) = -1
总和减少了 1
!
所以,在第二阶段,我们有了一个 0
,并且板上所有数的 ops
总和为 Sum
。我们需要执行 Sum
次 (0, k)
这样的操作,才能把这个总和降为 0
,也就是让所有数都变成 0
。
最终的答案
总操作数 = 阶段一的操作数 + 阶段二的操作数 Total_Ops = (cost(l) + 1) + (Σ_{k=l to r} ops(k))
这就是我们的最终公式啦!为了快速计算,我们可以预处理出 1
到 200000
所有的 ops(k)
和 cost(k)
值,并用前缀和来快速计算 Σ ops(k)
。这样每个查询就能在 O(1) 的时间内解决了,喵~
代码魔法时间!
#include <iostream>
#include <vector>
#include <numeric>
// 设置 r 的最大值,再加一点点缓冲,喵~
const int MAX_R = 200005;
// 全局变量来存放预处理的结果,这样每次查询就不用重新算啦
std::vector<int> ops(MAX_R);
std::vector<int> cost(MAX_R);
std::vector<long long> prefix_ops(MAX_R);
// 预处理函数,在所有测试用例开始前只调用一次
void preprocess() {
// ops[i] 表示把数字 i 变成 0 需要的 y -> floor(y/3) 操作次数
// 递推关系是 ops[i] = 1 + ops[i/3]
ops[0] = 0;
for (int i = 1; i < MAX_R; ++i) {
ops[i] = 1 + ops[i / 3];
}
// cost[i] 表示把数字 i 变成 1 或 2(我们叫它“准零数”)的最小操作次数
// 递推关系是 cost[i] = 1 + cost[i/3],当 i >= 3 时
cost[0] = 0; // 基础情况,虽然 l>=1 用不到
cost[1] = 0; // 1 已经是“准零数”了
cost[2] = 0; // 2 也是“准零数”
for (int i = 3; i < MAX_R; ++i) {
cost[i] = 1 + cost[i / 3];
}
// 计算 ops 数组的前缀和,方便快速查询区间和
prefix_ops[0] = ops[0];
for (int i = 1; i < MAX_R; ++i) {
prefix_ops[i] = prefix_ops[i - 1] + ops[i];
}
}
// 解决单个测试用例的函数
void solve() {
int l, r;
std::cin >> l >> r;
// 所有初始数字 [l, r] 都变成 0 所需的 y -> floor(y/3) 操作总次数
// 用预处理的前缀和可以 O(1) 算出
long long sum_ops = prefix_ops[r] - prefix_ops[l - 1];
// 获得第一个 0 的“启动成本”是 cost(l) + 1
// cost(k) 是不减的,所以 [l, r] 中最小的 cost 就是 cost(l)
int min_c = cost[l];
// 总操作数 = 启动成本 + 清理全场的成本
long long total_ops = (long long)min_c + 1 + sum_ops;
std::cout << total_ops << "\n";
}
int main() {
// 加速输入输出,让程序跑得更快,喵~
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
// 执行一次预处理
preprocess();
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}
复杂度分析喵
时间复杂度: O(MAX_R + T) 的说。
preprocess
函数对ops
,cost
,prefix_ops
数组进行填充,每个数组大小为MAX_R
,所以预处理的时间复杂度是O(MAX_R)
。- 对于
T
个测试用例,solve
函数中的每一步都是O(1)
的查表和计算。所以处理所有测试用例的时间是O(T)
。 - 总时间就是
O(MAX_R + T)
,非常高效!
空间复杂度: O(MAX_R) 的说。
- 我们用了三个大小为
MAX_R
的数组来存储预处理的结果,所以空间复杂度是O(MAX_R)
。
- 我们用了三个大小为
知识点小鱼干~
这道题真有趣,融合了好几种思想呢!
- 问题分解: 最核心的思路就是把复杂的过程分解为“获得第一个0”和“用0清理全场”两个阶段。这种分而治之的思想在算法题里很常见哦。
- 不变量思想: 发现
Σ ops(k)
在非零操作下的不变量特性,是理解为什么需要分别计算两个阶段成本的关键。找到问题中的不变量,往往能大大简化问题! - 动态规划与预处理:
ops(k)
和cost(k)
的计算都是简单的动态规划。通过预处理,我们将每次查询的复杂度降到了常数级别,这是典型的空间换时间策略。 - 前缀和: 这是一个超级实用的小技巧,能把区间求和问题从
O(N)
优化到O(1)
,一定要掌握呀!
希望这篇题解能帮助到你,主人!解题就像吃小鱼干,找到窍门就特别香,喵~ 继续加油哦!