解题思路

1、输入数据,获得数组a,并得到数组a中的最大值,记为maxa。

2、使用欧拉筛,计算 2到maxa的所有素数。

3、计算数组a中每个元素的素因子列表。

4、暴力枚举,递归+回溯,求最优解。

思路明确的话,写代码没有什么难度,就不做赘述了,下面详细讨论下素数的筛法。

素数的筛法

1、埃氏筛法(埃拉托斯特尼筛法,Sieve of Eratosthenes

该筛法的核心思想是从 2 开始,将每个素数的倍数标记为合数,逐步筛选出所有素数。具体来说,先假设所有数都是素数,然后从最小的素数 2 开始,把 2 的倍数(除 2 本身)都标记为合数;接着找到下一个未被标记的数,它就是素数,再把它的倍数标记为合数,依此类推,直到遍历完所有小于等于给定范围的数。

#include <iostream>
#include <vector>
using namespace std;

vector<int> prime; //素数集合
const int N = 10000;

int main() {
    //埃氏筛法求素数
    vector<bool> isP(N + 1, true);
    for (int i = 2; i <= N; ++i) {
        if (isP[i]) {
            prime.push_back(i);
            for (int j = i * i; j <= N; j += i) {
                isP[j] = false;
            }
        }
    }
}

埃氏筛法简单易懂,总结起来就是把所有素数的倍数全部标记为合数。

缺点是许多合数会有重复标记的问题,时间复杂度没有到最优。比如数字30,会被2、3、5分别标记一次。

2、欧拉筛法,或可称为线性筛法(Sieve of Eratosthenes with Linear Time Complexity)

该算法真正做到了每个合数只被标记一次。

其核心思想是,每个合数可以分解为其最小素因数和另一个数的乘积。依然拿数字30举例:30 = 2 * 15 = 3 * 10 = 5 * 6。其存在多种分解方式,但30的最小素因数是2,因此我们希望它只在 2 * 15的时候被标记一次。

有了这样的觉悟,我们再考虑怎么写代码。

很显然,我们不能像埃氏筛那样,只在出现新的素数时去标记其倍数作为合数了,而是必须对循环中的每一个数(无论素数还是合数),都去进行小范围的合数标记。

问题来了,这里的小范围,究竟有多小呢?

举个例子,假设现在循环到数字15,我们要标记一些15的倍数为合数,那么可选的数字有

30 = 2 * 15

45 = 3 * 15

60 = 4 * 15

75 = 5 * 15

……

很明显60 = 4 * 15 不能选,因为4是合数,还可以继续分解为 2 * 2,即60 = 2 * 30,我们应该在循环到数字30的时候去筛。也就是说,我们只考虑该数字乘以素数的情况。

那么75可以吗?显然也不行,75的最小素因子是3,即75 = 3 * 25,应该在循环到数字25的时候去筛。

正确的做法是,标记完30和45就停止。

再举个例子,假设现在循环到数字9,我们要标记一些9的倍数为合数,那么可选的数字有

18 = 2 * 9,27 = 3 * 9。到此停止,因为下一个45 = 5 * 9的最小素因子是3,45 = 3 * 15,该在循环到15时进行标记。

看到这里,聪明的小伙伴应该已经懂了。

对于任意一个数字k,我们依次将k和已有的每个素数p相乘,标记得到的k * p为合数。当k能被p整除时,跳出循环。

下面给出cpp代码。

#include <iostream>
#include <vector>
using namespace std;

vector<int> prime; //素数集合
int ans = 10000;

void dfs(vector<vector<int>> &pf, vector<int> &select, vector<bool> &flag) {
    int n = select.size();
    if(n == pf.size()) {
        int res = 0;
        for(int &i : select) {
            res += prime[i];
        }
        if(ans > res) ans = res;
        return;
    }
    for(int &i : pf[n]) {
        if(flag[i]) continue;
        flag[i] = true;
        select.push_back(i);
        dfs(pf, select, flag);
        select.pop_back();
        flag[i] = false;
    }
}

int main() {
    int n, maxa = 0;
    cin >> n;
    vector<int> a(n);
    for (int& i : a) {
        cin >> i;
        if (i > maxa) maxa = i;
    }
    //欧拉筛求素数
    vector<bool> isP(maxa + 1, true);
    for (int i = 2; i <= maxa; ++i) {
        if (isP[i]) prime.push_back(i);
        for (int j = 0; j < prime.size() && i * prime[j] <= maxa; ++j) {
            isP[i * prime[j]] = false;
            if (i % prime[j] == 0) break; //这一句是精髓
        }
    }
    //计算a中每个数的素因数
    vector<vector<int>> pf(n);
    for (int i = 0; i < n; ++i) {
        int k = a[i];
        for (int j = 0; k != 1; ++j) {
            if (k % prime[j]) continue;
            pf[i].push_back(j);
            do {
                k /= prime[j];
            } while (k % prime[j] == 0);
        }
    }
    //暴力枚举
    vector<int> select;
    vector<bool> flag(prime.size(), false);
    dfs(pf, select, flag);
    cout << (ans == 10000 ? -1 : ans);
}
// 64 位输出请用 printf("%lld")