- 科学计算
【免费下载链接】codeforces-go
算法竞赛模板库 by 灵茶山艾府 💭💡🎈
导读
本文深入解析 LeetCode 第 82 场双周赛第三题(2333. Minimum Sum of Squared Difference)的经典贪心解法:通过绝对值差构造目标数组、将两次独立操作合并为一次"至多 k 次 −1"的等效问题,再利用排序 + 前缀削平(削峰填谷)技巧在 O(n log n) 时间内求出平方和的最小值。读完本文,你将掌握一类"操作次数统一折算 + 贪心削峰"的竞赛套路,并看到该解法在 codeforces-go 仓库中的完整 Go 实现与测试验证链路。
题目背景与问题转化
题目给定两个等长数组nums1、nums2,以及两个操作次数k1、k2:每次操作可以将nums1[i]加 1 或减 1(共至多k1次),将nums2[i]加 1 或减 1(共至多k2次),目标是最小化 $\sum (\textit{nums}_1[i]-\textit{nums}_2[i])^2$。
关键洞察在于:在nums1[i]上 +1,等价于在nums2[i]上 −1,反之亦然。因此两个数组上的操作可以合并看待——真正影响平方和的只有两者之差的绝对值。
据此定义:
- $a[i]=|\textit{nums}_1[i]-\textit{nums}_2[i]|$
- $k=k_1+k_2$
原问题就转换为一个干净的子问题:
对数组 $a$ 执行至多 $k$ 次 $-1$ 操作,能得到的 $\sum a[i]^2$ 的最小值。
这个"操作合并"是本类题目的通用第一步:先消去问题中的结构差异,把多维度操作收敛到单一变量的单一操作,后续贪心决策会大幅简化。
贪心核心:先削最大的数
平方函数是凸函数,$x^2$ 随 $x$ 增长得越来越快。因此对于任意两个数,优先把较大的那个 $-1$ 一定更优:同样消耗一次操作,减小较大的数能带来更大的平方和下降。
由此得到一个确定性策略:将 $a$从大到小排序,然后从左到右遍历,同时维护剩余操作次数 $k$。遍历过程中保持一个不变量——当前位置 $i$ 之前的所有元素(即 $a[0]$ 到 $a[i-1]$)都已经在同一高度 $a[i]$ 上,即"前缀已被削平"。
分情况讨论:如何判断 k 次操作能否继续削平
当遍历到 $a[i]$ 时,$a[0]$ 到 $a[i-1]$ 均已减小至 $a[i]$。下一步的目标是判断:$k$ 次操作能否让 $a[0]$ 到 $a[i]$全部再减小至 $a[i+1]$。
所需次数为前缀元素个数乘以高度差:
$$c = (i+1)\cdot(a[i]-a[i+1])$$
分两种情况:
若 $c < k$:$a[0]$ 到 $a[i]$ 均可以减小至 $a[i+1]$,把前缀削平到下一个高度,更新 $k = k - c$,继续循环。
若 $c \ge k$:剩余操作次数不足以把整个前缀再压下一层。此时 $a[0]$ 到 $a[i]$ 中:
- 有 $k \bmod (i+1)$ 个元素可以额外减小 $\left\lfloor\dfrac{k}{i+1}\right\rfloor+1$;
- 有 $i+1-k \bmod (i+1)$ 个元素只能额外减小 $\left\lfloor\dfrac{k}{i+1}\right\rfloor$。
后续无法继续减小,应退出循环,答案可以直接用公式结算。
为什么 $c \ge k$ 时用k // (i+1)与k % (i+1)分配?因为此时 $k$ 次操作平摊到 $i+1$ 个元素上,每个元素至少获得 $\lfloor k/(i+1)\rfloor$ 次减小,余数部分再逐一加给前 $k \bmod (i+1)$ 个元素(由于元素完全等价,具体分配给谁不影响平方和)。最终这 $i+1$ 个元素的高度只有两种取值,平方和可以一次性算出。
一个关键边界:sum(a) ≤ k 直接返回 0
在排序前先做一次全局判断:若所有 $a[i]$ 之和不超过 $k$,那么可以把每一个 $a[i]$ 都削到 0,答案必然为 0,直接返回。这既是一个正确的剪枝,也避免了后续循环在"削平到 0"时产生不必要的复杂边界处理。
代码中对应注释 "所有 a[i] 均可为 0" 的提前返回分支:
if sum <= k { return 0 // 所有 a[i] 均可为 0 }哨兵技巧:让削平循环自然终止
实现上有一个非常实用的细节:在数组末尾追加一个哨兵 0。这样当前缀被削到 0 时,a[i+1]仍存在(值为 0),高度差a[i]-a[i+1]的计算不会越界,循环边界判断得以简化;同时"全部削为 0"的场景也天然落入 $c \ge k$ 的分支中,由公式直接结算。
a = append(a, 0) // 哨兵四种语言的完整实现
仓库文档给出了 Python3、Java、C++、Go 四种等价实现,均遵循"先构造差数组 → 提前判 0 → 降序排序 + 哨兵 → 前缀削平"的统一框架。
Python3
class Solution: def minSumSquareDiff(self, a: List[int], nums2: List[int], k1: int, k2: int) -> int: ans, k = 0, k1 + k2 for i in range(len(a)): a[i] = abs(a[i] - nums2[i]) ans += a[i] * a[i] if sum(a) <= k: return 0 # 所有 a[i] 均可为 0 a.sort(reverse=True) a.append(0) # 哨兵 for i, v in enumerate(a): ans -= v * v # 撤销上面的 ans += a[i] * a[i] j = i + 1 c = j * (v - a[j]) if c < k: k -= c continue v -= k // j return ans + k % j * (v - 1) * (v - 1) + (j - k % j) * v * vJava
class Solution { public long minSumSquareDiff(int[] a, int[] nums2, int k1, int k2) { int n = a.length; int k = k1 + k2; long ans = 0; long sum = 0; for (int i = 0; i < n; i++) { a[i] = Math.abs(a[i] - nums2[i]); sum += a[i]; ans += (long) a[i] * a[i]; } if (sum <= k) { return 0; // 所有 a[i] 均可为 0 } Arrays.sort(a); for (int i = n - 1; ; i--) { int m = n - i; long v = a[i]; long c = m * (v - (i > 0 ? a[i - 1] : 0)); ans -= v * v; // 撤销上面的 ans += a[i] * a[i] if (c < k) { k -= c; continue; } v -= k / m; return ans + k % m * (v - 1) * (v - 1) + (m - k % m) * v * v; } } }Java 版本通过Arrays.sort升序后从后往前扫描,语义上与"降序前缀削平"完全对称,并用(i > 0 ? a[i-1] : 0)隐式承担了哨兵 0 的作用。
C++
class Solution { public: long long minSumSquareDiff(vector<int>& a, vector<int>& nums2, int k1, int k2) { int n = a.size(), k = k1 + k2; long long ans = 0, sum = 0; for (int i = 0; i < n; i++) { a[i] = abs(a[i] - nums2[i]); sum += a[i]; ans += (long) a[i] * a[i]; } if (sum <= k) { // 所有 a[i] 均可为 0 return 0; } ranges::sort(a, greater()); a.push_back(0); // 哨兵 for (int i = 0; ; i++) { long long j = i + 1, v = a[i], c = j * (v - a[j]); ans -= v * v; // 撤销上面的 ans += a[i] * a[i] if (c < k) { k -= c; continue; } v -= k / j; return ans + k % j * (v - 1) * (v - 1) + (j - k % j) * v * v; } } };Go(仓库内完整实现)
仓库中的 c.go 即本题的 Go 提交版,与文档中的 Go 解法完全一致:
// https://space.bilibili.com/206214/dynamic func minSumSquareDiff(a, nums2 []int, k1, k2 int) int64 { ans, sum := 0, 0 for i, v := range a { a[i] = abs(v - nums2[i]) sum += a[i] ans += a[i] * a[i] } k := k1 + k2 if sum <= k { return 0 // 所有 a[i] 均可为 0 } slices.SortFunc(a, func(a, b int) int { return b - a }) a = append(a, 0) // 哨兵 for i, v := range a { i++ ans -= v * v // 撤销上面的 ans += a[i] * a[i] if c := i * (v - a[i]); c < k { k -= c continue } v -= k / i ans += k%i*(v-1)*(v-1) + (i-k%i)*v*v break } return int64(ans) } func abs(x int) int { if x < 0 { return -x }; return x }值得注意的细节是ans的"撤销—结算"机制:第一轮循环已经把所有 $a[i]^2$ 加进了ans,进入削平循环后每处理一个元素就ans -= v*v撤销其贡献;一旦到达终止位置($c \ge k$),就只把当前前缀最终结算出来的平方和加回ans。这样省去了单独维护前缀和的额外数组。
复杂度分析
- 时间复杂度:$\mathcal{O}(n\log n)$,瓶颈在排序(不同语言对应的
sort、Arrays.sort、ranges::sort、slices.SortFunc均为比较排序)。 - 空间复杂度:$\mathcal{O}(1)$(忽略排序的栈开销与哨兵元素的开销)。
整个削平循环每轮要么整体压一层(减少一个不同高度),要么直接结算退出,最多执行 $O(n)$ 轮,因此线性扫描部分不会成为瓶颈。
仓库中的测试验证链路
codeforces-go 仓库为本题配备了完整的自动化测试,形成"题解文档 → 源码实现 → 用例数据 → 通用测试框架"的闭环:
- 实现文件:c.go 提供
minSumSquareDiff函数,位于leetcode/biweekly/82/c/目录下(第 82 场双周赛第三题)。 - 测试入口:c_test.go 调用通用测试工具
testutil.RunLeetCodeFuncWithFile(t, minSumSquareDiff, "c.txt", targetCaseNum)完成用例驱动测试。 - 测试数据:c.txt 按"输入行 × 期望输出行"分组存放用例,例如:
[1,2,3,4] [2,10,20,19] 0 0 579即 $nums_1=[1,2,3,4], nums_2=[2,10,20,19], k_1=0, k_2=0$,期望输出 579——此时差数组为 $[1,8,17,15]$,平方和 $1+64+289+225=579$,验证了无操作时直接计算平方和的正确性。
- 测试框架实现:leetcode.go 中的
RunLeetCodeFuncWithFile会读取文件、按函数签名推断每组用例的输入输出行数(fNumIn + fNumOut行一组),通过反射把字符串输入解析为具体参数并执行断言。仓库中的 leetcode.go 由此提供了"题目文件 + 通用 runner"即可跑通题解的标准化测试模式,同一框架在leetcode/weekly、leetcode/biweekly等目录下被大量复用。
小结:一类贪心套路的迁移
本题的完整解题链条可以抽象为三步通用方法论,可迁移到其他"若干次操作 + 最小化凸函数"类题目:
- 统一变量:把不同位置、不同对象上的操作折算成对同一数组的同一操作(本题把 $k_1+k_2$ 合并为对差值数组的 $-1$ 操作)。
- 贪心方向:凸函数的边际收益递减特性决定了"先削最大值"的贪心正确性,排序后前缀削平让决策呈现单调结构。
- 数学结算:在无法继续整体削平时,用整除与取模一次性算出平摊后的结果,避免逐次模拟的 O(nk) 复杂度。
这种"排序 + 前缀削平 + 平摊结算"的组合在竞赛中十分常见,掌握它能显著提高处理此类"最小化/最大化凸目标函数"题目的效率。
- 科学计算
【免费下载链接】codeforces-go
算法竞赛模板库 by 灵茶山艾府 💭💡🎈
相关推荐
前缀和与差分数组实战:codeforces-go 仓库中 LeetCode 1732「找到最高海拔」的完整解法剖析
前缀和与差分数组实战:codeforces go 仓库中 LeetCode 1732「找到最高海拔」的完整解法剖析 本篇技术指南围绕 LeetCode 第 44
科学计算EmDash CLI 内容编辑流程全解:Portable Text 转换、`_rev` 乐观并发与发布生命周期
EmDash CLI 内容编辑流程全解:Portable Text 转换、 _rev 乐观并发与发布生命周期 EmDash 是一个基于 Astro 的全栈 Ty
科学计算codeforces-go 实战精解:LeetCode 双周赛 99 T1「拆分最小和」的贪心推导与多语言实现
codeforces go 实战精解:LeetCode 双周赛 99 T1「拆分最小和」的贪心推导与多语言实现 本文基于 codeforces go 仓库中 l
科学计算
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考