题解:学而思编程 真因数之和

【题目来源】

学而思编程:真因数之和

【题目描述】

完全数指的是恰好等于自己全部真因数之和的数,这样的数十分稀少。

我们现在把条件放宽一点,想要从 \(n\) 的真因数中可以选出若干个不同数,使得它们的和等于 \(n\)。我们想知道有多少种取数的方式,输入包含多组数据。

例如 \(n=24\) 的真因数有 \(1,2,3,4,6,8,12\)\(7\) 个,取出若干不同数使得和等于 \(24\) 的方法有 \(5\) 种:

\(1+2+3+6+12\)\(1+3+8+12\)\(2+4+6+12\)\(4+8+12\)\(1+2+3+4+6+8\)

【输入】

\(1\) 行,\(1\) 个正整数 \(T\)

接下来 \(T\) 行,每行 \(1\) 个正整数 \(n\)

【输出】

输出 \(T\) 行,对输入中每个 \(n\),输出取数方法数。

【输入样例】

3
6
24
15

【输出样例】

1
5
0

【核心思想】

  1. 问题分析:给定 \(T\) 个查询,每个查询给出一个正整数 \(n\),求从 \(n\) 的真因数中选出若干个不同的数,使得它们的和等于 \(n\) 的方案数。真因数是指小于 \(n\) 且能整除 \(n\) 的正整数。这是一个预处理 + 计数型01背包问题,关键在于预处理所有数的真因数,然后对每个查询使用01背包计算方案数。

  2. 算法选择

    • 预处理真因数:使用筛法思想,对于每个因数 \(i\),将其加入所有倍数 \(j\)\(j = 2i, 3i, ...\))的真因数列表中
    • 计数型01背包dp[j] 表示和为 \(j\) 的方案数,状态转移 dp[j] += dp[j - num],逆序枚举保证每个数只用一次
    • 多组查询处理:预处理所有数的真因数,每个查询独立做01背包
  3. 关键步骤

    • 预处理真因数(筛法):
      • 枚举 \(i\)\(1\)\(MAXN\)
      • 对于每个 \(i\),枚举其倍数 \(j = 2i, 3i, ... \leq MAXN\)
      • \(i\) 加入 a[j]\(j\) 的真因数列表)
    • 处理每个查询 \(x\)
      • 初始化 dp[0] = 1(和为0的方案数为1,空集),其余 dp[i] = 0
      • 01背包计数:遍历 \(x\) 的每个真因数 \(num\)
        • 逆序枚举 \(j\)\(x\)\(num\)dp[j] += dp[j - num]
      • 返回 dp[x](和为 \(x\) 的方案数)
  4. 时间/空间复杂度

    • 预处理时间复杂度:\(O(MAXN \log MAXN)\),每个数 \(i\) 贡献给 \(MAXN/i\) 个倍数
    • 单次查询时间复杂度:\(O(d(n) \times n)\)\(d(n)\)\(n\) 的真因数个数,最坏情况约为 \(O(n \sqrt{n})\)
    • 总时间复杂度:\(O(MAXN \log MAXN + T \times d(n) \times n)\)
    • 空间复杂度:\(O(MAXN \times \text{平均真因数个数})\),存储所有数的真因数列表
  5. 计数型01背包与预处理的核心思想

    • 计数型01背包:与最值型不同,dp[j] 表示方案数而非最大价值,状态转移为累加而非取最值
    • 逆序枚举:保证每个物品(真因数)只被使用一次,避免重复计算
    • 筛法预处理:利用倍数关系一次性预处理所有数的真因数,避免对每个查询单独分解质因数
    • 空集初始化dp[0] = 1 表示和为0有一种方案(不选任何数),是计数型背包的基础
    • 适用于子集和方案数统计、真因数分解、多组查询类问题

【算法标签】

01背包

【代码详解】

#include <bits/stdc++.h>
using namespace std;

// 定义长整型别名,便于处理大数据
#define int long long

// ================= 常量与全局变量 =================

const int N = 10005;    // 数值上限

int n;                  // 查询次数
vector<int> a[N];       // a[x] 存储 x 的所有真因数(不包括 x 本身)
int dp[N];              // 动态规划数组,用于计算分解方案数

// ================= 预处理:求每个数的所有真因数 =================
void init()
{
    // 枚举所有可能的因数 i
    for (int i = 1; i <= 10000; i++)
    {
        // j 是 i 的倍数,即 i 是 j 的真因数
        for (int j = i + i; j <= 10000; j += i)
        {
            a[j].push_back(i);      // 将 i 加入 j 的真因数列表
        }
    }
}

// ================= 计算将 x 分解为其真因数之和的方案数 =================
int solve(int x)
{
    // 初始化 dp 数组
    memset(dp, 0, sizeof(dp));
    dp[0] = 1;                      // 和为 0 的方案数为 1(空集)

    // 01 背包:每个真因数只能使用一次
    for (int num : a[x])            // 遍历 x 的所有真因数
    {
        for (int j = x; j >= num; j--)  // 逆序枚举,保证每个数只用一次
        {
            dp[j] += dp[j - num];   // 累加方案数
        }
    }

    return dp[x];                   // 返回和为 x 的方案数
}

// ================= 主函数 =================
signed main()
{
    // 预处理所有数的真因数
    init();

    // 读取查询次数
    cin >> n;

    // 处理每个查询
    while (n--)
    {
        int x;
        cin >> x;                   // 读取目标数
        cout << solve(x) << endl;   // 输出分解方案数
    }

    return 0;
}

【运行结果】

3
6
1
24
5
15
0
posted @ 2026-06-17 17:24  团爸讲算法  阅读(10)  评论(0)    收藏  举报