4 条题解
-
0
满分解法(100 分)
把因数个数写成质因数指数的乘积
困难在于, 的取值很多,而每个位置对同一个乘数的收益又不相同。先看乘上一个质数会改变什么。
若 ,它的每个正因数都由各质数的指数独立选出,第 项有 种选择,因此
给 再乘一个质数 ,只会把对应的因子从 改成 ,其他质数的贡献完全不变。
先确定可以把 全部用完。若某个可行方案中 ,因为 ,剩余的 中至少有一个质因数。把它乘到任意一个 上,方案仍然合法,而且对应的因数个数严格增加。因此最优方案一定满足 ; 时则只能全部取 。
不同质数可以分别决定分配
设
对固定的质数 ,记 为原来 中 的指数, 为分配给 的 的个数。合法分配恰好满足
而这个质数对目标的贡献是
不同质数的额度约束互不影响,目标也只是把它们的贡献相乘。把各个质数的最优分配合在一起,令 ,就会得到乘积恰为 的合法序列。因此可以分别最大化各个质数的贡献,再相乘。
不整除 的质数不能分配,其贡献保持不变。实现时,每处理一个 ,就从所有 中除尽 ,并记录除去的次数。所有这类质数都处理完以后,剩余值记为 ,它只含不能改变的质因数,最后再乘上 即可。
一份质因子应该给谁
现在只考虑一种质数。假设某个位置当前的指数为 ,给它增加一份质因子的倍率为
越小,这个倍率越大。于是每次选取当前指数最小的位置,给它加一,再进行下一次选择。比较的是这个质数的指数,不是 的大小,也不是 。
例如当前指数为 ,要分配三份同一种质因子:
已分配份数 一种贪心分配后的指数 当前贡献 第一次加一后,需要重新比较。若把三份都给原先最小的位置,得到 ,贡献只有 。
只比较当前倍率还需要说明不会影响后面的最优性。设位置 的当前指数最小。取一个最优的剩余分配:若它已经给 至少一份,就可以先给这一份;否则,选择一个得到至少一份的位置 ,从它转移一份给 。
设 在这个方案中的最终指数为 。由于它至少得到一份,且原来的指数不小于 ,有 。转移前后,这两个位置的贡献之差为
所以总能找到一个同样最优的方案,让当前最小的位置先得到一份。完成这一步后,问题仍是同样的分配问题,对剩余份数重复论证即可。相同指数任选一个位置都成立。
用有限次扫描完成分配
中所有质因子的总份数记为 。每一份至少是 ,所以 。本题 ,从而 。
因此每次直接扫描 个指数,找到最小值即可;全部质数合计至多扫描 次。每个质数分配完毕后,把所有最终的“指数加一”乘入答案。
为了计算剩余值的因数个数,令 为原输入的最大值,预处理 到 的因数个数。枚举一个因数 ,给它的所有倍数 各加一。每对整除关系恰好统计一次,因此表中保存的就是 ,其中 。
完整步骤是:先预处理因数个数并分解 ;随后逐个处理 的质因数,提取各位置的指数、逐份执行贪心、累乘对应贡献;最后累乘剩余值的因数个数。全程只保存指数和逐步取模的乘积,无需构造 。取模仅用于记录已经确定的最优乘积,不参与方案优劣的比较。
时间复杂度为 。因数表共更新 次;提取指数时,每次成功除法都使某个值至少减半;逐份寻找最小值共 次比较; 用试除分解。
空间复杂度为 ,包括因数表、剩余值、当前质数的指数和 的分解结果。
AC 代码(C++20)
#include <bits/stdc++.h> using namespace std; using i64 = long long; void solve() { int n, k; cin >> n >> k; vector<int> a(n); for (int& x : a) cin >> x; const int limit = *max_element(a.begin(), a.end()); constexpr int mod = 998244353; vector<int> div_cnt(limit + 1); for (int d = 1; d <= limit; d++) { for (int j = d; j <= limit; j += d) div_cnt[j]++; } vector<pair<int, int>> factors; int x = k; for (int p = 2; p <= x / p; p++) { if (x % p != 0) continue; int cnt = 0; while (x % p == 0) { x /= p; cnt++; } factors.push_back({p, cnt}); } if (x > 1) factors.push_back({x, 1}); i64 ans = 1; for (auto [p, cnt] : factors) { vector<int> exponent(n); for (int i = 0; i < n; i++) { while (a[i] % p == 0) { a[i] /= p; exponent[i]++; } } while (cnt--) { auto it = min_element(exponent.begin(), exponent.end()); (*it)++; } for (int e : exponent) ans = ans * (e + 1) % mod; } for (int x : a) ans = ans * div_cnt[x] % mod; cout << ans << '\n'; } int main() { cin.tie(0)->sync_with_stdio(0); solve(); } -
0
逐个枚举因数(通过子任务 2、4,共 50 分)
把因数统计留作直接枚举
当所有数都不超过 时,可以保留朴素的因数个数计算:对一个数 ,枚举 ,累计满足 的个数。若 ,即使 达到 ,这样统计也很快。
分配部分仍按质数处理。由 , 中不同质因子的分配限制互相独立,目标是各自贡献的乘积;把所有剩余因子用完一定更优。固定 ,若各位置当前指数为 ,增加一次的倍率为 ,应优先增加当前最小指数。
这个选择可以用交换证明:若最优剩余方案没给最小指数 分配,就从一个得到分配的位置移一份过来。设后者在方案中的最终指数为 ,则 ,两处乘积的变化是
因此总有最优方案包含这次选择,之后可以继续选最小指数。比如 时,把六份 分别给六个位置,得到 ;每次都重新找最小指数就会做出这样的分配。
先分解 。对每个质数,反复除尽所有 中的该因子,记录指数,逐份扫描并增加当前最小指数,最后乘入这一质数的全部贡献。所有质数处理完后,对剩余的每个 逐个枚举正因数,乘入相应个数。余数不含可分配的质因数,故这一步恰好补足固定贡献。各次乘法及时对 取模;分配规则不比较模值。
令 。时间复杂度为 ,其中因数枚举的实际次数是各个剩余值之和,至多 。空间复杂度为 。
子任务 的因数枚举次数至多 ,子任务 至多 ,所以本方法覆盖子任务 。全范围下 可达 ,需要把重复的因数统计改为预处理。
参考代码(C++20)
#include <bits/stdc++.h> using namespace std; using i64 = long long; void solve() { int n, k; cin >> n >> k; vector<int> a(n); for (int& x : a) cin >> x; constexpr int mod = 998244353; vector<pair<int, int>> factors; int x = k; for (int p = 2; p <= x / p; p++) { if (x % p != 0) continue; int cnt = 0; while (x % p == 0) { x /= p; cnt++; } factors.push_back({p, cnt}); } if (x > 1) factors.push_back({x, 1}); i64 ans = 1; for (auto [p, cnt] : factors) { vector<int> exponent(n); for (int i = 0; i < n; i++) { while (a[i] % p == 0) { a[i] /= p; exponent[i]++; } } while (cnt--) { auto it = min_element(exponent.begin(), exponent.end()); (*it)++; } for (int e : exponent) ans = ans * (e + 1) % mod; } for (int x : a) { int cnt = 0; for (int d = 1; d <= x; d++) { if (x % d == 0) cnt++; } ans = ans * cnt % mod; } cout << ans << '\n'; } int main() { cin.tie(0)->sync_with_stdio(0); solve(); } -
0
枚举指数分配(通过子任务 1–2,共 25 分)
用小 枚举每种质数的分配
增加任意一个剩余质因子都会让某个位置的因数个数变大,所以最优方案会用完 。若 ,由每个因数对各个指数的独立选择可得 。
固定一个整除 的质数 。设它在 中出现 次,在各个 中出现 次。分配 份给位置 后,它对答案的贡献为
$$\prod_{i=1}^n(e_i+t_i+1),\qquad t_i\ge0,\quad\sum_i t_i=E.$$不同质数的约束和贡献相互独立;各自的分配能通过 合成合法答案。因此可以独立求每个质数的最大贡献。
当 时,直接枚举所有 。递归记录下一个位置、未分配份数和已确定位置的实际乘积。到一个位置时,枚举给它 到全部剩余份数,乘上相应的 ;最后一个位置必须得到所有剩余份数。这样每个合法分配恰好出现一次,取叶子处乘积的最大值即可。比较期间不取模,最大值确定以后再乘入模意义下的答案。
例如两个位置的初始指数为 ,。三种分配 的贡献分别为 ,枚举就能选择最后一种,无需事先得出分配规则。
处理 时从 中除尽 来获得 。全部处理完后,剩余值只含不整除 的质因数,其贡献不能改变,直接把它们的因数个数相乘。因数个数用枚举约数、更新其所有倍数的筛法预处理。
本方法对 成立。各个原始指数与 都不超过 ,这里需要比较的单质数贡献至多为 ,可以直接保存实际整数。若 ,没有分配过程,任意 都只执行因数个数相乘,所以也覆盖子任务 。本方法声明覆盖子任务 ,对其他范围不保证递归成本与实际乘积的表示。
令 。时间复杂度为 $O(A\log(A+1)+\sqrt{k}+n\log(A+1)+n\sum_{p\mid k}\binom{E_p+n-1}{n-1})$。每个质数有 种非负分配,一条递归路径长度至多 ;当 时,每种质数至多 种分配。
空间复杂度为 ,包含输入、因数表、指数、分解结果与递归栈。
参考代码(C++20)
#include <bits/stdc++.h> using namespace std; using i64 = long long; void solve() { int n, k; cin >> n >> k; vector<int> a(n); for (int& x : a) cin >> x; const int limit = *max_element(a.begin(), a.end()); constexpr int mod = 998244353; vector<int> div_cnt(limit + 1); for (int d = 1; d <= limit; d++) { for (int j = d; j <= limit; j += d) div_cnt[j]++; } vector<pair<int, int>> factors; int x = k; for (int p = 2; p <= x / p; p++) { if (x % p != 0) continue; int cnt = 0; while (x % p == 0) { x /= p; cnt++; } factors.push_back({p, cnt}); } if (x > 1) factors.push_back({x, 1}); i64 ans = 1; for (auto [p, cnt] : factors) { vector<int> exponent(n); for (int i = 0; i < n; i++) { while (a[i] % p == 0) { a[i] /= p; exponent[i]++; } } i64 best = 0; auto dfs = [&](auto&& self, int pos, int rem, i64 prod) -> void { if (pos == n - 1) { best = max(best, prod * (exponent[pos] + rem + 1)); return; } for (int t = 0; t <= rem; t++) { self(self, pos + 1, rem - t, prod * (exponent[pos] + t + 1)); } }; dfs(dfs, 0, cnt, 1); ans = ans * (best % mod) % mod; } for (int x : a) ans = ans * div_cnt[x] % mod; cout << ans << '\n'; } int main() { cin.tie(0)->sync_with_stdio(0); solve(); } -
0
至多一份质因子(通过子任务 1、3,共 20 分)
与
当 时,所有 只能为 ,答案就是 。
当 时,可以使一个位置乘上 ,其余位置保持不变。使用这个因子会增加因数个数,因此一定会使用。
把 写成 ,其中 为奇数,则 。若把唯一的 分给位置 ,目标会乘上
于是只需选择 最小的位置,把它的指数加一。比如 时,两个指数为 ,应让 乘上 :此时因数个数乘积为 ,大于让 乘上 得到的 。
用反复除以 得到全部 ,根据 决定是否增加一次最小指数,再计算 。预处理 时,枚举每个正整数 ,给它的每个倍数的计数加一。每个真正的因数恰好贡献一次,故该表准确统计因数个数。
本方法保证 ,覆盖子任务 。代码中的一次分配只处理 的情况,其他 不属于适用范围。
令 。时间复杂度为 ,空间复杂度为 。
参考代码(C++20)
#include <bits/stdc++.h> using namespace std; using i64 = long long; void solve() { int n, k; cin >> n >> k; vector<int> a(n); for (int& x : a) cin >> x; const int limit = *max_element(a.begin(), a.end()); constexpr int mod = 998244353; vector<int> div_cnt(limit + 1); for (int d = 1; d <= limit; d++) { for (int j = d; j <= limit; j += d) div_cnt[j]++; } vector<int> exponent(n); for (int i = 0; i < n; i++) { while (a[i] % 2 == 0) { a[i] /= 2; exponent[i]++; } } if (k == 2) { auto it = min_element(exponent.begin(), exponent.end()); (*it)++; } i64 ans = 1; for (int i = 0; i < n; i++) { ans = ans * (exponent[i] + 1) % mod * div_cnt[a[i]] % mod; } cout << ans << '\n'; } int main() { cin.tie(0)->sync_with_stdio(0); solve(); }
- 1
信息
- ID
- 2304
- 时间
- 1000ms
- 内存
- 512MiB
- 难度
- 9
- 标签
- (无)
- 递交数
- 84
- 已通过
- 21
- 上传者