1 条题解
-
0
核心思路
操作只能使元素变大(因为 时 ),因此要满足 :
- 如果当前 ,我们必须对 进行若干次平方操作,使其不小于 。
- 注意,对 进行平方操作会使其变得更大,只会恶化前面已经满足的不等式,因此我们只对后面的元素进行操作,不会回头操作前面的元素。
- 另外,如果某个 ,则 ,操作无效;如果 且 ,则永远无法使 ,因为 的平方永远是 ,而 且不会变小,此时答案为 。
于是可以从左到右贪心:维护当前位置经过若干次平方后的值(实际维护的是其对数,避免数值爆炸),对于每个 (),计算需要多少次平方才能使 不小于 。
数值爆炸问题
直接计算平方会导致数字巨大(例如 远超计算机能表示的范围),但操作次数并不会很大。注意到:
- 当 时,平方一次后至少为 ,再平方一次至少为 ,增长极快。实际上,从 开始,平方 次后变为 。
- 我们最多需要对一个元素操作多少次?因为 ,而 在经过前面操作后可能变得很大(但本身也是通过平方得到的)。实际上,如果 已经很大,那么 只需要少量平方就能超过它;如果 比较小,那么 需要的平方次数也很有限。
为了安全且高效地计算,我们使用双对数()技巧。
双对数变换
定义: [ b_i = \log(\log(a_i)) ] 其中 通常取自然对数或 ,只要底数一致即可。本题中采用 会非常方便,因为平方操作对应: [ \log_2(x^2) = 2\log_2(x) ] 再取一次对数: [ \log_2(\log_2(x^2)) = \log_2(2\log_2(x)) = \log_2(2) + \log_2(\log_2(x)) = 1 + \log_2(\log_2(x)) ] 也就是说,在双对数空间(底数为 )中,一次平方操作等价于加 !
这样我们就不再需要处理巨大的数值,只需要在 上加一个常数即可。由于 ,,所以 $\log_2(\log_2(10^6)) \approx \log_2(20) \approx 4.32$,范围非常小。即使经过很多次操作, 也只会线性增长(最多加 次),完全在浮点数的精度范围内。
贪心过程
初始化 ,同时维护当前答案 。
对于 从 到 :-
若 :此时 无定义(,再取 为 )。需单独处理:
- 如果 且 ,则已经满足,不需要操作。
- 如果 且 ,那么无论对 做多少次平方, 始终 ,而 永远为 ,所以不等式自动满足,不需要操作。
- 如果 且 ,则永远无法使 变大,直接输出 。
-
一般情况 且 :
- 我们要找到最小的非负整数 ,使得对 进行 次平方后,满足 。
- 在双对数空间, 平方 次后的双对数值为 (因为每次加 )。而 的双对数值为 。不等式等价于: [ b_i + k \ge b_{i-1} ] 因此 ,但需要保证 。
- 注意,由于 本身可能已经被前面的操作修改过(实际上我们并不真正修改 ,而是在双对数空间维护它的值),但 已经反映了它经过若干次平方后的状态。因此 是我们当前维护的值,而 是原始值。我们需要将 更新为 (因为对 执行了 次平方)。
- 将 累加到答案 中。
-
特殊情况:当 但 已经在第一步判断为不可能。如果 且 ,则 和 都无法定义,但我们已经知道不需要操作,直接跳过。
-
关于精度:由于 是浮点数,直接计算 可能因浮点误差产生 的偏差。可以在计算时加一个极小量(如 )再取整,或者使用整数方法(见下文)。
整数方法(替代浮点)
如果不希望使用浮点数,也可以用整数记录“对数”或直接利用指数比较。由于操作次数很小,我们可以直接模拟平方,但需要防止溢出。一个常用技巧是:当 已经大于某个阈值(比如 )时,它的平方会爆炸,但此时它显然已经大于任何可能的前驱,所以我们可以设定一个上限,比如一旦 且 远大于 ,就不再继续平方。不过这种方法实现起来稍复杂,浮点双对数方法更简洁安全。
算法步骤总结
- 读入 ,对每个测试用例:
- 读入 和数组 。
- 预处理:如果出现 且 ,直接输出 并跳过。
- 计算 ,注意当 时,我们将其双对数视为 ,但在代码中可以用一个特殊标记(例如
-1e18)表示,并在比较时单独处理。 - 初始化 。
- 从 到 :
- 如果 ,则无需操作( 自动满足 ),且 为负无穷,跳过。
- 否则:
- 计算 ,如果 则 。
- 。
- 更新 。
- 输出 。
复杂度分析
- 每个测试用例 时间。
- 浮点运算精度足够,且 总和不超过 ,完全可行。
示例验证
以第五个例子为例:。
- 。
- $b_2 = \log_2(\log_2 3) \approx \log_2(1.585) \approx 0.664$。
- :,,更新 。
- :。
- ,,更新 。 最终答案 ,与样例一致。
参考代码
#include <bits/stdc++.h> using namespace std; const double INF = 1e18; // 用于表示 log(log(1)) = -∞ const double EPS = 1e-12; void solve() { int n; cin >> n; vector<long long> a(n); for (int i = 0; i < n; ++i) cin >> a[i]; // 特殊情况:若存在 a[i]==1 且 a[i-1]>1,永远无法使 a[i-1] <= a[i] for (int i = 1; i < n; ++i) { if (a[i] == 1 && a[i-1] > 1) { cout << "-1\n"; return; } } // 计算每个元素的双对数 b[i] = log2(log2(a[i])) vector<double> b(n); for (int i = 0; i < n; ++i) { if (a[i] == 1) b[i] = -INF; // log2(log2(1)) 无定义,用负无穷表示 else b[i] = log2(log2(a[i])); } long long ans = 0; for (int i = 1; i < n; ++i) { if (a[i-1] == 1) { // 前一个数是 1,不等式自动成立,不需要操作 continue; } // 此时 a[i-1] > 1 且 a[i] > 1(因为前面已经排除了 a[i]==1 的情况) double diff = b[i-1] - b[i]; long long k = 0; if (diff > EPS) { // 需要 ceil(diff) 次操作,注意浮点误差 k = (long long)ceil(diff - EPS); } ans += k; b[i] += k; // 更新 b[i] 为操作后的值 } cout << ans << "\n"; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int t; cin >> t; while (t--) solve(); return 0; }
注意事项
- 当 且 时无解,因为 的任何次方都是 ,无法变大。
- 当 且 时,虽然双对数无定义,但不等式自动满足。
- 浮点误差处理:计算 可以避免因舍入导致多算或少算 。
通过双对数变换,我们成功将平方操作转化为加法,从而用简单的贪心解决了问题。
- 1
信息
- ID
- 7242
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 2
- 标签
- 递交数
- 2
- 已通过
- 1
- 上传者