USACA-silver-2025-1-25-T2 Farmer John’s Favorite Operation题解

题目分析:

题目大意:

给定一个长度为 ( N N N ) 的整数数组 ( a a a ) 和一个整数 ( M M M ),目标是找到一个整数 ( x x x ),使得对于每个元素 ( a i − x a_i - x aix ) 都能被 ( M M M ) 整除。我们需要最小化从 ( a i a_i ai ) 变为符合条件的值所需的最小操作数。每次操作可以将某个 ( a i a_i ai ) 增加或减少 1 1 1

输入格式:

  1. 第一行输入一个整数 ( T T T ),表示有 ( T T T ) 个测试用例。
  2. 对于每个测试用例,第一行输入两个整数 ( N N N ) 和 ( M M M )。
  3. 第二行输入一个数组 ( a a a ),包含 ( N N N ) 个元素。

输出格式:

对于每个测试用例,输出最小操作数,即使得所有元素 ( a i a_i ai ) 都符合要求的最小操作数。


解题思路:

通过模运算的方式来优化。要使每个 a i − x a_i - x aix 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(); // 解决每个测试用例
    }
}

代码说明:

  1. 输入处理:

    • 读取测试用例数量 T T T
    • 对于每个测试用例,读取 N N N M M M,然后读取数组 a a a
  2. 余数计算:

    • 对于每个 a i a_i ai,计算它对 M M M 的余数并存储在 a 数组中。
  3. 前缀和计算:

    • 对数组 a a a 进行排序后,计算前缀和 s[i],这有助于我们快速计算任意区间的和。
  4. 最小操作数计算:

    • 对每个可能的分界线 a [ i ] a[i] a[i],通过 get_ans(x) 计算以 a [ i ] a[i] a[i] 为分界线时的最小操作数。
    • get_ans(x) 会遍历 4 个区间,分别计算将数组元素调整到目标余数所需的操作代价。
  5. 结果输出:

    • 输出每个测试用例的最小操作数。

时间复杂度分析:

  1. 排序: 对数组 a a a 进行排序,时间复杂度为 O ( N log ⁡ N ) O(N \log N) O(NlogN)
  2. 前缀和计算: 计算前缀和需要遍历一次数组,时间复杂度为 O ( N ) O(N) O(N)
  3. 查询操作: 对于每个测试用例,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)
  4. 总体时间复杂度: 每个测试用例的时间复杂度为 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)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值