2025“钉耙编程”中国大学生算法设计暑期联赛_3 1012

发现AI题解写得比我清晰易懂多得多,呜呜呜

好的,我结合你的代码和提供的思路,帮你详细梳理这道题的思维过程,讲得尽量规范和清晰,适合新手理解。可以一步步跟着思考,掌握关键点。


题目简述

你有 n 个能量核心,每个核心有坐标 \((x_i, y_i)\) 和能量强度 \(a_i\)

对任意一对核心 i 和 j,会产生共振能量:

\( f_{i,j} = (a_i + a_j) \times \max(|x_i - x_j|, |y_i - y_j|) \)

我们需要计算所有不同对的共振能量之和(包括 i=j 时也算,但 i=j 的能量为 0),即:

\( Ans = \sum_{1 \le i < j \le n} f_{i,j} \)

并对答案取模 \(10^9 + 7\)


主要难点

  • \(n\) 最多可达 2×10^5,所有询问中 $ \sum n \le 10^6 $。
  • 直接两两枚举计算,会超时(O(n²) 时间复杂度不可接受)。
  • 如何用数学方法和数据结构优化计算。

关键观察和转化

  1. max 距离的特殊性

函数中的距离是以 max(最大值)为度量:

\( d(i,j) = \max(|x_i - x_j|, |y_i - y_j|) \)

这实际上是 L∞范数距离,不是欧几里得距离。

L∞ 距离,也被称为切比雪夫距离,是在多维空间中定义的距离度量,表示两个点在任意一个坐标轴上的最大差值。

  1. 把 L∞ 距离转成对坐标的操作

一个比较经典的技巧是:

定义

\( u = x + y, \quad v = x - y \)

有一个结论:

\( \max(|x_i - x_j|, |y_i - y_j|) = \frac{|u_i - u_j| + |v_i - v_j|}{2} \)

为什么?

  • \(u_i - u_j = (x_i - x_j) + (y_i - y_j)\)
  • \(v_i - v_j = (x_i - x_j) - (y_i - y_j)\)

取绝对值后,两个表达式的和除以 2,恰好等于 L∞ 距离。

这个变换让问题变得更容易拆分,因为绝对值的结构更简单。


目标转化

将目标表达式写成:

\( Ans = \sum_{i<j} (a_i + a_j) \cdot \max(|x_i - x_j|, |y_i - y_j|) = \sum_{i<j} (a_i + a_j) \cdot \frac{|u_i - u_j| + |v_i - v_j|}{2} \)

拆开:

\( Ans = \frac{1}{2} \left( \sum_{i<j} (a_i + a_j) |u_i - u_j| + \sum_{i<j} (a_i + a_j) |v_i - v_j| \right) \)


关键问题变成:

计算下面两个和:

\( S_u = \sum_{i<j} (a_i + a_j) |u_i - u_j| \)

\( S_v = \sum_{i<j} (a_i + a_j) |v_i - v_j| \)


如何快速计算 \(\sum_{i<j} (a_i + a_j) |coord_i - coord_j|\)

这里的 \(coord\) 指代 \(u\)\(v\)


思考绝对值拆分

先排序所有点,使坐标递增:

\( coord_1 \le coord_2 \le \cdots \le coord_n \)

对于 \(i < j\):

\( |coord_j - coord_i| = coord_j - coord_i \)

因为 \(coord_j \ge coord_i\),绝对值去掉了。


将和拆成两部分

\( S = \sum_{i<j} (a_i + a_j)(coord_j - coord_i) \)

展开:

\( S = \sum_{i<j} a_i (coord_j - coord_i) + \sum_{i<j} a_j (coord_j - coord_i) \)

换顺序为:

\( S = \sum_j \sum_{i<j} a_i (coord_j - coord_i) + \sum_j \sum_{i<j} a_j (coord_j - coord_i) \)

其中,第二部分注意 \(a_j\) 不依赖 \(i\),可以写成:

\( \sum_j a_j \sum_{i<j} (coord_j - coord_i) = \sum_j a_j (j \cdot coord_j - \sum_{i<j} coord_i) \)

第一部分:

\( \sum_j \sum_{i<j} a_i (coord_j - coord_i) = \sum_j \left( coord_j \sum_{i<j} a_i - \sum_{i<j} a_i coord_i \right) \)


计算前缀和

为了快速计算 \(\sum_{i<j} coord_i\), \(\sum_{i<j} a_i\), \(\sum_{i<j} a_i coord_i\),我们使用前缀和数组。

