Farmer John's Favorite Operation (USACO Silver 2025 January)

Farmer John's Favorite Operation (USACO Silver 2025 January)

这个题解法简单,但是理解起来不太直观(出题人应该是先想好的解法,再编写题目的),官方题解也写得很绕,所以是一道考验思维能力的题目。下面是一种解释的方法,供参考。

bi=aimodMb_i=a_i \mod M,然后从小到大排序,然后我们断言xx在以下数字中:

b0,b1,b2,...,bn1b0+M,b1+M,...,bn1+Mb0M,b1M,...,bn1Mb_0, b_1, b_2, ..., b_{n-1}\\ b_0+M, b_1+M, ..., b_{n-1}+M\\ b_0-M, b_1-M, ..., b_{n-1}-M

这个成立的原因如下:

  1. 原题要求操作之后的aixa_i-x可以被MM整除,这个操作的次数就是ans=if(aixmodM)\text{ans}=\sum_i f(|a_i-x| \mod M),其中f(y)=min(y,My)f(y)=\min(y, M-y)f(y)f(y)的意思,就是当yy小于M/2M/2的时候,我们把(aix)modM(a_i-x) \mod M移动变成0,而当yy大于M/2M/2的时候,我们把它移动变成MM,这样可以让操作次数最小。
  2. 按这个定义,我们把aia_i变成bi=aimodMb_i=a_i \mod M,对于上面1里面的计算是没有影响的:
    ans=imin(bix,Mbix)=imin(bix,(bi+M)x,x(biM))\begin{align*} \text{ans} &= \sum_i \min(|b_i - x|, M - |b_i - x|) \\ &= \sum_i \min(|b_i - x|, (b_i+M)-x, x-(b_i-M)) \end{align*}
    第二步第二第三项,分别当bi<xb_i < xbi>xb_i > x时成立。这三项,分别对应上面“断言”中的三行。
  3. 因为随着xx的变化,整体ans也是线性变化的,所以极值只会取在端点,也就是x只需要取bi,bi+M,biMb_i, b_i+M, b_i-M这些值。
  4. 对于一个具体的xx,我们观察上面表达式中的三项,分别是bib_ixx的距离,bi+Mb_i+Mxx的距离,biMb_i-Mxx的距离。那么最后的ans就是xx为中心,上面的数组中上下各M/2M/2的范围内的数字与xx的距离之和

所以我们只需要在以上范围内枚举xx,然后计算以xx为中心,上下M/2M/2的范围内的数字与xx的距离之和(下限xM/2x-M/2,上限xM/2+Mx-M/2+M), 然后取最小值就可以了。使用prefix sum,可以在O(n)\mathcal{O}(n)时间内完成。再加上排序时间,最后复杂度是O(nlogn)\mathcal{O}(n\log n)

(以下程序上实现中,实际只使用了bi,bi+Mb_i, b_i+M2n2n个值,这个可以证明和这里讲的3n3n个值是一样的,具体证明略过,复杂度一样)

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
 
int t, n;
ll m;
ll a[400005], b[400005];  // b is prefix sum of a
 
int main() {
    scanf("%d",&t);
    while(t--) {
        scanf("%d%lld",&n, &m);
        for(int i=0;i<n;++i) {
            scanf("%lld", &a[i]);
            a[i] %= m;
        }
        sort(a, a+n);
        for (int i = 0; i < n; i++)
            a[i+n] = a[i] + m;
        for(int i=0;i<2*n;++i) 
            b[i+1] = b[i] + a[i];
        int li = 0, ri = 0;
        ll ans = 1e18;
        for (int i = 0; i < 2*n; i++) {
            ll l = a[i]-m/2;
            ll r = l+m;
            if (l < 0) continue;
            if (r >= 2*m) continue;
            while (a[li] < l) li++;
            while (a[ri] < r && ri < 2*n) ri++;
            ll v = (i-li)*a[i] - (b[i]-b[li]);     // sum of abs(a[j]-a[i]) from li to i-1
            v += (b[ri]-b[i+1]) - (ri-i-1)*a[i];   // sum of abs(a[j]-a[i]) from i+1 to ri
            ans = min(ans, v);
        }
       printf("%lld\n",ans);
    }
    return 0;
}