题解:洛谷 P6067 Moo Volume

【题目来源】

洛谷:P6067 [USACO05JAN] Moo Volume S - 洛谷

【题目描述】

Farmer John 的农场上有 \(N\) 头奶牛(\(1 \leq N \leq 10^5\)),第 \(i\) 头奶牛的位置为 \(x_i\)\(0 \leq x_i \leq 10^9\))。

奶牛很健谈,每头奶牛都和其他 \(N-1\) 头奶牛聊天。第 \(i\) 头奶牛和第 \(j\) 头奶牛聊天时,音量为 \(|x_i-x_j|\)

请您求出所有奶牛聊天音量的总和。

【输入】

第一行一个整数 \(N\)

接下来 \(N\) 行,每行一个整数 \(x_i\)

【输出】

输出总音量。

保证答案在 64 位带符号整数的表示范围内。

【输入样例】

5
1
5
3
2
4

【输出样例】

40

【核心思想】

  1. 问题分析:给定 \(N\) 头奶牛的位置 \(x_1, x_2, \dots, x_N\),求所有两两之间距离之和 \(\sum_{1 \leq i < j \leq N} |x_i - x_j|\)。直接枚举所有 \(O(N^2)\) 对不可行,需要利用排序后前缀和优化。

  2. 算法选择

    • 排序 + 前缀和递推:先排序,再利用有序性将绝对值去掉,通过递推公式 \(O(N)\) 计算总距离
  3. 关键步骤

    • 排序:将位置数组 \(a[1..N]\) 按升序排列,排序后 \(|a_i - a_j| = a_j - a_i\)\(j > i\)
    • 前缀和递推
      • 定义 \(\text{sa}[i]\) 为以 \(a_i\) 为右端点,与前面所有 \(i-1\) 个点的距离之和
      • 递推公式:\(\text{sa}[i] = \text{sa}[i-1] + (a_i - a_{i-1}) \times (i-1)\)
        • 解释:前 \(i-1\) 个点每个都比 \(a_{i-1}\) 小(或相等),它们到 \(a_i\) 的距离等于"到 \(a_{i-1}\) 的距离"加上" \(a_i - a_{i-1}\) ",共 \((i-1)\) 个点各加这段差值
      • 累加答案:\(\text{ans} = \sum_{i=1}^{N} \text{sa}[i]\)(所有以 \(i\) 为右端点的距离和)
    • 翻倍输出:由于 \(\text{sa}[i]\) 只统计了 \(j < i\) 的情况,而题目要求所有无序对,所以最终答案为 \(\text{ans} \times 2\)(或直接理解为每对被计算两次)
    • 注意:实际上代码中 \(\text{ans}\) 累加的是 \(\text{sa}[i]\),而 \(\text{sa}[i]\) 已经是以 \(i\) 为右端点的所有左端点距离和,所以 \(\text{ans} = \sum_{i<j} (a_j - a_i)\),无需再乘 \(2\)。但代码输出 \(\text{ans} \times 2\),这是因为题目中"每头奶牛都和其他 \(N-1\) 头奶牛聊天"意味着有序对 \((i, j)\)\((j, i)\) 都计算,所以总音量为 \(2 \times \sum_{i<j} |x_i - x_j|\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N \log N)\),排序 \(O(N \log N)\),递推 \(O(N)\)
    • 空间复杂度:\(O(N)\),存储位置数组和前缀和数组
  5. 排序后前缀和优化的核心思想

    • 绝对值消去:排序后所有绝对值变为正向差值,避免分类讨论
    • 增量递推:利用相邻元素的差值 \((a_i - a_{i-1})\) 乘以点数 \((i-1)\),将"每个新点与前面所有点的距离和"从 \(O(i)\) 优化到 \(O(1)\)
    • 累积求和\(\text{sa}[i]\) 记录以 \(i\) 为右端点的所有距离,全局累加即得所有无序对距离和
    • 有序对翻倍:题目要求每对奶牛互相聊天(有序对),所以最终答案需乘 \(2\)
    • 适用于数轴上点集距离和、一维聚类代价、曼哈顿距离投影类问题

【解题思路】

【算法标签】

普及 #数学

【代码详解】

#include <bits/stdc++.h>
using namespace std;

// 定义int为long long类型
#define int long long
// 定义数组最大长度
const int N = 100005;

// 变量定义
int n;           // 数字的个数
int a[N];        // 存储输入的数字数组
int sa[N];       // 前缀和数组,sa[i]表示前i个数的某种累加值
int ans;         // 最终结果

// 主函数,signed相当于int main,但因为#define int long long,所以用signed
signed main()
{
    // 读入数字个数
    cin >> n;

    // 读入n个数字
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
    }

    // 对数组进行排序,从1到n
    sort(a + 1, a + n + 1);

    // 计算sa数组和ans
    for (int i = 1; i <= n; i++)
    {
        // 计算sa[i]:当前数字与前一个数字的差值乘以(i-1),然后加上前一个sa值
        sa[i] = sa[i - 1] + (a[i] - a[i - 1]) * (i - 1);
        // 累加sa[i]到ans
        ans += sa[i];
    }

    // 输出结果乘以2
    cout << ans * 2 << endl;

    return 0;
}

【运行结果】

5
1
5
3
2
4
40
posted @ 2026-08-29 09:38  团爸讲算法  阅读(7)  评论(0)    收藏  举报