Project Euler 615
Project Euler 615
题目
The millionth number with at least one million prime factors
Consider the natural numbers having at least \(5\) prime factors, which don’t have to be distinct.
Sorting these numbers by size gives a list which starts with:
\(\begin{aligned} &32=2\cdot2\cdot2\cdot2\cdot2\\ &48=2\cdot2\cdot2\cdot2\cdot3\\ &64=2\cdot2\cdot2\cdot2\cdot2\cdot2\\ &72=2\cdot2\cdot2\cdot3\cdot3\\ &80=2\cdot2\cdot2\cdot2\cdot5\\ &96=2\cdot2\cdot2\cdot2\cdot2\cdot3\\ &\dots \end{aligned}\)
So, for example, the fifth number with at least \(5\) prime factors is \(80\).
Find the millionth number with at least one million prime factors.
Give your answer modulo \(123454321\).
解决方案
设质因子个数(含重)为\(\displaystyle{\Omega(n)=\sum_{p^e\parallel n} e.}\)令\(M=10^6\)表示质因子个数的下界,\(K=10^6\)表示题目所要求的序数。
显然满足 \(\Omega(n)\ge M\) 的最小数是\(2^M\)。因为 \(2\) 是最小的质数,想让 \(\Omega(n)\) 尽量大且 \(n\) 尽量小,就应该全用 \(2\)。
我们用“质数指数向量”表示一个数:设第 \(k\) 个质数为 \(p_k\)(\(p_0=2,p_1=3,p_2=5,\dots\)),用\(\mathbf e=(e_0,e_1,e_2,\dots)\) 表示\(\displaystyle{n(\mathbf e)=\prod_{k\ge 0} p_k^{e_k}.}\)
初始状态就是\(\mathbf e^{(0)}=(M,0,0,\dots)\)对应 \(2^{M}\)。
为了按从小到大的顺序枚举所有 \(\Omega(n)\ge 10^6\) 的数,我们考虑两种对 \(n\) 的“最小增量”变换:
第一类操作是把 \(n\) 乘上 \(2\):\((e_0,e_1,\dots)\to (e_0+1,e_1,\dots).\)这会使\(\Omega(n)\to \Omega(n)+1\)且数值乘以 \(2\),是最便宜的增加质因子个数的方式。
第二类操作是把一个质数替换成下一个质数:从分解里取出一个 \(p_k\),换成 \(p_{k+1}\):\(e_k\to e_k-1,e_{k+1}\to e_{k+1}+1.\)对应数值变化为\(n\to n\cdot \dfrac{p_{k+1}}{p_k}.\)这会保持 \(\Omega(n)\) 不变,但让 \(n\) 变大一些;并且在保持因子数不变的所有变换里,这是非常自然的一种单步增长。
可见,从 \(2^{M}\) 出发足够覆盖全部答案。原因在于,任取一个满足 \(\Omega(n)\ge M\) 的数 \(n\),设\(\Omega(n)=10^6+t(t\ge 0).\)把其中额外的 \(t\) 个质因子尽量“拿成 2”,写成\(n=2^t\cdot m,\Omega(m)=M.\)
而 \(m\) 是恰好含 \(M\) 个质因子的数,意味着它可以写成 \(M\) 个质数的乘积。由于任意质数 \(p_k\) 都能通过反复执行“替换到下一个质数”(从 \(2=p_0\) 一路替换到 \(p_k\))得到,所以 \(m\) 一定能从 \(2^{M}\) 出发用若干次第二类操作得到;然后再用 \(t\) 次第一类操作乘上 \(2^t\) 即可得到 \(n\)。
直接比较大整数会很慢,我们用对数对状态 \(\mathbf e\) 定义权值\(\displaystyle{\mathrm{val}(\mathbf e)=\log n(\mathbf e)=\sum_{k\ge 0} e_k\log p_k.}\)
两种操作都会使 \(n\) 变大,所以也都会让 \(\mathrm{val}\) 变大(权值严格上升)。
于是我们可以用一个最小堆做最短路式枚举(本质是BFS):
- 堆中始终存放尚未输出的候选状态,键为 \(\mathrm{val}(\mathbf e)\);
- 每次弹出堆顶,就是目前还没输出的最小 \(n\);
- 对该状态执行所有可能的两类操作,生成新状态入堆并去重。
这样弹出的序列就按 \(n\) 从小到大排列。
当我们弹出第 \(K\) 个状态时,对应的 \(n\) 就是题目要的第 \(K\) 个数。
代码
1 | from heapq import heappush, heappop |