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²) 时间复杂度不可接受)。
- 如何用数学方法和数据结构优化计算。
关键观察和转化
- max 距离的特殊性
函数中的距离是以 max(最大值)为度量:
\( d(i,j) = \max(|x_i - x_j|, |y_i - y_j|) \)
这实际上是 L∞范数距离,不是欧几里得距离。
L∞ 距离,也被称为切比雪夫距离,是在多维空间中定义的距离度量,表示两个点在任意一个坐标轴上的最大差值。
- 把 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)\) 拆成了两个前缀和表达式计算。
- 模运算和逆元是常见的技巧,保证答案不溢出
你可以这样复习巩固
- 先理解 L∞ 距离与 u, v 坐标的转换。
- 理解为什么排序后绝对值可以去掉。
- 理解双重求和如何拆成前缀和。
- 确认掌握代码实现细节(前缀和更新,模运算)。
- 多练习类似的双重求和化简题目.
这样,你就扎实掌握了本题的核心思想和做法。
浙公网安备 33010602011771号