1. 问题重述与理解
LeetCode 1877题要求我们解决一个关于数组配对的优化问题。给定一个长度为偶数n的数组nums,我们需要将数组中的元素分成n/2个数对,使得:
- 每个元素恰好出现在一个数对中
- 所有数对和中的最大值尽可能小
这里的"数对和"指的是一个数对中两个元素相加的结果,"最大数对和"则是指所有数对和中的最大值。我们的目标是找到一种配对方式,使得这个最大数对和尽可能小。
2. 问题分析与直觉
2.1 简单例子分析
让我们先看题目给出的两个示例:
示例1:
输入:[3,5,2,3]
最优配对:(3,3)和(5,2)
最大数对和:max(3+3,5+2)=7
示例2:
输入:[3,5,4,2,4,6]
最优配对:(3,5),(4,4),(6,2)
最大数对和:max(8,8,8)=8
从这两个例子中,我们可以观察到一个共同点:数组被排序后,最小的元素与最大的元素配对,次小的与次大的配对,以此类推。
2.2 为什么这种配对方式最优?
这种"最小配最大"的策略背后有着深刻的数学原理:
-
避免大数叠加:如果我们不把最大的数与最小的数配对,那么最大的数就必须与一个相对较大的数配对,这样会产生更大的数对和。
-
平衡负载:通过将最大数与最小数配对,我们实际上是在"平衡"数对和,避免出现极端大的和。
-
贪心算法的体现:这是一种典型的贪心策略,每一步都做出局部最优的选择,希望这些局部最优能导致全局最优。
3. 算法设计与实现
3.1 算法步骤
基于上述分析,我们可以设计出以下算法:
- 对数组进行排序
- 初始化最大数对和为0
- 使用双指针法:
- 左指针从数组开头开始
- 右指针从数组末尾开始
- 计算每一对的和,并更新最大数对和
- 移动指针直到所有元素配对完成
3.2 时间复杂度分析
- 排序步骤:O(n log n)
- 配对步骤:O(n)
因此总时间复杂度为O(n log n),主要由排序步骤决定。
3.3 空间复杂度分析
- 如果使用原地排序算法(如快速排序):O(log n)(递归栈空间)
- 如果需要额外空间(如归并排序):O(n)
题目中给出的空间复杂度为O(log n),说明使用的是原地排序。
4. 代码实现与解析
4.1 C++实现
cpp复制class Solution {
public:
int minPairSum(vector<int>& nums) {
int ans = 0;
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size() / 2; i++) {
ans = max(ans, nums[i] + nums[nums.size() - i - 1]);
}
return ans;
}
};
代码解析:
- 首先对数组进行排序
- 使用双指针法,i从0开始,对应元素与从末尾开始的元素配对
- 每次计算配对和,并更新最大值
- 最后返回最大数对和
4.2 Python实现
python复制from typing import List
class Solution:
def minPairSum(self, nums: List[int]) -> int:
nums.sort()
return max(nums[i] + nums[~i] for i in range(len(nums) // 2))
代码解析:
- Python的简洁实现利用了列表推导式
~i是位运算,相当于-i-1,可以方便地从末尾取元素- 直接返回生成器表达式中的最大值
4.3 Java实现
java复制import java.util.Arrays;
class Solution {
public int minPairSum(int[] nums) {
Arrays.sort(nums);
int ans = 0;
for (int i = 0; i < nums.length / 2; i++) {
ans = Math.max(ans, nums[i] + nums[nums.length - i - 1]);
}
return ans;
}
}
代码解析:
- 使用Java的Arrays.sort()进行排序
- 类似C++的双指针实现
- 使用Math.max()来更新最大值
4.4 Go实现
go复制package main
import "sort"
func minPairSum(nums []int) (ans int) {
sort.Ints(nums)
for i := 0; i < len(nums)/2; i++ {
ans = max(ans, nums[i]+nums[len(nums)-i-1])
}
return
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
代码解析:
- Go语言使用sort.Ints()进行排序
- 需要自定义max函数
- 命名返回值ans简化了代码
4.5 Rust实现
rust复制impl Solution {
pub fn min_pair_sum(mut nums: Vec<i32>) -> i32 {
nums.sort();
let mut ans = 0;
for i in 0..nums.len() / 2 {
ans = ans.max(nums[i] + nums[nums.len() - i - 1]);
}
ans
}
}
代码解析:
- Rust使用sort()方法进行排序
- 利用了Rust的max方法(a.max(b))
- 变量需要声明为mut才能修改
5. 算法正确性证明
为了证明这个算法的正确性,我们可以使用反证法:
假设存在一种更好的配对方式,使得最大数对和比我们算法得到的结果更小。那么在这种配对方式中,至少有一个大数没有与最小的可用数配对。这意味着这个大数必须与一个更大的数配对,导致这对的和会更大,从而使得最大数对和不会比我们算法的结果更小。这与我们的假设矛盾,因此我们的算法是正确的。
6. 边界条件与注意事项
6.1 边界条件
- 数组长度为2时:直接返回两个数的和
- 所有元素相同:任何配对方式结果相同
- 数组包含极大值:注意整数溢出问题(但题目限制元素大小<=1e5)
6.2 注意事项
- 确保数组长度是偶数(题目已保证)
- 排序是算法的关键步骤,不能省略
- 在实现时,注意数组索引不要越界
- 对于某些语言(如Python),可以利用语言特性写出更简洁的代码
7. 性能优化思考
虽然O(n log n)的时间复杂度已经相当不错,但我们还可以考虑:
- 如果输入范围有限(如题目中1<=nums[i]<=1e5),可以使用计数排序,将时间复杂度降到O(n)
- 对于非常大的n(如接近1e5),需要注意排序算法的选择
- 在实际应用中,如果多次查询不同配对方式,可以预先排序并缓存结果
8. 相关题目推荐
- LeetCode 561. 数组拆分 I - 类似的配对问题,但目标是最大化最小和
- LeetCode 462. 最少移动次数使数组元素相等 II - 涉及中位数和配对思想
- LeetCode 410. 分割数组的最大值 - 更复杂的分割问题
- LeetCode 135. 分发糖果 - 类似的贪心策略应用
9. 实际应用场景
这类问题在实际中有许多应用:
- 任务分配:将任务分配给工人,平衡工作量
- 服务器负载均衡:将任务配对分配到服务器,避免过载
- 体育比赛配对:平衡比赛双方的实力
- 资源分配:将需求与资源合理匹配
10. 总结与个人体会
解决这个问题让我深刻理解了贪心算法的精妙之处——有时候最简单的策略反而是最优的。在实际编程中,我经常遇到类似的问题,关键在于:
- 先从小规模例子入手,寻找规律
- 尝试证明这个规律是否普遍适用
- 考虑边界条件和特殊情况
- 最后才是代码实现
对于这道题,排序后最小配最大的策略看似简单,但效果却出奇的好。这提醒我在面对问题时,不要一开始就追求复杂的解决方案,而应该先寻找简单直观的方法。