[R61E] 树

  • 难度 普及+/提高
  • 时限 1s
  • 空限 512m
  • 质因子最近公共祖先

数据规模:2n1062 \le n \le 10^6

思路

把每个数 xx 的质因子按从大到小的顺序排列(含重数),记为序列 P(x)P(x)。例如 1212 对应 [3,2,2][3, 2, 2]。题目要求任意两节点 x,yx, y 的最近公共祖先编号等于 P(x)P(x)P(y)P(y) 的最长公共前缀的乘积。

关键观察:若 uu 的质因子序列是 vv 的质因子序列的前缀,则 uu 必须是 vv 的祖先(否则无法满足 LCA 定义)。于是可以这样构造:祖先关系等价于质因子序列的前缀关系,树上两节点的 LCA 就对应两序列的最长公共前缀。

考虑 xx 及其质因子序列的最后一个(最小的)质因子 pkp_k,令:

y=x/pk=x/minp[x]y = x / p_k = x / \operatorname{minp}[x]

P(y)P(y) 恰好是 P(x)P(x) 去掉最后一个元素的前缀,因此 yyxx 的祖先,且 xxyy 的 LCA 就是 yy 本身。由于 yyxx 之间不存在任何其他数(它们的质因子序列相邻),yy 就是 xx 的直接父亲:

fa[x]=x/minp[x],fa[1]=0fa[x] = x / \operatorname{minp}[x], \quad fa[1] = 0

minp[x]\operatorname{minp}[x](最小质因子)用埃氏筛求出:从小到大枚举质数 pp,用它标记所有还没被更小质因子标记的倍数。

复杂度

时间 O(nloglogn)O(n \log \log n)(埃氏筛),空间 O(n)O(n)

仓颉实现

import std.env.*
import std.convert.*

main(): Int64 {
    let reader = getStdIn()
    let n = Int64.parse(reader.readln().getOrThrow().split(" ", removeEmpty: true)[0])
    let nn = n
    // minp[x]: x 的最小质因子,埃氏筛求得
    let minp = Array<Int64>(nn + 1, { _ => 0 })
    var i = 2
    while (i <= nn) {
        if (minp[i] == 0) {
            // i 是质数,用它标记所有还没被更小质因子标记的倍数
            var j = i
            while (j <= nn) {
                if (minp[j] == 0) {
                    minp[j] = i
                }
                j += i
            }
        }
        i += 1
    }
    // fa[1] = 0,fa[x] = x / minp[x]
    let sb = StringBuilder()
    sb.append(0)
    var x = 2
    while (x <= nn) {
        sb.append(" ")
        sb.append(x / minp[x])
        x += 1
    }
    println(sb.toString())
    return 0
}

要点:

  • 埃氏筛中每个合数只被其最小质因子标记一次(先到先得),数组初始为 00 即代表「尚未被标记」。
  • 输出用 StringBuilder 一次性拼出,避免 10610^6 次逐行打印。