题目分析:
题目大意:
给定一个长度为 ( N N N ) 的整数数组 ( a a a ) 和一个整数 ( M M M ),目标是找到一个整数 ( x x x ),使得对于每个元素 ( a i − x a_i - x ai−x ) 都能被 ( M M M ) 整除。我们需要最小化从 ( a i a_i ai ) 变为符合条件的值所需的最小操作数。每次操作可以将某个 ( a i a_i ai ) 增加或减少 1 1 1。
输入格式:
- 第一行输入一个整数 ( T T T ),表示有 ( T T T ) 个测试用例。
- 对于每个测试用例,第一行输入两个整数 ( N N N ) 和 ( M M M )。
- 第二行输入一个数组 ( a a a ),包含 ( N N N ) 个元素。
输出格式:
对于每个测试用例,输出最小操作数,即使得所有元素 ( a i a_i ai ) 都符合要求的最小操作数。
解题思路:
通过模运算的方式来优化。要使每个 a i − x a_i - x ai−x被 M M M 整除,我们需要对每个 a i a_i ai 调整到某个余数,并使其操作代价最小。最优的选择是通过对 ( a i a_i ai % M M M) 计算和调节来找出最小代价。
1. 数组元素的余数:
对于每个元素 ( a i a_i ai ),我们可以计算它对 ( M M M ) 的余数。然后通过调整这个余数使得 ( a i a_i ai - x ) 能被 ( M ) 整除。
2. 查询和分治:
query(l, r)
函数计算从第 ( l ) 个元素到第 ( r ) 个元素的区间和(前缀和的思想)。这可以大大减少重复计算的代价。get_ans(x)
函数用于计算以 ( x ) 为分界点的最小操作数。具体做法是将数组 ( a ) 分为 4 个区间,分别处理每个区间的差距,并通过加权求和的方式得到答案。
3. 四个区间的处理:
每个区间内都会计算需要的操作代价,即使得每个元素与目标值 ( x ) 的差最小化。
4. 优化:
- 数组 ( a ) 在每次计算前会进行排序,这样可以方便地通过二分查找来寻找符合条件的区间,从而有效地加速计算。
5. 最小化代价:
我们对每个可能的分界线 ( x ) 进行计算,找到最小的操作代价。
代码解读:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=5e5+5;
int a[N], s[N], n, m;
// 前缀和查询函数
int query(int l, int r){
return s[r] - s[l-1]; // 计算区间和
}
// 计算以 x 为分界线时的最小操作数
int get_ans(int x){
int R = n;
int sum = 0;
// 处理四个区间
// 1. 区间 [x + m/2 + 1, x + m]
int l = x + m/2 + 1;
int r = x + m;
int pos = lower_bound(a + 1, a + n + 1, l) - a;
if (pos <= R) {
int cnt = R - pos + 1;
sum += r * cnt - query(pos, R); // 计算区间的操作代价
}
R = pos - 1;
// 2. 区间 [x, x + m/2]
l = x;
r = x + m/2;
pos = lower_bound(a + 1, a + R + 1, l) - a;
if (pos <= R) {
int cnt = R - pos + 1;
sum += query(pos, R) - l * cnt;
}
R = pos - 1;
// 3. 区间 [x - m/2, x - 1]
l = x - m/2;
r = x - 1;
pos = lower_bound(a + 1, a + R + 1, l) - a;
if (pos <= R) {
int cnt = R - pos + 1;
sum += x * cnt - query(pos, R);
}
R = pos - 1;
// 4. 区间 [x - m, x - m/2 - 1]
l = x - m;
r = x - m/2 - 1;
pos = lower_bound(a + 1, a + R + 1, l) - a;
if (pos <= R) {
int cnt = R - pos + 1;
sum += query(pos, R) - l * cnt;
}
R = pos - 1;
return sum;
}
void solve(){
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
a[i] %= m; // 对数组 a 进行取模
}
if (m == 1) {
cout << 0 << '\n';
return;
}
sort(a + 1, a + n + 1); // 排序数组 a
for (int i = 1; i <= n; i++) {
s[i] = s[i-1] + a[i]; // 计算前缀和
}
int ans = 1e18; // 初始化答案为一个很大的数
// 遍历每个可能的分界线 x
for (int i = 1; i <= n; i++) {
int sum = get_ans(a[i]); // 计算以 a[i] 为分界线时的操作代价
ans = min(ans, sum); // 更新最小操作数
}
cout << ans << '\n'; // 输出最小操作数
}
signed main(){
int t;
cin >> t; // 读取测试用例数量
while (t--) {
solve(); // 解决每个测试用例
}
}
代码说明:
-
输入处理:
- 读取测试用例数量 T T T。
- 对于每个测试用例,读取 N N N 和 M M M,然后读取数组 a a a。
-
余数计算:
- 对于每个
a
i
a_i
ai,计算它对
M
M
M 的余数并存储在
a
数组中。
- 对于每个
a
i
a_i
ai,计算它对
M
M
M 的余数并存储在
-
前缀和计算:
- 对数组
a
a
a 进行排序后,计算前缀和
s[i]
,这有助于我们快速计算任意区间的和。
- 对数组
a
a
a 进行排序后,计算前缀和
-
最小操作数计算:
- 对每个可能的分界线
a
[
i
]
a[i]
a[i],通过
get_ans(x)
计算以 a [ i ] a[i] a[i] 为分界线时的最小操作数。 get_ans(x)
会遍历 4 个区间,分别计算将数组元素调整到目标余数所需的操作代价。
- 对每个可能的分界线
a
[
i
]
a[i]
a[i],通过
-
结果输出:
- 输出每个测试用例的最小操作数。
时间复杂度分析:
- 排序: 对数组 a a a 进行排序,时间复杂度为 O ( N log N ) O(N \log N) O(NlogN)。
- 前缀和计算: 计算前缀和需要遍历一次数组,时间复杂度为 O ( N ) O(N) O(N)。
- 查询操作: 对于每个测试用例,
get_ans(x)
会遍历所有的 a [ i ] a[i] a[i],每次计算操作代价的复杂度是 O ( N ) O(N) O(N),因此对于每个分界线 a [ i ] a[i] a[i] 的处理复杂度是 O ( N ) O(N) O(N)。 - 总体时间复杂度: 每个测试用例的时间复杂度为 O ( N log N ) O(N \log N) O(NlogN),因此整个程序的时间复杂度为 O ( T × N log N ) O(T \times N \log N) O(T×NlogN)。
总结:
该解法通过前缀和的优化,减少了不必要的重复计算,并通过排序和二分查找提高了效率。每个测试用例的时间复杂度为 O ( N log N ) O(N \log N) O(NlogN) 。