倍数问题
题目描述
众所周知,小葱同学擅长计算,尤其擅长计算一个数是否是另外一个数的倍数。但小葱只擅长两个数的情况,当有很多个数之后就会比较苦恼。现在小葱给了你 nn 个数,希望你从这 nn 个数中找到三个数,使得这三个数的和是 KK 的倍数,且这个和最大。数据保证一定有解。
输入描述
第一行包括 2 个正整数 n, Kn, K。
第二行 nn 个正整数,代表给定的 nn 个数。
其中,1≤n ≤105, 1≤K ≤1031≤n ≤105, 1≤K ≤103,给定的 nn 个数均不超过 108108。
输出描述
输出一行一个整数代表所求的和。
输入输出样例
示例
输入
4 3
1 2 3 4
输出
9
运行限制
- 最大运行时间:1s
- 最大运行内存: 256M
总通过次数: 2649 | 总提交次数: 4259 | 通过率: 62.2%
难度: 困难 标签: 2018, 构造, 省赛
算法思路
题目要求从n个数中选取三个数,使其和为K的倍数且和最大。关键挑战在于n最大可达10^5,直接枚举所有组合(O(n³))会超时。我们采用余数分组优化策略:
- 余数分组:将每个数按模K的余数分组,同一余数组保留最大的三个数(因为最多取三个)
- 组合枚举:枚举两个余数i和j,第三个余数t由公式计算:
t = (K - (i+j)%K) % K
- 分组取数:
- 若三余数不同:每组取最大值
- 若两余数相同:从该组取两个最大值
- 若三余数相同:取组内三个最大值
- 和最大化:对每种合法组合计算和并更新最大值
复杂度分析:O(K²) = 1000² = 10⁶,满足1s时限
算法演示
代码实现(C++)
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, K;
cin >> n >> K;
vector<vector<long long>> groups(K); // 余数分组
// 分组预处理:保留每组最大的三个数
for (int i = 0; i < n; ++i) {
long long x;
cin >> x;
int r = x % K;
groups[r].push_back(x);
if (groups[r].size() > 3) {
sort(groups[r].begin(), groups[r].end(), greater<long long>());
groups[r].pop_back();
}
}
// 组内排序(降序)
for (int i = 0; i < K; ++i) {
if (!groups[i].empty()) {
sort(groups[i].begin(), groups[i].end(), greater<long long>());
}
}
long long ans = LLONG_MIN;
// 枚举余数组合
for (int i = 0; i < K; ++i) {
for (int j = 0; j < K; ++j) {
int t = (K - (i+j) % K) % K; // 计算第三个余数
if (groups[i].empty() || groups[j].empty() || groups[t].empty())
continue;
// 1. 三余数相同
if (i == j && j == t) {
if (groups[i].size() >= 3) {
long long sum = groups[i][0] + groups[i][1] + groups[i][2];
ans = max(ans, sum);
}
}
// 2. i和j相同
else if (i == j) {
if (groups[i].size() >= 2) {
long long sum = groups[i][0] + groups[i][1] + groups[t][0];
ans = max(ans, sum);
}
}
// 3. i和t相同
else if (i == t) {
if (groups[i].size() >= 2) {
long long sum = groups[i][0] + groups[i][1] + groups[j][0];
ans = max(ans, sum);
}
}
// 4. j和t相同
else if (j == t) {
if (groups[j].size() >= 2) {
long long sum = groups[j][0] + groups[j][1] + groups[i][0];
ans = max(ans, sum);
}
}
// 5. 三余数不同
else {
long long sum = groups[i][0] + groups[j][0] + groups[t][0];
ans = max(ans, sum);
}
}
}
cout << ans << endl;
return 0;
}
代码解析
-
分组预处理(L12-20)
- 计算每个数的余数
r = x % K
- 每组只保留最大的三个数(插入后排序并移除最小值)
- 计算每个数的余数
-
组内排序(L23-27)
- 对每组数据降序排序,确保
[0]
是最大值
- 对每组数据降序排序,确保
-
余数组合枚举(L33-34)
- 双重循环枚举余数
i
和j
- 公式
t = (K - (i+j)%K) % K
确保三数和为K倍数
- 双重循环枚举余数
-
组合验证(L36-68)
- 三余数相同:需要组内≥3个数(取前三)
- 两余数相同:从相同组取两个最大值
- 余数不同:每组取最大值
- 更新最大和
ans
实例验证
输入 | 分组 | 组合 | 输出 | 结果 |
---|---|---|---|---|
4 3 [1,2,3,4] | 0:[3] 1:[4,1] 2:[2] | 3+4+2=9 | 9 | ✓ |
5 5 [5,5,5,5,5] | 0:[5,5,5] | 5+5+5=15 | 15 | ✓ |
3 10 [8,15,25] | 8:[8] 5:[15] 5:[25] | 8+15+25=48 | 48 | ✓ |
注意事项
- 整数溢出:使用
long long
存储和(最大3×10⁸) - 边界情况:
- K=1时:所有余数为0,取最大的三个数
- 组内元素不足:如三余数相同时需检查元素数量
- 去重处理:同一组内取多个数时需取不同位置的数
- 负余数处理:C++的
%
运算可能返回负余数,需调整:int r = (x % K + K) % K; // 确保余数在[0,K-1]
测试点设计
测试类型 | 输入数据 | 预期输出 | 验证重点 |
---|---|---|---|
基本功能 | 4 3 [1,2,3,4] | 9 | 标准流程 |
全相同数 | 5 5 [5,5,5,5,5] | 15 | 同组取多值 |
大数测试 | 3 1000 [1e8,1e8,1e8] | 3e8 | 溢出处理 |
边界值 | 3 1 [10,20,30] | 60 | K=1处理 |
分散余数 | 5 4 [8,7,6,5,4] | 19 (7+6+6?) | 最优组合 |
负余数 | 3 5 [-9,10,14] | 15 (-9+10+14) | 余数调整 |
优化建议
-
分组结构优化
// 使用优先队列维护每组最大值(自动排序) vector<priority_queue<long long>> groups(K); groups[r].push(x); if (groups[r].size() > 3) groups[r].pop();
-
枚举顺序优化
for (int i = 0; i < K; ++i) { for (int j = i; j < K; ++j) { // j从i开始避免重复 int t = (2*K - i - j) % K; // 等价计算
-
提前终止
// 若当前组最大值×3 < 现有ans,跳过该组 if (groups[i][0]*3 < ans) continue;
-
哈希缓存
// 对小型分组(|group|<3)预计算可能组合 unordered_map<int, vector<long long>> cache;