[信息与未来 2016] 素数分解

筛出≤n的所有素数,再做0/1背包计数取max:dp[j]=max(dp[j], dp[j-p]+1),求最多项数。

OJ: luogu

题目 ID: B4141

难度:普及-

标签:动态规划01背包素数

日期: 2026-08-08 23:13

题意

输入一个正整数 nn (10n20010\le n\le 200),要求把 nn 分解成若干个互不相同的素数的和,问最多能分解成多少个素数。

例如 21=2+3+5+1121=2+3+5+11,用了 4 个互不相同的素数,所以答案是 4。

思路

一句话本质:在不超过 nn 的素数集合中做 0/1 背包,每个素数最多选一次,求最多能选几个素数使和恰好为 nn

选出哪些素数才能让项数最多?

题面要求把 nn 写成若干互不相同素数的和。直接的想法是把所有不超过 nn 的素数全列出来,然后从中选一个子集。每个素数要么选、要么不选——这正是 0/1 背包的模型。

"互不相同"给定了什么约束?

它强制每个素数在整个分解中最多出现一次。所以不能把一个素数重复使用,只能是每个素数用 0 次或 1 次。

选或不选时,状态需要记住什么?

对于每一个素数 pp,如果选它,当前和就增加 pp,项数加 1;不选则维持原样。我们只关心"当前和是多少"以及"最多能有多少项",不需要记住具体选了哪些素数。

因此设 dp[j]dp[j] 表示:选一些互不相同的素数,使它们的和恰好为 jj 时,最多能选多少个素数。初始化 dp[0]=0dp[0]=0,其余 dp[j]=1dp[j]=-1 表示目前无法凑出和 jj

转移顺序怎么定?

外层枚举素数列表中的每一个 pp,内层容量从 nn 倒序到 pp。倒序是因为每个素数只能用一次(0/1 背包性质):

dp[j]=max(dp[j],  dp[jp]+1)(当 dp[jp]1 时)dp[j] = \max(dp[j],\; dp[j-p]+1) \quad (\text{当 } dp[j-p]\neq -1 \text{ 时})

其中 dp[jp]1dp[j-p]\neq -1 保证转移来源是一个可达状态。

转移为什么在 dp[jp]dp[j-p] 上加 1?

因为"选 pp"意味着在原来凑出 jpj-p 的方案中多用一个素数 pp,项数自然加 1。一路取 max\max 就能得到对于每个和 jj 的最多素数项数。

最终答案是 dp[n]dp[n]。题目保证有解,所以 dp[n]dp[n] 一定 2\ge 2(至少 nn 本身可能是素数,但最小 n=10n=1010=3+710=3+7 两项)。

代码

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

const int MAXN = 205;

int n;
bool is_prime[MAXN];           // 埃氏筛标记
vector<int> primes;            // ≤n 的所有素数
// dp[j] 表示和为 j 的素数分解方案数(这里求的是最多项数)。
int dp[MAXN];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;

    fill(is_prime, is_prime + n + 1, true);
    is_prime[0] = is_prime[1] = false;
    for (int i = 2; i * i <= n; i++) {
        if (is_prime[i]) {
            for (int j = i * i; j <= n; j += i) {
                is_prime[j] = false;
            }
        }
    }
    for (int i = 2; i <= n; i++) {
        if (is_prime[i]) primes.push_back(i);
    }

    // dp 初始化为 -1 表示不可达,dp[0] = 0 表示和为 0 用 0 项。
    fill(dp, dp + n + 1, -1);
    dp[0] = 0;
    // 0/1 背包:每个素数最多用一次,从大到小编排容量。
    for (int p : primes) {
        for (int j = n; j >= p; j--) {
            if (dp[j - p] != -1) {
                dp[j] = max(dp[j], dp[j - p] + 1);
            }
        }
    }

    cout << dp[n] << '\n';
    return 0;
}

复杂度

  • 筛素数:O(nloglogn)O(n\log\log n)
  • DP:O(π(n)n)O(\pi(n)\cdot n),其中 π(n)\pi(n) 是不超过 nn 的素数个数,n200n\le 200 时数值很小
  • 空间复杂度:O(n)O(n)
  • 总体在 n200n\le 200 下轻松跑过

总结

这题把"素数分解"和"背包计数取 max"结合在一起。筛出素数之后就和普通的 0/1 背包一样:每件物品重量为 pp,"价值"是使用次数(+1+1),目标是最大化总价值(项数)且总重量恰好为 nn