「一本通 6.3 练习 2」聪明的燕姿

GitHub跳转原题关系图返回列表

把约数和公式 σ(n)=∏(1+p+…+p^a) 视为对 S 的编号段分解,DFS 按质数递增枚举质数幂,指数为 1 的大质数因子靠 rest-1 的素性判定收尾。

OJ: roj

题目 ID: 1629

难度:提高+/省选-

标签:数论搜索DFS素数python

创建: 2026-09-30 23:10

更新: 2026-10-07 13:50

形式化题目

给定正整数 SS,求所有满足 σ(n)=S\sigma(n)=S 的正整数 nn(σ(n)\sigma(n) 表示 nn 的正约数之和),并把它们升序排列。

若 n=∏ipiain=\prod_i p_i^{a_i} 是 nn 的质因数分解,则

σ(n)=∏i(1+pi+pi2+⋯+piai).\sigma(n)=\prod_i\bigl(1+p_i+p_i^2+\cdots+p_i^{a_i}\bigr).

记 G(p,a)=1+p+⋯+pa=pa+1−1p−1G(p,a)=1+p+\cdots+p^a=\dfrac{p^{a+1}-1}{p-1},下称编号段。题目等价于:把 SS 分解成若干个编号段之积,再按 G(p,a)↔paG(p,a)\leftrightarrow p^a 还原出 nn。

样例 S=42S=42:

编号段分解 还原出的 nn 验证
42=(1+2)×(1+13)42=(1+2)\times(1+13) 2×13=262\times 13=26 σ(26)=1+2+13+26=42\sigma(26)=1+2+13+26=42
42=(1+2+4)×(1+5)42=(1+2+4)\times(1+5) 4×5=204\times 5=20 σ(20)=1+2+4+5+10+20=42\sigma(20)=1+2+4+5+10+20=42
42=(1+41)42=(1+41) 4141 σ(41)=1+41=42\sigma(41)=1+41=42

三行分别对应 4242 的三种编号段拆法,所以输出 3 与 20 26 41。注意第三行说明答案可以是很大的质数本身,4141 不可能被「枚举质数」提前覆盖。

正解

思路

为什么不能枚举 nn。 由 σ(n)⩾n+1\sigma(n)\geqslant n+1 得 n⩽S−1≈2×109n\leqslant S-1\approx 2\times 10^9,逐个数分解质因数在 k⩽100k\leqslant 100 组数据下完全不可行。而且 nn 是答案、SS 才是已知量,枚举 nn 等于丢掉了 SS 的全部信息。

换成枚举 SS 的因子。 由于 σ(n)=∏iG(pi,ai)\sigma(n)=\prod_i G(p_i,a_i),每个 G(pi,ai)G(p_i,a_i) 都必须是 SS 的因子,并且 G(pi,ai)⩾2G(p_i,a_i)\geqslant 2。于是问题变成在 SS 的因子上做分解。枚举时只用到 SS,与答案的大小无关。

两类编号段要分开处理。

  • 指数 a⩾2a\geqslant 2 的编号段:此时 G(p,a)⩾1+p+p2>p2G(p,a)\geqslant 1+p+p^2>p^2,而 G(p,a)∣SG(p,a)\mid S,所以 p2<S⩽2×109p^2<S\leqslant 2\times 10^9,即 p<44722p<44722。这一范围内的质数只有 46484648 个,可以预筛出来遍历。 同时,当枚举到某个质数 pp 满足 p2>restp^2>rest 时,pp 的指数已不可能 ⩾2\geqslant 2,可以直接停止枚举。
  • 指数为 11 的编号段:G(p,1)=p+1G(p,1)=p+1,其中 pp 可以是任意大的质数(如样例中的 4141), 无法枚举。但它可以反向识别:当前还剩 restrest 没被解释时,只要 rest−1rest-1 是质数,就用编号段 1+p=rest1+p=rest 收尾,把 p=rest−1p=rest-1 乘进答案。

DFS 状态。 用 (start, rest, cur) 表示:只能用第 start 个及之后的质数,编号段乘积还差 rest, 已确定的答案为 cur。转移过程如下。

