[题解] 2024 ICPC 武汉邀请赛 D. 贪心与质因数
[题解] 2024 ICPC 武汉邀请赛 D. 贪心与质因数
TAG: 贪心, 数理推导, 数论
题目链接: 点击此处打开题目
题目大意
给定一个整数 \(N\)。每一次操作中,你可以选取一个整数 \(y \in [N, 2N-1]\),然后将 \(N\) 取值为 \(\frac{y}{\text{gpf}(y)}\),其中 \(\text{gpf}(y)\) 是 \(y\) 的最大质因数。
当前目标是在每一步中采取“贪心”策略:使得每次变化后的新数尽量大。基于此确定性的最优化贪心策略,求出将 \(N\) 变小到 \(1\) 需要经历多少次操作。
(注:数据范围达到 \(10^{18}\),对于这种体量的数据运算极易在中途超出界限,对应代码中运用了 __int128_t 进行防越界保护)。
核心思路
直观分析可知,要在区间 \([N, 2N-1]\) 中找到一个 \(y\),使得除以其最大质因数后的商 \(\frac{y}{\text{gpf}(y)}\) 最大,这直接要求其最大质因数本身要尽可能地小。顺着这条线索,我们要证明只需考虑最大质因数为 2 和 3 的情况即可。
推导与证明全过程:
- 区间 \([N, 2N-1]\) 的长度为 \(N\)。对于任意正整数 \(N\),该区间内必定包含至少一个 \(2\) 的自然数次幂(若 \(N\) 恰好不是 \(2\) 的幂,则中间必跨过一个;若 \(N\) 是,则端点本身即是)。
- 设这个位于区间内且仅有质因数 2 的数为 \(y = 2^j\),显然它的最大质因数毫无争议是 2。如果我们在这轮选中了它,那么这步状态能稳定转移到 \(\frac{y}{2} = 2^{j-1}\)。结合 \(2^j \ge N\),必然有转移结果 \(2^{j-1} \ge \frac{N}{2}\)。
- 也就是说,我们就算当个瞎子,每轮无论如何都有一个绝对保底的选项,能让新产生的数字 \(\ge \frac{N}{2}\)。
此时反观最大质因数稍微大点的情况:
如果我们在区间内选取的 \(y\) 包含别的因子,其最大质因数一旦达到界限(即 \(\text{gpf}(y) \ge 5\)),那么我们将得到:
因为 \(0.4N < 0.5N\),所以凡是最大质因数大于等于 5 的备选数字,其除法带来的数值衰减过于恐怖。它辛辛苦苦算出来的商,连我们在上面得到的保底选项 \(\frac{N}{2}\) 的尾灯都看不到,绝对不可能成为我们需要的最大值。
这说明在庞大的 \(10^{18}\) 数字范围内,真正有资格成为最优跳板的 \(y\),必然只出自于以下两种极其极端的组合:
- 最大质因数为 2:即 \(y = 2^j\) 形式,对应的下一步转移到 \(\frac{y}{2}\)。
- 最大质因数为 3:即 \(y = 2^j \times 3^k \ (k \ge 1)\) 形式,下一步转移到 \(\frac{y}{3}\)。注意它极有可能比单纯的 \(\frac{N}{2}\) 答案更优从而篡权,因为 \(\frac{2N}{3} \approx 0.66N\)。
因此贪心模拟做法呼之欲出:
我们无需挨个枚举数字。只需枚举所有可能的 \(2^j\)(作为基准参数),然后不断去乘上尽可能多的 \(3\)、让其死死逼近区间右边界 \(2N-1\)。如果得到了一个依然存活在 \([N, 2N-1]\) 里的合法数值,此时只会有最大因子是 2 或者 3 这两种干净纯粹的身份。看看是哪种身份,直接算出除下来的结果,在全局滚一个 \(\max\) 进行更新即可。
复杂度分析
- 时间复杂度:
在getnxt一次寻找的过程中,外层循环单纯枚举 \(x = 2^j\),只会执行区区 \(\log_2(2N)\) 趟。内部逼近时顶多经历 \(\log_3\) 次,因此计算一次下一步的耗时被严格降维在 \(O(\log N)\)。
又因每次的 \(N\) 会被至少除以一半缩水,外层的总模拟趟数也就只是 \(\log_2 N\) 次即可跌落到 1。
两相组合,总体执行时间复杂度为极度优秀的 \(O(\log^2 N)\)。 - 空间复杂度:由于只是使用变量更新最大值,没有开辟哪怕一个附加数组缓存,因此空间复杂度为 \(O(1)\)。
原理解析与 AC 代码
x: 代表外层循环枚举的 \(2^j\) 这部分基础数字。y: 代表以x为框架出发,尽全力通过乘 3 吃满空间得到的刚好没有越过r(即 \(2N-1\)) 的极限最大值。res: 单边一轮转移里用来筛选更新的最大结果,用来当作下一步轮回的起手式。
代码原版输出如下:
#include<bits/stdc++.h>
using namespace std;
#define INF 0x3f3f3f3f3f3f3f3f
using i128=__int128_t;
#define int long long
#define endl '\n'
int n;
// 获取当前状态 n 执行最优贪心转移后的下一个结果
int getnxt(int n){
i128 l=n, r=2*n-1;
i128 res=0;
// 枚举全部由于底数带来的 x = 2^j
for(i128 x=1; x<=r; x*=2){
i128 y=x;
// 把 3 的因子尽可能贪心塞入,一旦超过右边界立刻停止
while(y*3<=r) y*=3;
// 如果塞满后发现 y 连区间最底部的左边界 l 都不够
// 此基底分支完全不在 [n, 2n-1] 范围内,直接忽略
if(y<l) continue;
// 如果 y 就是一个 2 的整次幂 (即代码中没执行过前面的乘3,k=0的情况)
// 那么这轮该数最大的质因数就是 2,产生候选结果为 y / 2
if(y==x) res=max(res, y/2);
// 否则如果在里面填充了 3 的因子
// 那么哪怕掺杂了 2,其最大质因数依然是起决定作用的 3,候选结果为 y / 3
else res=max(res, y/3);
}
return res;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin>>n;
int ans=0;
// 跌到 1 之前不停步
while(n!=1){
n=getnxt(n);
ans++;
}
cout<<ans<<endl;
return 0;
}

浙公网安备 33010602011771号