1. 问题描述:
如果一个数 x 的约数之和 y(不包括他本身)比他本身小,那么 x 可以变成 y,y 也可以变成 x。例如,4 可以变为 3,1 可以变为 7。限定所有数字变换在不超过 n 的正整数范围内进行,求不断进行数字变换且不出现重复数字的最多变换步数。
输入格式
输入一个正整数 n。
输出格式
输出不断进行数字变换且不出现重复数字的最多变换步数。
数据范围
1 ≤ n ≤ 50000
输入样例:
7
输出样例:
3
样例解释
一种方案为:4→3→1→7。
来源:https://www.acwing.com/problem/content/1077/
2. 思路分析:
分析题目可以知道每一个数的约数之和是固定的,所以对于当前数以及它的约数是存在关系的,这种关系是唯一确定的,借助于图论中的思想,如果两个节点是有关系的那么可以向这两个节点连一条边,由题目可知约数之和y必须小于x的前提下x可以转换为y,y也可以转换为x,所以我们可以将当前数的约数之和y作为父节点,x作为子节点,y向x连一条有向边(y指向x),这样就可以将原问题转换为有向树的问题,因为题目要求我们求解出最长路径也即求解树中的最长路径,与1072 树的最长路径题求解最长路径是一模一样的,对于树的最长路径的问题,我们可以使用dfs遍历树中的每一个节点,求解出当前节点往下走的最长路径d1与次长路径d2,使用一个全局变量res来更新每一个节点对应的最长路径与次长路径之和,由于在dfs遍历节点的过程中会遍历到每一个节点所以我们一定可以找到某一条路径是经过某个点的,这个点的最长路径与次长路径之和就是答案,也即在dfs的过程中一定会更新到最长路径;本质上是考察我们将题目转换为已经学过的模型的能力,题目一般会有相应的背景,我们需要在这些背景中提炼出本质上考察的模型,其实这也是一种能力;对于这道题目来说其实画画图就可以知道考察的本质了:
解决了上面的问题之后我们还需要解决另外一个问题:如果快速求解1~n中的约数之和,比较常规的方法是使用试除法,但是这样求解的时间复杂度为O(nlogn),不太容易扩展,对于n较大一点可能就超时了,这里我们可以反着来求解,我们可以枚举1~n中的每一个数字,枚举这些数字是哪些数的约数,这样时间复杂度可以降为O(nlogn):n + n / 2 + n / 3 + ... = nlgn + c
for (int i = 1; i <= n; ++i){
for (int j = 2; j <= n / i; ++j){
...
}
}
除了使用上面树形dp的求解思路之外,可以发现父节点的编号一定小于子节点的编号,而且求解的是每一个点往下走的最长路径与次长路径,所以我们可以从子节点往父节点的方向递推,可以定义一个一维数组dp,i为当前节点,s[i]为当前节点的约数之和,dp[s[i]]表示父节点s[i]往下走对应的最长路径,dp[i]表示子节点往下走的最长路径,dp[s[i]] + dp[i] + 1表示的就是以当前父节点s[i]为根节点往下走的最长路径与次长路径的的和,在递推的时候需要更新一下dp[s[i]](逆着递推)和答案。
3. 代码如下:
树形dp:
from typing import List
class Solution:
res = 0
# 因为是有向图所以不需要传递父节点fa, 在递推的时候是一直往下递归的所以不会造成死循环的情况
def dfs(self, u: int, g: List[List[int]], sta: List[int]):
sta[u] = 1
# d1, d2记录当前节点往下走的最长路径与次长路径
d1, d2 = 0, 0
# next为当前节点的子节点编号
for next in g[u]:
# 递归的结果加上当前节点u到子节点的距离1就是当前节点u==>next往下走的最长路径
d = self.dfs(next, g, sta) + 1
if d > d1:
d2 = d1
d1 = d
elif d > d2:
d2 = d
# 对于每一个节点都更新最长路径
self.res = max(self.res, d1 + d2)
return d1
def process(self):
n = int(input())
# 有向图, g的每一个节点都是一个列表, 这样可以存储节点的所有邻接点编号
g = [list() for i in range(n + 1)]
s = [0] * (n + 1)
# 计算约数之和, 这里反着来枚举, 枚举当前的i是哪些数的倍数, 然后累加到对应的数上, 时间复杂度为nlogn
for i in range(1, n + 1):
for j in range(2, n // i + 1):
s[i * j] += i
for i in range(1, n + 1):
# i的约数之和小于s[i]才是有效的
if s[i] < i:
# 注意s[i]是i的父节点, 也即i指向s[i]
g[s[i]].append(i)
sta = [0] * (n + 1)
# 因为可能存在多棵树所以需要使用sta表示一下那些节点已经被搜过了,
for i in range(1, n + 1):
if sta[i] == 0:
# 搜索以当前的节点为根节点的子树
self.dfs(i, g, sta)
return self.res
if __name__ == '__main__':
# 本质上求解的是树的最长路径
print(Solution().process())
递推:
class Solution:
def process(self):
n = int(input())
s = [0] * (n + 1)
dp = [0] * (n + 1)
# 由题目可知, 父节点一定小于子节点所以递推即可
for i in range(1, n + 1):
for j in range(i * 2, n + 1, i):
s[j] += i
res = 0
# 递推的时候应该逆序递推, 父节点的节点编号都是小于子节点编号的
for i in range(n, 0, -1):
if s[i] >= i: continue
# 最长路径与次长路径之和, 求解出另外一条子节点的最长路径(之前已经求解出父节点的一条最长路径了)相当于是次长路径所以两者相加 + 1就是当前节点往下走的最长路径与次长路径之和
res = max(res, dp[s[i]] + dp[i] + 1)
# 更新一下父节点
dp[s[i]] = max(dp[s[i]], dp[i] + 1)
return res
if __name__ == '__main__':
# 递推
print(Solution().process())