算法分析
-
1、如果一个数
x
的约数之和y
(不包括他本身)比他本身小,则x
与y
连一条边,问题转化为求树的直径问题 -
2、由于只用到
y
到x
的边,因此只需要连一条从y
到x
的有向边即可,但必须从当前树的最小的数(树根)开始向下递归 -
3、由于题目可能会存在多棵树,因此需要
st[]
数组标记该点是否被遍历过
(其中这里只有一棵以1
为根结点的树,只需要dfs(1)
即可,原因是啥没证明出来)
时间复杂度 $O(nlogn)$
//通过筛法求出1到n的所有约数之和
for(int i = 1;i <= n;i ++) //O(n)
{
for(int j = 2;j <= n / i;j ++) // 调和级数O(logn) = n + n / 2 + n / 3 + ... + n/n = lnn + c
{
sum[i * j] += i;
}
}
参考文献
算法提高课
Java 代码
import java.util.Arrays;
import java.util.Scanner;
public class Main {
static int N = 50010;
static int M = N;
static int[] h = new int[N];
static int[] e = new int[M];
static int[] ne = new int[M];
static int idx = 0;
static int[] sum = new int[N];//sum[i]表示i的约数之和
static boolean[] st = new boolean[N];
static int ans = 0;
static void add(int a,int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
//找到u点往下走的最大长度
static int dfs(int u)
{
st[u] = true;
int d1 = 0;
int d2 = 0;
for(int i = h[u];i != -1;i = ne[i])
{
int j = e[i];
int d = dfs(j) + 1;
if(d > d1) {d2 = d1;d1 = d;}
else if(d > d2) {d2 = d;}
}
ans = Math.max(ans, d1 + d2);
return d1;
}
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
Arrays.fill(h, -1);
//通过筛法求出1到n的所有约数之和
for(int i = 1;i <= n;i ++)
{
for(int j = 2;j <= n / i;j ++)
{
sum[i * j] += i;
}
}
for(int i = 1;i <= n;i ++)
if(sum[i] < i)
{
add(sum[i],i);
}
for(int i = 1;i <= n;i ++)
{
if(!st[i])
dfs(i);
}
System.out.println(ans);
}
}
只递归根结点是1的树
import java.util.Arrays;
import java.util.Scanner;
public class Main {
static int N = 50010;
static int M = N;
static int[] h = new int[N];
static int[] e = new int[M];
static int[] ne = new int[M];
static int idx = 0;
static int[] sum = new int[N];//sum[i]表示i的约数之和
static int ans = 0;
static void add(int a,int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
//找到u点往下走的最大长度
static int dfs(int u)
{
st[u] = true;
int d1 = 0;
int d2 = 0;
for(int i = h[u];i != -1;i = ne[i])
{
int j = e[i];
int d = dfs(j) + 1;
if(d > d1) {d2 = d1;d1 = d;}
else if(d > d2) {d2 = d;}
}
ans = Math.max(ans, d1 + d2);
return d1;
}
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
Arrays.fill(h, -1);
for(int i = 1;i <= n;i ++)
{
for(int j = 2;j <= n / i;j ++)
{
sum[i * j] += i;
}
}
for(int i = 1;i <= n;i ++)
if(sum[i] < i)
{
add(sum[i],i);
}
dfs(1);
System.out.println(ans);
}
}
按照这个循环,每个数的最后都会走到质数,然后质数约数之和就会走到1。(22可以继续约数之和2111,得到13)。
不知道这样证明能不能行得通。
有道理,这个证明!tql
这个证明不严谨啊,把序列倒过来的话直接就不成立了
6, 1 + 2 + 3 = 6, 6 <====> 6
题目要求和比本身小,所以6不包含在里面
为什么是只用到了y到x的边???
由约数和sum[i]向i建边,因为sum[i]<i是题目要求的,所以就是由小的数向大的数建边。
sum[i]是唯一的,它可以走到多个i去,也就是一个节点有多个子节点,子节点之间没有边,这才是一棵树。
如果反过来,i->sum[i],就是多个节点指向同一个点,这时就不是树了。我们本质上是在想办法构建树,然后跑以1为根的最长直径,所以,由小到大创建才是正道。
加边应该从 $2$ 开始吧, 因为所有数字变换在不超过 $n$ 的正整数范围内进行
从 $1$ 开始会添加一条从 $0$ 到 $1$ 的边, 但是在
dfs
的时候由于从 $1$ 开始, 且是有向图所以没有对最终的结果产生影响.说的妙
细节!
楼主那个筛法求约数的时间复杂度是不是笔误写作了哇
好像应该是n*lnn
是的 但是lnn和logn 的差别只是换个底 差别不大 习惯上还是写nlogn
确实会有多颗树 如:34-20-22,大概是由于这种树路径确实比能够返回1的短多了吧
没懂你在讲什么
我意思是的确会构成多颗树,但是最长路径是以1为根的树;我猜测是因为大部分数都能经过层层转换使得最后约数变成1导致其他树的直径比“1”这棵树小,但稳妥起见还是每棵树都求解一次好
嗯嗯,有道理
没看懂,完整的应该是34-20-22-14-10-8-7-1,最终肯定是会回到1的啊
突然有点懵逼了,这道题不是一个有向图吗,从根节点往下找最长路径 ans 跟新时候不应该只用最大值d1更新就可以了吗, 为啥是用d1 + d2呢, 之前那个树的直径的题是一个无向图呀
可能是
因为可以相互转化且不能走回头路吧
这个不是有向图吗,为啥可以相互转换
题目要求可以相互转换,但是y总说因为 一个数的约数之和是唯一的, 但是一个约数之和 不是唯一对应一个约数所以建成了有向图 ,其实也可以建成无向图的,所以是可以相互转换的
是的,例如题目的样例是4 3 1 7,分别对应的是
1
到3
到4
的路径d1
,和1
到7
的路径d2
,因此总路径是d1 + d2
,由于我们可以规定他用有向图去做(本身是无向图),因此从最小的值开始出发递归遍历时间复杂度假了吧,这个筛法肯定非线性啊
但是具体不会证(
确实写错了,已修改
会证了,近似于 $O(n \log n)$
tql
您不是也会了吗..
求问一下这里为啥不需要 当为父节点时就continue 这句话。。。🤣
因为建的是单向边,你也可以建双向边,建双向边就要记录父结点
哦哦,感谢!!😂
双向边要超时诶
你是不是没特判回到父节点的情况
只dfs(1)是因为1是任何数的约数(说明其他数都是1的后继节点),并且1也是最小的约数之和(保证了1是根节点)
个人想的一点证明方式
如果存在一些数没有连到以1为根结点的树中,例如20,20的约数之和是22,怎么证明出不存在任何数的约数之和是20,除了20,其他类似的数应该怎么证明?
整理的太好了
加油