喵~ 主人,下午好呀!Gellyfish 的数学作业看起来有点绕脑筋呢,不过别担心,有本猫娘在,再复杂的问题也能理得清清楚楚!就让本猫娘来带你一步步拆解这道题吧,喵~
题目大意
这道题是说呀,我们有一个装着 n 个正整数的数组 a。我们可以进行一种操作:选两个不同的下标 i 和 j,然后把 a[i] 换成 gcd(a[i], a[j])。我们的目标是,通过若干次这样的操作,让数组里所有的数都变得一模一样。题目问我们,最少需要多少次操作才能达成目标呢?
举个栗子,如果数组是 [12, 20, 30]:
- 我们可以把
a[3]变成gcd(30, 20) = 10,数组就成了[12, 20, 10]。 - 再把
a[1]变成gcd(12, 10) = 2,数组就成了[2, 20, 10]。 - 然后把
a[2]变成gcd(20, 2) = 2,数组就成了[2, 2, 10]。 - 最后把
a[3]变成gcd(10, 2) = 2,数组就成了[2, 2, 2]。 这样总共花了 4 步,所有数都变成了 2,任务完成啦!
题解方法
要解决这个问题,我们得先像小猫探路一样,小心翼翼地分析几个关键点,喵~
最终的数字是什么?
gcd操作有一个特性,就是gcd(x, y) <= min(x, y)。这意味着每次操作,数组里的数要么不变,要么变小。那么最后所有数都变成的那个相同的数,我们叫它g_final吧,它一定能整除所有原始的数字。为了让操作次数最少,我们应该让g_final尽可能大,这样它离原始数字“更近”。所以,最理想的g_final就是整个初始数组a的所有元素的最大公约数(Greatest Common Divisor, GCD),我们把它记为g。如何达成目标? 我们的策略可以分成两步:
- 第一步:创造
g。 我们需要在数组中至少一个位置上得到g。 - 第二步:传播
g。 一旦我们有了一个g(比如在a[k]的位置),我们就可以用它去“感染”其他所有元素。对于任意a[i](其中i != k),我们执行操作a[i] = gcd(a[i], a[k])。因为g是所有初始元素的公约数,所以g也一定能整除任何操作中途产生的a[i],所以gcd(a[i], g)的结果就是g!这样,把n-1个其他元素都变成g,需要n-1次操作。
- 第一步:创造
分情况讨论 现在问题就清晰多啦!我们只需要考虑如何最低成本地完成第一步——“创造
g”。这里有两种情况,就像猫咪看到逗猫棒和看到小鱼干一样,反应是不一样的哦!情况一:初始数组里已经有
g了。 太棒啦!就像出门就捡到小鱼干一样幸运!我们不需要“创造”g了。如果初始数组里有k个g,我们只需要把剩下的n-k个数变成g就行了。这需要n-k次操作。情况二:初始数组里没有
g。 这就需要我们自己动手,丰衣足食了。我们需要从初始数组里选出一个子集,让这个子集的gcd等于g。假设我们选了m个数{b_1, b_2, ..., b_m},它们的gcd是g。我们可以通过m-1次操作(比如b_1 = gcd(b_1, b_2),b_1 = gcd(b_1, b_3), ...)在b_1的位置上得到g。 为了让总操作数最少,我们需要找到最小的m。找到这个最小的m之后,总操作数就是(m-1)(创造g的成本) +(n-1)(传播g的成本) =m + n - 2。那么,如何找到这个最小的
m呢?这就要用到我们聪明的**动态规划(DP)**啦!
题解
好啦,思路清晰了,我们来看看具体的代码实现是怎么做的吧,喵~
计算全局GCD
g首先,读入数据,然后遍历一遍数组,计算出所有数的最大公约数g。同时,用一个counts数组记录一下每个数字出现的次数,后面会用到。处理情况一 检查一下
counts[g]是不是大于 0。如果是,说明初始数组里就有g,直接输出n - counts[g]就好啦。处理情况二(DP登场!) 如果初始数组里没有
g,我们就需要用 DP 来找到最小的m。- 定义 DP 状态:
dp[x]表示,从初始数组中选出若干个数,使得它们的gcd为x,所需的最少数字个数。 - 初始化:
- 创建一个
dp数组,大小为数值上限(比如 5001),所有值初始化为一个很大的数(表示无穷大,即当前还无法凑出)。 - 遍历初始数组中所有不重复的数
v,设置dp[v] = 1。因为一个数自己和自己的gcd就是它本身,所以用 1 个数就能凑出它自己。
- 创建一个
- 状态转移: 这是最核心的一步!我们要想办法用已知的
dp值来更新其他的dp值。 我们可以遍历所有可能的gcd值i(从大到小遍历可以保证我们用到dp[i]时它已经是最终值了)。 如果dp[i]不是无穷大(说明我们已经知道如何用dp[i]个数凑出gcd为i),我们就再从初始数组里选一个不重复的数v,计算new_g = gcd(i, v)。 现在,我们用dp[i]个数凑出了i,再加一个v,总共用了dp[i] + 1个数,凑出了new_g。所以,我们可以更新dp[new_g]:dp[new_g] = min(dp[new_g], dp[i] + 1) - 最终答案: 通过上面的 DP 过程,我们就能计算出
dp[g]的值,这就是我们梦寐以求的最小m! 最终答案就是dp[g] + n - 2。
- 定义 DP 状态:
下面是带注释的代码,希望能帮主人更好地理解~
#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>
// 一个求最大公约数的函数,喵~
int custom_gcd(int a, int b) {
while (b) {
a %= b;
std::swap(a, b);
}
return a;
}
void solve() {
int n;
std::cin >> n;
const int MAX_A = 5000;
std::vector<int> a(n);
std::vector<int> counts(MAX_A + 1, 0); // 用来计数的桶
int g = 0;
// 读入数据,同时计算全局GCD和每个数的频率
for (int i = 0; i < n; ++i) {
std::cin >> a[i];
if (i == 0) {
g = a[i];
} else {
g = custom_gcd(g, a[i]);
}
counts[a[i]]++;
}
// 情况一:初始数组里已经有g了
if (counts[g] > 0) {
std::cout << n - counts[g] << "\n";
return;
}
// 情况二:初始数组里没有g,需要用DP来解决
const int INF = 1e9; // 表示无穷大
std::vector<int> dp(MAX_A + 1, INF);
// 找出所有不重复的初始数字
std::vector<int> unique_a;
for(int i = 1; i <= MAX_A; ++i) {
if (counts[i] > 0) {
unique_a.push_back(i);
dp[i] = 1; // DP 初始化:凑出数字i本身只需要1个数
}
}
// DP过程,从大到小遍历所有可能的GCD值
for (int i = MAX_A; i >= 1; --i) {
if (dp[i] == INF) { // 如果i都凑不出来,就跳过
continue;
}
// 尝试和每一个初始数字组合
for (int val : unique_a) {
int new_g = custom_gcd(i, val);
// 状态转移方程
if (dp[i] + 1 < dp[new_g]) {
dp[new_g] = dp[i] + 1;
}
}
}
// 最终答案:m + n - 2,这里的m就是dp[g]
std::cout << dp[g] + n - 2 << "\n";
}
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}知识点介绍
这道题里藏着两个很重要的知识点哦,就像猫咪的宝藏一样!
最大公约数 (GCD) 最大公约数,也就是
GCD,是能同时整除好几个数的那个最大的正整数。比如 12 和 20 的gcd就是 4 啦。求gcd最经典的方法就是欧几里得算法,也叫辗转相除法。它的原理是gcd(a, b) = gcd(b, a % b)。不断重复这个过程,直到其中一个数变成 0,另一个数就是答案。就像猫咪追自己的尾巴一样,转啊转就求出来了,很有趣的!动态规划 (Dynamic Programming) 动态规划,简称
DP,是一种非常聪明的算法思想,专门用来解决最优化问题。它的核心是**“把大问题分解成小问题,并记录小问题的解,避免重复计算”**。 就像猫咪学捕猎,会把“抓住老鼠”这个大目标分解成“悄悄靠近”、“找准时机”、“猛扑”等小步骤。每一步练习好了,就把技巧记在脑子里,下次就能用得更熟练。DP 就是这样,先解决小规模的问题,然后利用这些解来构建更大规模问题的解,一步步走向最终答案!在这道题里,我们就是先解决“如何用少数几个数凑出某个gcd”的小问题,然后一步步推导出“如何凑出最终目标g”的答案。
好啦,这道题的讲解就到这里啦!主人学会了吗?喵~ 如果还有不懂的地方,随时可以再来问我哦!本猫娘随时待命!