喵呜~ 各位小可爱,我是你们最爱的猫娘!今天我们要一起来研究一道有趣的题目,是 Codeforces Round #895 (Div. 3) 中的 D 题——《Plus Minus Permutation》。听起来有点复杂,但别担心,有猫娘在,一切都会变得简单哒!
题目大意:喵~ 题目在讲什么呀?
题目呀,给了我们三个整数 n, x, y。然后呢,它定义了一个排列的“分数”(score)。这个分数是怎么算的呢?
假设我们有一个从 1 到 n 的排列 p1, p2, ..., pn。
- 首先,我们会把所有下标
i能够被x整除的pi值加起来。比如,如果x=2,那就会加p2, p4, p6, ...。 - 接着,我们会把所有下标
i能够被y整除的pi值加起来。比如,如果y=3,那就会加p3, p6, p9, ...。 - 最后,用第一部分的和减去第二部分的和,就是这个排列的分数啦!
我们的目标呢,就是找到一个排列,让它的分数最大!喵呜~ 也就是说,我们要尽可能地让那些被 x 整除的位置上的数字大,同时尽可能地让那些被 y 整除的位置上的数字小。
举个栗子(题目里也有哦!): n=7, x=2, y=3 一个可能的排列是 [2, 6, 1, 7, 5, 4, 3]。
- 被
x=2整除的下标有2, 4, 6。对应的pi是p2=6, p4=7, p6=4。它们的和是6+7+4=17。 - 被
y=3整除的下标有3, 6。对应的pi是p3=1, p6=4。它们的和是1+4=5。 - 分数就是
17 - 5 = 12。
我们要做的就是找到最大化这个分数的方法!
题解方法:喵呜~ 怎么才能得到最高分呢?
为了最大化 (被x整除的pi之和) - (被y整除的pi之和),我们当然希望:
- 被
x整除但不被y整除的位置:放最大的数字!这样它们会被加到分数里,越大越好。 - 被
y整除但不被x整除的位置:放最小的数字!这样它们会被减掉,越小越好。 - 同时被
x和y整除的位置:这些位置的数字既会被加又会被减,相当于抵消了。所以这些位置放什么数字,对最终分数没有影响。我们可以放那些既不是最大也不是最小,剩下的数字就好啦。
好啦,思路有了,接下来就是计算这些位置有多少个,以及对应的数字应该是什么。
1. 统计位置数量
被
x整除的下标数量:count_div_by_x = n / x(向下取整)被
y整除的下标数量:count_div_by_y = n / y(向下取整)同时被
x和y整除的下标数量:这等价于被lcm(x, y)整除的下标数量。lcm(x, y)是x和y的最小公倍数。- 我们知道
lcm(x, y) = (x * y) / gcd(x, y),其中gcd(x, y)是x和y的最大公约数。 - 为了防止
x * y溢出,我们可以先计算x / gcd(x, y)再乘以y。 - 所以
common_multiple = (x / gcd(x, y)) * y。 count_div_by_both = n / common_multiple。
- 我们知道
只被
x整除而不被y整除的下标数量:count_pos = count_div_by_x - count_div_by_both- 这些位置要放最大的数字。
只被
y整除而不被x整除的下标数量:count_neg = count_div_by_y - count_div_by_both- 这些位置要放最小的数字。
2. 分配数字
给
count_pos个位置放最大的数字: 从1到n中,最大的count_pos个数字就是n, n-1, ..., n - count_pos + 1。 它们的和是Sum(1..n) - Sum(1..n - count_pos)。 利用等差数列求和公式Sum(1..k) = k * (k + 1) / 2。 所以,sum_pos_terms = (n * (n + 1) / 2) - ((n - count_pos) * (n - count_pos + 1) / 2)。给
count_neg个位置放最小的数字: 从1到n中,最小的count_neg个数字就是1, 2, ..., count_neg。 它们的和是sum_neg_terms = (count_neg * (count_neg + 1) / 2)。
最后,最大分数就是 sum_pos_terms - sum_neg_terms 啦!喵~
题解代码:喵呜~ 看代码咯!
#include <iostream> // 输入输出
#include <numeric> // std::gcd 用于计算最大公约数
// 解决一个测试用例的函数
void solve() {
long long n, x, y;
std::cin >> n >> x >> y; // 读入 n, x, y
// 计算 lcm(x, y) 最小公倍数
// lcm(a, b) = (a / gcd(a, b)) * b,这样可以避免 x*y 溢出
long long common_divisor = std::gcd(x, y); // 计算最大公约数
long long common_multiple = (x / common_divisor) * y; // 计算最小公倍数
// 统计各种位置的数量
long long count_div_by_x = n / x; // 被 x 整除的下标数量
long long count_div_by_y = n / y; // 被 y 整除的下标数量
long long count_div_by_both = n / common_multiple; // 同时被 x 和 y 整除的下标数量
// 需要放最大数字的位置数量 (只被 x 整除)
long long count_pos = count_div_by_x - count_div_by_both;
// 需要放最小数字的位置数量 (只被 y 整除)
long long count_neg = count_div_by_y - count_div_by_both;
// 计算要加上的那些最大数字的和
// 从 n, n-1, ..., n - count_pos + 1
// 可以看作 1到n 的和 减去 1到(n-count_pos) 的和
long long sum_pos_terms = (n * (n + 1) / 2) - ((n - count_pos) * (n - count_pos + 1) / 2);
// 计算要减去的那些最小数字的和
// 从 1, 2, ..., count_neg
long long sum_neg_terms = (count_neg * (count_neg + 1) / 2);
long long max_score = sum_pos_terms - sum_neg_terms; // 最终的最大分数
std::cout << max_score << "\n"; // 输出结果,记得换行哦!
}
int main() {
// 优化输入输出,让程序跑得更快一点点!
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int t;
std::cin >> t; // 读入测试用例的数量
while (t--) { // 循环处理每个测试用例
solve();
}
return 0; // 程序结束,喵~
}知识点介绍:喵呜~ 学到了什么呀?
排列 (Permutation):
- 在数学中,排列是指从给定数量的元素中取出指定数量的元素进行排序。这道题中,是指将
1到n这n个数字,不重复地、全部放到n个位置上。 - 比如
[2, 3, 1]是1, 2, 3的一个排列,但[1, 2, 2]就不是,因为2重复了;[1, 3, 4]也不是,因为n=3但出现了4。
- 在数学中,排列是指从给定数量的元素中取出指定数量的元素进行排序。这道题中,是指将
最大公约数 (GCD - Greatest Common Divisor) 和 最小公倍数 (LCM - Least Common Multiple):
- GCD:两个或多个整数共有约数中最大的一个。比如
gcd(4, 6) = 2。 - LCM:两个或多个整数的公倍数中最小的一个。比如
lcm(4, 6) = 12。 - 重要关系:对于任意两个正整数
a和b,有a * b = gcd(a, b) * lcm(a, b)。 - 计算 LCM 的技巧:为了避免
a * b溢出,我们通常用lcm(a, b) = (a / gcd(a, b)) * b来计算。这道题中n可以达到10^9,x, y也可以很大,所以使用这个技巧非常关键! - C++ 的
<numeric>头文件提供了std::gcd函数,用起来很方便哦!
- GCD:两个或多个整数共有约数中最大的一个。比如
等差数列求和:
1 + 2 + ... + k = k * (k + 1) / 2。- 这个公式在计算连续整数的和时非常有用。
- 在这道题中,我们用它来计算最大
k个数之和(Sum(1..n) - Sum(1..n-k))和最小k个数之和(Sum(1..k))。
贪心策略 (Greedy Strategy):
- 这道题的核心思想就是贪心。为了最大化分数,我们总是把最大的数字放到加分的位置上,把最小的数字放到减分的位置上。
- 贪心策略不总是最优的,但在这类问题中,因为每个数字的贡献是独立的,并且我们有足够的“自由度”去选择数字(排列的性质),所以贪心是正确的。
整数溢出:
n可以达到10^9。n * (n + 1) / 2可能会达到(10^9)^2 / 2 = 0.5 * 10^18级别。long long类型在 C++ 中可以存储大约9 * 10^18的数值,所以使用long long是非常必要的!喵呜~ 如果用int就会溢出,答案就不对啦!
总结:喵呜~
这道题是一个典型的数学和贪心结合的题目。通过分析目标(最大化分数),我们可以确定哪些位置需要大的数字,哪些位置需要小的数字。然后利用数学知识(GCD, LCM, 等差数列求和)来精确计算这些数字的和。同时,也要注意数据范围,选择合适的变量类型,避免溢出。
好啦,今天的题解就到这里啦!有没有觉得猫娘很棒呀?如果还有不明白的地方,随时问我哦!喵~ 下次再见啦!