text
search(start, rest, cur)
├─ rest == 1                  -> 收集 cur
├─ rest-1 >= p_start 且是质数 -> 收集 cur*(rest-1)      # 一次收尾,指数取 1
└─ 对 j = start.. 且 p_j^2 <= rest:
     令 pk = p^a, segment = 1+p+...+p^a 逐次增长
     segment | rest 时 search(j+1, rest/segment, cur*pk)

S=42S=42 的递归过程。 下图展示 DFS 在样例上的全部分支,可见三条成功路径恰好对应表中的三个答案。

text
search(start=2, rest=42, cur=1)
├─ 收尾:rest-1 = 41 是质数 ──────────────────────────────► 答案 41
├─ 枚举 p=2(2^2 = 4 ≤ 42),逐段尝试:
│    G(2,1)=3  | 42  -> search(start 取 3 及以后, rest=14, cur=2)
│         └─ 收尾:14-1 = 13 是质数 ──────────────────────► 答案 26
│    G(2,2)=7  | 42  -> search(start 取 3 及以后, rest=6, cur=4)
│         └─ 收尾:6-1 = 5 是质数 ────────────────────────► 答案 20
│    G(2,3)=15 不整除 42;G(2,4)=31 不整除 42;G(2,5)=63 > 42 停
├─ p=3:G(3,1)=4 不整除 42,不递归
├─ p=5:G(5,1)=6 | 42 -> search(start 取 7 及以后, rest=7, cur=5)
│         └─ 7-1 = 6 不是质数,且 7^2 = 49 > 7,死路
└─ p=7:7^2 = 49 > 42,break

三点观察:一是样例的三个答案分别来自「只用一个收尾编号段」「一个指数 1 的段加一个收尾段」「一个指数 ⩾2\geqslant 2 的段加一个收尾段」,正好覆盖了 DFS 的三种收尾方式;二是 p=5p=5 那条分支说明 p2>restp^2>rest 的剪枝是必要的,否则会继续试探更大的质数。

为什么要把 pp 限制在递增顺序。 nn 的质因子集合与顺序无关,固定「从小到大取」后, 每个 nn 的编号段序列唯一,既不漏也不重:递归传 j+1 使同一个质数不会被用两次, 收尾分支同样要求 rest−1⩾pstartrest-1\geqslant p_{start},否则又会取到已经用过的较小质数。 这条规则也保证了算法能枚举完整:设 n=∏ipiain=\prod_i p_i^{a_i}(pip_i 递增), 只要 DFS 依次选中 ai⩾2a_i\geqslant 2 的那些编号段,剩下的 restrest 就是所有 ai=1a_i=1 的段之积; 若只剩一个这样的段,rest−1rest-1 就是质数,被收尾分支产出,否则 rest 已化为 11。

代码

Python 版:

python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-30 23:11
# update_at: 2026-09-30 23:11

import sys

MAX_S = 2 * 10**9
LIMIT = int(MAX_S**0.5) + 1   # 只筛到 sqrt(S):指数 >= 2 的质因子必然落在这个范围内
BASES = (2, 3, 5, 7)          # n <= 2e9 时这 4 个底数的 Miller-Rabin 已能确定素性


