先说原题目,应该是跟一个算法有关系,看了 优快云 有几个 C 语言代码,可能原题是让用 C 语言编写的?题目如下:
这个应该是题解 因数最多的数_c语言输出一个范围内因子数最多的数-优快云博客,我就只放链接,不往文章里放代码了
原理和思路还是说一下,对于一个正整数 \(n\) ,若其素因子分解形式如下
$$n = p_1^{\alpha _1}p_2^{\alpha _2} \cdots p_r^{\alpha _r}$$
则 \(n\) 的因子个数为 \(\sigma _0(n)=(\alpha _1+1)(\alpha _2+1)\cdots (\alpha _r+1)\) ,说白了就是在 \(n\) 的所有的素数因子里挑选组合,每个素数因子 \(p_i\) 可选 \(0\) 到 \(\alpha _i\) 次,共 \(\alpha _i+1\) 种情况,将其相乘就是其因子的所有可能情况数了
于是我们可以通过构造一个因子数尽可能多的数来筛选排除,一般来说,其素因子越小越好,将较小的素因子分配的指数大一点。题解基本上就是这个思路
模仿着用 python 写了一段,也挺好写的,说实话用 python 确实香,不怕整数超限,像题解用 C 语言的话,\(n=10^{18}\) 就超限了
然后就是我看题解还有其他文章,都是只考虑因子数最多且本身最小的那个数,不过相同的因子个数可以对应不止一个原数,我还是想让这些原数全都展示出来。然后就是我不只关注因子个数与原数是什么,我还想知道每个原数的素因子分解情况,让它顺便展示出来,代码如下
n = eval(input()) # 如 1e20、10**20
# n < 1e20 时,15个素数足够使用,这里给20个
prime = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71]
mc = 0 # 最大因子个数
lst = [] # 所有解的素因子指数列表
ans = [] # 所有解的原数
def dfs(m, u=0, x=1, cnt=1, r=[]): # m:每个素数的最大指数 u:当前使用的素数索引 x:当前原数 cnt:因子个数 r:指数列表
'深度优先搜索(Depth-First Search)函数'
global mc, lst, ans
if cnt == mc: ans.append(x); lst.append(r) # 添加到解集中
if cnt > mc: mc = cnt; ans = [x]; lst = [r] # 更新解集
if u == 15: return
for i in range(1, m + 1): # 遍历每个素数的所有可能指数
x *= prime[u]
if x > n: break
dfs(i, u + 1, x, cnt * (i + 1), r + [i]) # 递归调用,使用下一个素数
dfs(10) # n < 1e20 时 m = 10 足够
# 遍历得到的解,尝试交换素数的指数来找到所有解
for num, s in zip(ans.copy(), lst.copy()): # 将原数与指数列表对应组合
p = s + [0, 0, 0] # 末尾添加0,用于后续处理
for j in range(len(s)):
for k in range(1, 4): # 考虑最多相差3个索引的素数
if p[j] - p[j + k] == 1: # 只考虑指数相差1的情况
new = num // prime[j] * prime[j + k] # 计算新的原数
if new < n:
q = p.copy()
q[j], q[j + k] = q[j + k], q[j] # 交换指数
while q[-1] == 0: # 移除末尾的0
q.pop()
lst.append(q)
ans.append(new)
print(mc)
print(lst)
print(ans)
一般来说,解得的原数中,其较小的素因子的指数都不会小于其较大的,比如 \(12=2^2 \times 3\) 与 \(18=2\times 3^2\) 的因子个数相同,但 \(12\) 显然更小,更有可能成为解,不过实际情况也有例外。而代码中的 dfs 函数只考虑指数依次递减或相等的情况,如果修改 dfs 函数,允许较大素因数的指数大于较小的,哪怕只允许一次,也会降低其搜索的效率。因此我还是想分析已经获得的解,因为这些解的个数不会很多。对于已经得到的解,可以尝试通过交换其素因子的指数,来寻找同样符合条件的解
测试了大量的实际情况,发现在 \(n<10^{20}\) 的情况下,没有逃出以下这几个限制条件的
1. 最多交换两个素数一次
2. 两个素数的指数只能相差1
3. 两个素数的索引最多相差3
否则交换后的原数就会大于 \(n\) ,比如 \(n=10^{10}\) 时,有 \(6983776800=2^5×3^3×5^2×7×11×13×17×19\) 、\(9311702400=2^7×3^2×5^2×7×11×13×17×19\) 、\(9777287520=2^5×3^3×5×7^2×11×13×17×19\) 、\(9448639200=2^5×3^3×5^2×7×11×13×19×23\) 、\(8454045600=2^5×3^3×5^2×7×11×13×17×23\) 这 5 个解,其中第 3 个解是交换 5 与 7 的指数,第 4 个是交换 17 与 23,第 5 个是交换 19 与 23,都在上述的限制条件下
然后就比较好处理了,正如我上面的代码,用 for 循环处理 dfs 搜索后得到的解,注意这个解既包含原数,也包含其素因子的指数列表,以避免丢失信息。在每次循环中,只要找到一个交换指数后仍然满足条件的解,往解集里面补充就行了。而我在添加解集之前,先把指数列表末尾的 0 都删掉,这样看起来形式更统一
验证一下运行时间合理性,有两种方法
一、使用 PyCharm 的分析功能
首先把前面的 n = eval(input()) 改为具体的值 n=1e20 ,避免输入的时间也计算在内
n = 1e20
然后进行分析
可见当 \(n=10^{20}\) 时,dfs 函数一共调用了 13611 次,总用时在 20 毫秒左右,还是挺高效的
二、用 Pycharm 对 Jupyter 笔记本的支持功能
首先把前面的代码一股脑地全部封装起来,方便直接调用
def div(n):
'''展示给定范围内的正整数中
因子个数最大值、对应的所有素因子指数列表、对应的所有原数
调用范例:div(1e20),无须再 print'''
# n < 1e20 时,15个素数足够使用,这里给20个
prime = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71]
mc = 0 # 最大因子个数
lst = [] # 所有解的素因子指数列表
ans = [] # 所有解的原数
def dfs(m, u=0, x=1, cnt=1, r=[]): # m:每个素数的最大指数 u:当前使用的素数索引 x:当前原数 cnt:因子个数 r:指数列表
'深度优先搜索(Depth-First Search)函数'
nonlocal mc, lst, ans
if cnt == mc: ans.append(x); lst.append(r) # 添加到解集中
if cnt > mc: mc = cnt; ans = [x]; lst = [r] # 更新解集
if u == 15: return
for i in range(1, m + 1): # 遍历每个素数的所有可能指数
x *= prime[u]
if x > n: break
dfs(i, u + 1, x, cnt * (i + 1), r + [i]) # 递归调用,使用下一个素数
dfs(10) # n < 1e20 时 m = 10 足够
# 遍历得到的解,尝试交换素数的指数来找到所有解
for num, s in zip(ans.copy(), lst.copy()): # 将原数与指数列表对应组合
p = s + [0, 0, 0] # 末尾添加0,用于后续处理
for j in range(len(s)):
for k in range(1, 4): # 考虑最多相差3个索引的素数
if p[j] - p[j + k] == 1: # 只考虑指数相差1的情况
new = num // prime[j] * prime[j + k] # 计算新的原数
if new < n:
q = p.copy()
q[j], q[j + k] = q[j + k], q[j] # 交换指数
while q[-1] == 0: # 移除末尾的0
q.pop()
lst.append(q)
ans.append(new)
print(mc)
print(lst)
print(ans)
然后在 Jupyter 笔记本里面调用函数
可见运行时间在几十毫秒以内,完全在合理时间范围内
作为一种笔记本环境,Jupyter 确实好用,可以帮我们一步步分析代码,还能保留运行结果
代码思路及原理讲完了,也验证了时间合理性,但都是我自己一个人验证(我让 AI 给我优化代码,实在不靠谱),可能会有遗漏(比如 \(n<10^{20}\) 时有漏解的,不过我验证了不下几百次,应该不会有)或优化不到位的地方,如果读者有什么问题或想法,可以在评论区提出来