题目描述
又是农夫约翰的农场上寒冷而无聊的一天。
为了打发时间,农夫约翰发明了一种关于在整数数组上进行操作的有趣的休闲活动。
农夫约翰有一个包含 N 个非负整数的数组 a 和一个整数 M。
然后,农夫约翰会请贝茜给出一个整数 x。
在一次操作中,农夫约翰可以选择一个索引 i,并对 ai 加 1 或减 1。
农夫约翰的无聊值是他必须执行的最小操作次数,以使得对于所有的 1≤i≤N,ai−x 均可被 M 整除。
对于所有可能的 x,输出农夫约翰的最小无聊值。
输入格式
输入的第一行包含 T,为需要求解的独立的测试用例数量。
每一个测试用例的第一行包含 N 和 M。
第二行包含 a1,a2,…,aN。
输出格式
对于每一个测试用例输出一行,包含对于所有可能的 x,农夫约翰的最小无聊值。
数据范围
1≤T≤10,
1≤N≤2×105,
1≤M≤109,
0≤ai≤109,
输入保证所有测试用例的 N 之和不超过 5×105。
输入样例:
2
5 9
15 12 18 3 8
3 69
1 988244353 998244853
输出样例:
10
21
样例解释
在第一个测试用例中,x 的一个最优选择是 3。
农夫约翰可以执行 10 次操作使得 a=[12,12,21,3,12]。
算法
(枚举、前缀和、贪心)
首先,思考题目中的“对于所有的 1≤i≤N,ai−x 均可被 M 整除” 这句话,用数学式就是 (ai−x)%M=0,换个写法就是 ai%M=x (当 x 小于 M 时)。
所以,这题变为问最少操作次数,对任意个 ai 操作后,所有 ai%M 的结果等于 x 。
我们可以先对所有的 ai 进行模 m 操作,然后思考将 ai 变为 x 需要操作几次。这时,我们可以再转变题目的问法,即这时所有的 ai 到 x 的距离之和最小是多少。就是我们做过的贪心模板题 货仓选址 ,答案就是 x 取序列的中位数,算所有序列元素与中位数的绝对值之和。证明建议看y总的视频讲解,或者去找找别人的博客。(ps:这个算法也叫中位数定理。)
接下来,我们还要解决一个问题,那就是 ai 变 x 是有两种方式的,一个是减一个是加:
假设当前 ai=2,x=8,m=10,可以选择加 6 ,也可以选择 −4,解释一下:ai−4=2−4=−2 ,这时因为 ai<0 了,是有一个默认操作加上模数的 −2+10=8 它相当于 2+10−4=12−4=8。
所以,我们还要考虑序列元素再加一个模数的情况,这样枚举每一个长度为 n 的区间,套中位数定理的模板算最小操作数,取其中最小值就是答案。
这里模板是要进行优化的,因为朴素写法是枚举所有序列元素,这里明显不可以枚举否则会超时,用前缀和优化,每个数与中位数的绝对值之和,就是前半部分 (mid−l)∗a[mid]−∑i=lmid−1a[i] 再加上后半部分 ∑i=mid+1ra[i]−(r−mid)∗a[mid] 。
时间复杂度 O(nlog(n)+n)
代码
t = int(input())
for _ in range(t):
n, m = map(int, input().split())
a = list(map(int, input().split()))
a = [a[i] % m for i in range(n)]
a.sort()
a += [x + m for x in a] # 扩展数组,后半部分加上 m
a=[0]+a
pre = [0] * (2 * n + 1)
for i in range(1, 2 * n + 1):
pre[i] = pre[i - 1] + a[i]
ans = float('inf')
for l in range(1,n+1):
r = l + n - 1
mid = (l + r) // 2 # 中位数位置
left = (mid - l) * a[mid] - (pre[mid-1] - pre[l-1])
right = (pre[r] - pre[mid]) - (r - mid) * a[mid]
ans = min(ans, left + right)
print(ans)