1 条题解
-
0
2685. 「BalticOI 2013」Brunhilda 的生日 题解
问题分析
我们有 个素数 ,每次操作可以:
- 选择素数
- 去掉当前人数 对 取模的人数,即去掉 人
- 剩余人数变为
关键点:
- 去掉后剩余人数 = =
- 也就是说,剩余人数必须是 的倍数
问题转化
设 表示将 人变为 0 所需的最小操作次数。
状态转移:
$$f(n) = \min_{p \in \text{primes}} f\left(n - (n \bmod p)\right) + 1 $$其中 $n - (n \bmod p) = \lfloor \frac{n}{p} \rfloor \times p$
边界:
重要观察:
- 如果 能被某个 整除,那么 ,一步可以去掉 0 人,无意义
- 实际上,只有当 时,操作才有意义
- 所以,我们要找 使得 不是 的倍数
关键性质
1. 转移的限制
从 可以转移到哪些状态? 对于素数 ,如果 ,那么:
$$n \to n' = n - (n \bmod p) = \left\lfloor \frac{n}{p} \right\rfloor \times p $$所以 是 的倍数,且 。
2. 无解情况
什么时候无解(输出 "oo")? 如果对于某个 ,所有素数 都满足 (即 是所有素数的倍数),那么无法进行任何有效操作。
更准确:如果 的所有素因子都不在给定的素数集合中,且 不能被任何给定的素数整除到可以操作的地步。
实际上,我们需要判断是否存在 使得 。
3. 更高效的思考
我们可以从 0 开始反向思考:从 0 开始,通过逆操作能到达哪些数字?
逆操作:从 人,通过一次操作可以到达 人,其中 ,且存在素数 使得:
$$y = x + k \quad \text{其中} \quad 1 \le k < p \quad \text{且} \quad y \bmod p = 0 $$实际上更简单: 必须是某个 的倍数,且 但 不确定。
其实更直接的动态规划:从大到小计算 。
算法设计
1. 朴素DP(不可行)
直接计算 对于 需要 , 太大。
2. 优化转移
观察转移方程:
$$f(n) = \min_{p} f\left(\left\lfloor \frac{n}{p} \right\rfloor \times p\right) + 1 $$对于固定的 , 是 的倍数,且是小于等于 的最大 的倍数。
我们可以考虑:对于每个 的倍数 ,它能更新哪些 ?
如果 在区间 中,那么 ,所以 可以转移到 。
因此:
$$f(n) = \min_{p} f(k \times p) + 1 \quad \text{其中} \quad k = \lfloor \frac{n}{p} \rfloor, \quad n > k \times p $$3. 预处理每个位置的最大素数
定义 为能整除 的最大素数(在给定的素数集合中),如果没有则为 0。
那么对于 ,我们可以选择 吗?不一定,因为如果 ,操作无效。
实际上,我们需要 不是 的倍数,所以 不能是 的因子。
更好的方法:对于每个 ,找到最大的素数 使得 且 。
4. 更聪明的DP
设 表示将 人变为 0 的最小步数。
我们可以从 向更小的数转移:
$$dp[n] = \min_{p \in P, \, n \bmod p > 0} dp\left[n - (n \bmod p)\right] + 1 $$优化:对于每个 ,我们不需要检查所有 ,只需要检查 的最大素因子相关的素数。
5. 关键突破
定义 为:从 开始,通过一次操作能到达的最小数字(即最优转移目标)。
实际上,,其中 是满足 的最大素数。
证明:使用更大的素数 可以去掉更多的人(因为 更大),所以应该选择最大的满足条件的素数。
因此:
- 对于每个 ,找到最大的素数 使得 且
- 然后
6. 如何快速找到最大素数
我们需要预处理:对于每个 ,找到最大的素数 (在给定集合中)使得 。
等价于:找到最大的素数 使得 不整除 。
我们可以用筛法预处理:
vector<int> max_prime(n_max + 1, 0); for (int p : primes) { for (int multiple = p; multiple <= n_max; multiple += p) { max_prime[multiple] = max(max_prime[multiple], p); } }但这里记录的是能整除 的最大素数,我们需要的是不整除 的最大素数。
7. 另一种思路:从大到小更新
对于每个素数 ,考虑所有 满足 。对于这样的 , 可以转移到 。
我们可以用 更新 。
具体实现:对于每个 ,对于每个 ,考虑区间 中的 ,它们都可以转移到 。
所以:
$$dp[n] = \min(dp[n], dp[k \times p] + 1) \quad \text{对于} \quad n \in [k \times p + 1, (k+1) \times p - 1] $$我们可以用区间更新来优化。
完整算法
1. 预处理
const int MAXN = 10000000; // n的最大值 vector<int> primes; // 给定的素数 int m, Q; int dp[MAXN + 5]; int max_transfer[MAXN + 5]; // max_transfer[n] = 最大的p使得n mod p > 02. 计算 max_transfer
对于每个素数 ,对于每个倍数 :
- 区间 中的数 都可以被 转移
- 我们记录这些数能被哪个素数转移,取最大的素数
fill(max_transfer, max_transfer + MAXN + 1, 0); for (int p : primes) { for (long long k = 1; k * p <= MAXN; k++) { long long L = k * p + 1; long long R = min((long long)MAXN, (k + 1) * p - 1); if (L > R) continue; // 标记区间[L, R]可以用p转移 // 我们需要区间更新,但可以用后缀最大值 // 更简单:从后向前更新 } }更高效的方法:对于每个 ,我们需要知道能整除 的所有素数中最大的一个,然后找一个不整除 的素数。
实际上,我们可以:
- 找到 的最大素因子 (在给定素数中)
- 如果 (即 不能被任何给定素数整除),那么无解
- 否则,选择 吗?不, 整除 ,所以 ,无效
- 我们需要找第二大的素数?
3. 最终算法
实际上,官方题解使用的方法是:
定义 为最小步数,则:
$$f(n) = \min_{p \in P, \, p \mid (n - r)} f(n - r) + 1 \quad \text{其中} \quad 1 \le r < p $$但 必须是 的倍数。
等价地:$f(k \times p) = \min_{1 \le r < p} f(k \times p + r) + 1$
所以对于每个 的倍数 ,它可以更新区间 中的数。
我们可以用 BFS 的方式计算:
- 从 0 开始,步数为 0
- 对于当前步数 step,处理所有步数为 step 的数字 x
- 对于每个素数 p,对于 r = 1 到 p-1,如果 x+r 还未访问,则 f[x+r] = step+1
- 但这样复杂度太高
4. 优化算法
我们维护一个数组 表示: 最早能被哪个数更新。
从 0 开始:
- 0 可以更新区间 [1, p-1] 对于所有 p
- 取这些区间的并集,得到 step=1 能到达的数字
然后对于这些数字,继续更新。
具体实现:
#include <bits/stdc++.h> using namespace std; const int MAXN = 10000000; const int INF = 1e9; int main() { ios::sync_with_stdio(false); cin.tie(0); int m, Q; cin >> m >> Q; vector<int> primes(m); for (int i = 0; i < m; i++) { cin >> primes[i]; } // 预处理每个位置的最大可用素数 vector<int> max_prime(MAXN + 1, 0); for (int p : primes) { for (int multiple = p; multiple <= MAXN; multiple += p) { // 实际上我们需要的是:对于n,最大的p使得n mod p > 0 // 这里我们先标记p能整除哪些数 } } // 更高效的方法:DP vector<int> dp(MAXN + 1, INF); dp[0] = 0; // 对于每个素数p,更新能到达的数字 for (int p : primes) { for (int k = 0; k * p <= MAXN; k++) { int x = k * p; if (dp[x] == INF) continue; // x可以更新区间[x+1, x+p-1] int R = min(MAXN, x + p - 1); for (int y = x + 1; y <= R; y++) { if (dp[y] > dp[x] + 1) { dp[y] = dp[x] + 1; } } } } // 但这样还是O(MAXN * m)太慢 // 需要优化区间更新 // 我们可以维护每个位置的最小步数,用单调队列优化 }5. 官方解法
实际上,标准解法是:
定义 为最小步数,则:
$$f(n) = 1 + \min_{p \in P} f\left(\left\lfloor \frac{n}{p} \right\rfloor \times p\right) $$条件:
我们可以预处理:
- = 能使得 的最大的
- 然后
如何计算 ? 我们可以用筛法:对于每个素数 ,所有 的倍数 都不能用 转移(因为余数为0)。 所以对于非 的倍数, 是一个候选。
我们从大到小处理素数,对于每个素数 :
- 对于所有 不是 的倍数,如果 ,则更新
实现:
vector<int> g(MAXN + 1, 0); for (int p : primes) { for (int n = p; n <= MAXN; n += p) { // n是p的倍数,不能用p转移 // 我们需要标记非倍数 } }更好的方法:先初始化为最大值,然后对于每个 的倍数,将其 值减小。
6. 最终实现
下面是可行的实现:
#include <bits/stdc++.h> using namespace std; const int MAXN = 10000000; const int INF = 1e9; int main() { ios::sync_with_stdio(false); cin.tie(0); int m, Q; cin >> m >> Q; vector<int> primes(m); for (int i = 0; i < m; i++) { cin >> primes[i]; } // 预处理每个数的最大可用素数 vector<int> max_prime(MAXN + 1, 0); // 初始化:对于每个数,最大的可用素数是所有素数中的最大值 int max_p = *max_element(primes.begin(), primes.end()); for (int i = 1; i <= MAXN; i++) { max_prime[i] = max_p; } // 对于每个素数p,所有p的倍数都不能用p,所以将它们的max_prime设为更小的值 // 实际上,我们需要的是:对于n,最大的p使得n mod p > 0 // 所以对于p的倍数n,p不能用于n,但可能用于n-1, n-2等 // 更简单:我们直接计算dp vector<int> dp(MAXN + 1, INF); dp[0] = 0; // 使用BFS vector<int> next_update(MAXN + 1, INF); // 初始:0可以更新区间[1, p-1]对于所有p for (int p : primes) { for (int r = 1; r < p && r <= MAXN; r++) { dp[r] = min(dp[r], 1); } } // 然后对于每个已经确定的位置,继续更新 for (int n = 1; n <= MAXN; n++) { if (dp[n] == INF) continue; for (int p : primes) { // n可以更新哪些位置? // 对于k = floor(n/p),n可以更新区间[k*p+1, (k+1)*p-1] long long k = n / p; long long L = k * p + 1; long long R = min((long long)MAXN, (k + 1) * p - 1); if (L > R) continue; // 更新区间[L, R] for (long long y = L; y <= R; y++) { if (dp[y] > dp[n] + 1) { dp[y] = dp[n] + 1; } } } } // 回答查询 while (Q--) { int n; cin >> n; if (dp[n] == INF) { cout << "oo\n"; } else { cout << dp[n] << "\n"; } } return 0; }复杂度优化
上面的代码还是太慢。我们需要 。
实际的正解是:
- 预处理 : 的最大素因子(在给定素数中)
- 然后
- 如果 或 ,则无解
预处理 可以用筛法 。
vector<int> max_factor(MAXN + 1, 0); for (int p : primes) { for (int multiple = p; multiple <= MAXN; multiple += p) { max_factor[multiple] = p; } } vector<int> dp(MAXN + 1, INF); dp[0] = 0; for (int n = 1; n <= MAXN; n++) { int p = max_factor[n]; if (p == 0) { // n不能被任何给定素数整除 continue; } if (n % p == 0) { // 如果n是p的倍数,需要找下一个最大的 // 实际上,我们需要最大的q使得n mod q > 0 // 这里简化:如果n是p的倍数,看n/p int target = n - p; // 去掉p个人? // 这样不对 } else { int target = n - (n % p); if (dp[target] != INF) { dp[n] = dp[target] + 1; } } }总结
本题的核心是理解状态转移,并优化DP计算。由于 可达 ,需要 或 的算法。
正确解法:
- 预处理每个数的最大可用素数
- 使用DP递推计算最小步数
- 对于无解情况特殊处理
对于比赛,可能需要参考官方题解实现更高效的算法。
- 1
信息
- ID
- 6163
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 2
- 已通过
- 1
- 上传者