分别定义:

  • \(prefix\_coord_j = \sum_{i=1}^{j-1} coord_i\)
  • \(prefix\_a_j = \sum_{i=1}^{j-1} a_i\)
  • \(prefix\_a\_coord_j = \sum_{i=1}^{j-1} a_i \cdot coord_i\)

这样,对每个 \(j\)

\( term1 = a_j \times (j \times coord_j - prefix\_coord_j) \)

\( term2 = coord_j \times prefix\_a_j - prefix\_a\_coord_j \)

然后

\( S = \sum_{j=1}^n (term1 + term2) \)


最后求出答案

计算 \(S_u\)\(S_v\) 后,

\( Ans = \frac{S_u + S_v}{2} \mod (10^9 + 7) \)


代码中的具体实现技巧

  • 先读入所有核心,计算 u 和 v。
  • 分别排序 u 和 v 序列。
  • 用前缀和方法计算 \(S_u\)\(S_v\)
  • 注意取模和大数溢出问题。
  • 计算答案,输出。

详细代码注释版讲解

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

typedef long long ll;
const ll MOD = 1000000007;

// 结构体保存坐标与能量
struct Core {
    ll coord;  // u 或 v 坐标
    ll a;      // 能量强度
};

// 计算 S = sum_{i<j} (a_i + a_j)*|coord_i - coord_j|
// 注意:arr 已经按 coord 升序排列
ll computeSum(vector<Core>& arr) {
    ll res = 0;                // 最终结果
    ll sum_coord = 0;          // 前缀坐标和 ∑ coord[i], i<j
    ll sum_a = 0;              // 前缀能量强度和 ∑ a[i], i<j
    ll sum_a_coord = 0;        // 前缀 a[i]*coord[i] 和
    
    int n = (int)arr.size();
    for (int j = 0; j < n; j++) {
        ll coord = arr[j].coord % MOD;
        ll a = arr[j].a % MOD;

        // 计算第 j 个点的贡献
        // term1 = a_j * (j*coord_j - sum_coord)
        ll term1 = a * (( (ll)j * coord - sum_coord + MOD) % MOD) % MOD;
        // term2 = coord_j * sum_a - sum_a_coord
        ll term2 = (coord * sum_a - sum_a_coord + MOD) % MOD;

        ll add = (term1 + term2) % MOD;
        res = (res + add) % MOD;

        // 更新前缀和
        sum_coord = (sum_coord + coord) % MOD;
        sum_a = (sum_a + a) % MOD;
        sum_a_coord = (sum_a_coord + a * coord) % MOD;
    }
    return res;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int t;
    cin >> t;

    const ll inv2 = 500000004; // 2 的逆元 (mod MOD)
    
    while (t--) {
        int n;
        cin >> n;

        vector<Core> arrU(n), arrV(n);

        for (int i = 0; i < n; i++) {
            ll x, y, a;
            cin >> x >> y >> a;

            ll u = x + y;
            ll v = x - y;

            arrU[i] = {u, a};
            arrV[i] = {v, a};
        }

        // 按 u 和 v 升序排序
        sort(arrU.begin(), arrU.end(), [](const Core& c1, const Core& c2){
            return c1.coord < c2.coord;
        });
        sort(arrV.begin(), arrV.end(), [](const Core& c1, const Core& c2){
            return c1.coord < c2.coord;
        });

        ll sumU = computeSum(arrU);
        ll sumV = computeSum(arrV);

        // 最终答案
        ll ans = (sumU + sumV) % MOD;
        ans = ans * inv2 % MOD;   // 除以 2 的逆元

        cout << ans << "\n";
    }
    return 0;
}

总结

  • 题目里用到了 L∞距离,我们把它转化成 u 和 v 坐标绝对差的和,成功将问题拆成两部分。
  • 然后利用排序和前缀和,把双重求和的复杂度降为 O(n log n) (排序)+ O(n)(求和)。
  • 利用数学推导将 \(\sum_{i<j}(a_i + a_j)(coord_j - coord_i)\) 拆成了两个前缀和表达式计算。
  • 模运算和逆元是常见的技巧,保证答案不溢出

你可以这样复习巩固

  1. 先理解 L∞ 距离与 u, v 坐标的转换。
  2. 理解为什么排序后绝对值可以去掉。
  3. 理解双重求和如何拆成前缀和。
  4. 确认掌握代码实现细节(前缀和更新,模运算)。
  5. 多练习类似的双重求和化简题目.

这样,你就扎实掌握了本题的核心思想和做法。

posted @ 2025-07-25 17:47  aminuosi  阅读(112)  评论(0)    收藏  举报