def sieve(limit: int) -> tuple[list[int], bytearray]:
    """埃氏筛:返回 limit 以内的质数表,以及「是否合数」的标记表。"""
    composite = bytearray(limit + 1)
    for i in range(2, int(limit**0.5) + 1):
        if not composite[i]:
            composite[i * i :: i] = b"\x01" * ((limit - i * i) // i + 1)
    return [i for i in range(2, limit + 1) if not composite[i]], composite


PRIMES, COMPOSITE = sieve(LIMIT)


def is_prime(n: int) -> bool:
    """判断 n 是否为质数:小范围查筛表,大范围用 Miller-Rabin。

    n <= 2e9 远小于已知反例下界 3.2e9,底数 2、3、5、7 的强伪素数判定没有遗漏。
    """
    if n < 2:
        return False
    if n < LIMIT:
        return not COMPOSITE[n]
    d, s = n - 1, 0
    while not d & 1:
        d >>= 1
        s += 1
    for a in BASES:
        x = pow(a, d, n)
        if x == 1 or x == n - 1:
            continue
        for _ in range(s - 1):
            x = x * x % n
            if x == n - 1:
                break
        else:
            return False
    return True


def search(start: int, rest: int, cur: int, found: list[int]) -> None:
    """继续枚举:只能用第 start 个及之后的质数,编号段的乘积还差 rest,已确定部分为 cur。

    sigma(x) = (1+p1+...+p1^a1)*(1+p2+...+p2^a2)*... 是若干编号段之积,
    所以枚举 x 等价于把 rest 拆成若干个「1+p+...+p^a」再拼回去,质数只能从小到大用。
    """
    if rest == 1:                              # 编号段全部拆完,cur 就是一个答案
        found.append(cur)
        return
    if rest - 1 >= PRIMES[start] and is_prime(rest - 1):
        found.append(cur * (rest - 1))         # 编号段 (1+p) 一次收尾,对应指数取 1
    for j in range(start, len(PRIMES)):
        p = PRIMES[j]
        if p * p > rest:                       # p 的指数已只能取 1,交给上面那行收尾
            break
        pk, segment = p, 1 + p                 # pk = p^a,segment = 1+p+...+p^a
        while segment <= rest:
            if rest % segment == 0:
                search(j + 1, rest // segment, cur * pk, found)
            pk *= p
            segment += pk                      # 下一段是 1+p+...+p^(a+1)
    return


def divisor_sum_candidates(s: int) -> list[int]:
    """返回所有正约数之和等于 s 的正整数,升序。"""
    found: list[int] = []
    search(0, s, 1, found)
    found.sort()
    return found


def solve() -> None:
    out: list[str] = []
    for s in map(int, sys.stdin.buffer.read().split()):
        ans = divisor_sum_candidates(s)
        out.append(str(len(ans)))
        if ans:                                # m = 0 时第二行不输出
            out.append(" ".join(map(str, ans)))
    sys.stdout.write("\n".join(out) + "\n")


if __name__ == "__main__":
    solve()

C++ 版(同一算法):

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-05 23:14
 * update_at: 2026-10-05 23:14
 */

// 求 sigma(n) = S 的所有 n:把 S 拆成编号段 1+p+...+p^a 之积,DFS 按质数递增枚举,
// 指数为 1 的大质数段靠「rest-1 是质数」反向收尾(Miller-Rabin 判定)。

#include <cstdio>
#include <cstring>
#include <vector>
#include <algorithm>
typedef long long ll;

const int LIMIT = 44722; // sqrt(2e9) 上取整,指数 >= 2 的质因子 p 必有 p^2 < S
const int MAXN = 45005;

bool comp[MAXN];      // comp[i] = true 表示 i 是合数(筛表)
std::vector<ll> pri;  // 小质数表,只覆盖指数 >= 2 的情形
ll ans[100005];       // 本组询问的所有答案,最后排序输出
int cnt;              // 答案个数

// 埃氏筛出 LIMIT 以内的质数
void sieve() {
    memset(comp, 0, sizeof(comp));
    for (ll i = 2; i <= LIMIT; i++) {
        if (!comp[i]) {
            pri.push_back(i);
            for (ll j = i * i; j <= LIMIT; j += i) comp[j] = true;
        }
    }
}

// Miller-Rabin 判素:n <= 2e9 时底数 2,3,5,7 已足够(无强伪素数遗漏)
bool is_prime(ll n) {
    if (n < 2) return false;
    if (n < (ll)MAXN && n <= LIMIT) return !comp[n];
    ll d = n - 1; // n-1 = d * 2^s
    int s = 0;
    while (!(d & 1)) { d >>= 1; s++; }
    int base[4] = {2, 3, 5, 7};
    for (int t = 0; t < 4; t++) {
        ll a = base[t];
        ll x = 1;
        // 快速幂算 a^d mod n
        ll e = d;
        while (e > 0) {
            if (e & 1) x = (__int128)x * a % n;
            a = (__int128)a * a % n;
            e >>= 1;
        }
        if (x == 1 || x == n - 1) continue;
        bool ok = false;
        for (int i = 0; i < s - 1; i++) {
            x = (__int128)x * x % n;
            if (x == n - 1) { ok = true; break; }
        }
        if (!ok) return false;
    }
    return true;
}

// 只能用第 start 个及之后的质数;编号段乘积还差 rest;已确定的答案部分为 cur
// 注意 n 的质因子个数不超过 9(2*3*5*7*11*13*17*19*23 > 2e9),递归深度很小
void dfs(int start, ll rest, ll cur) {
    if (rest == 1) { // 编号段全部拆完,cur 是一个答案
        ans[++cnt] = cur;
        return;
    }
    // 收尾分支:G(p,1) = 1+p = rest,即 p = rest-1 是质数时取指数 1 的大质数段
    if (rest - 1 >= pri[start] && is_prime(rest - 1)) ans[++cnt] = cur * (rest - 1);
    // 枚举指数 >= 2 的编号段:段值 1+p+...+p^a 整除 rest 才能递归
    for (int j = start; j < (int)pri.size(); j++) {
        ll p = pri[j];
        if (p * p > rest) break; // p 的指数已只能取 1,交给上面的收尾
        ll pk = p;       // pk = p^a
        ll seg = 1 + p;  // seg = 1+p+...+p^a
        while (seg <= rest) {
            if (rest % seg == 0) dfs(j + 1, rest / seg, cur * pk);
            pk *= p;
            seg += pk;
        }
    }
}

int main() {
    sieve();
    ll S;
    while (scanf("%lld", &S) == 1) {
        cnt = 0;
        dfs(0, S, 1);
        std::sort(ans + 1, ans + cnt + 1);
        printf("%d\n", cnt);
        for (int i = 1; i <= cnt; i++) printf("%lld%c", ans[i], i == cnt ? '\n' : ' ');
    }
    return 0;
}

复杂度

  • 预处理:埃氏筛出 p<44722p<44722 的质数,O(Slog⁡log⁡S)O(\sqrt{S}\log\log\sqrt{S}),只做一次。
  • 每组询问:根层遍历 π(S)=4648\pi(\sqrt{S})=4648 个质数做常数次尝试;递归深度不超过 nn 的不同质因子个数 (最多 99 个,因为 2⋅3⋅5⋯23>2×1092\cdot3\cdot5\cdots23>2\times 10^9),且每层枚举范围随 restrest 收缩。 因此单组代价约为 O(π(S)+可行分支数)O\bigl(\pi(\sqrt{S})+\text{可行分支数}\bigr),与 nn 的大小无关。
  • 空间:O(S)O(\sqrt{S}) 的表 + O(log⁡S)O(\log S) 递归栈 + 答案列表。
  • 实测 11 组真实数据共约 460 个询问,最慢一组 0.47s(含 m=12554m=12554 的大输出),峰值内存 27.5MB。

总结

关键在于不枚举答案 nn,而是分解已知的 SS:σ(n)\sigma(n) 的乘性公式把「求 nn」变成了 「把 SS 拆成编号段 G(p,a)=1+p+⋯+paG(p,a)=1+p+\cdots+p^a 的乘积」。其中指数 ⩾2\geqslant 2 的编号段保证 p<S⩽44722p<\sqrt{S}\leqslant 44722,可以预筛质数遍历;指数为 1 的编号段 G(p,1)=p+1G(p,1)=p+1 用 「rest−1rest-1 是质数」反向收尾,从而覆盖任意大的质数因子。质数按下标严格递增取,保证不漏不重。

图示解析

从 SS 到答案的主线

text
S = 42
 │  约数和公式 σ(n)=∏(1+p+…+p^a)
 ▼
把 S 拆成编号段之积 42 = G(p1,a1) × G(p2,a2) × …
 │  ├── a ≥ 2 的段:G(p,a) > p^2,故 p < √S < 44722 ──► 预筛质数 + p^2 > rest 剪枝
 │  └── a = 1 的段:G(p,1) = p+1 ────────────────► rest-1 是质数时一次收尾
 ▼
DFS (start, rest, cur)  ├─ rest == 1        ─► 收集 cur
                        ├─ rest-1 是质数    ─► 收集 cur×(rest-1)
                        └─ 枚举 p_j^2 ≤ rest 的编号段 ─► 递归 (j+1, rest÷段, cur×p^a)
 ▼
答案 {20, 26, 41} ──► 排序输出 m 与列表

图中三个分支分别对应:直接收尾(大质数答案)、剩余部分收尾、继续枚举指数 ⩾2\geqslant 2 的编号段。 只要注意到每个编号段必是 SS 的因子,就能理解为什么搜索空间与 nn 的上界无关。