561. 数组拆分
问题描述
给定一个长度为 2n
的整数数组 nums
,将数组中的元素两两配对,形成 n
个数对 (a₁, b₁), (a₂, b₂), ..., (aₙ, bₙ)
,使得所有数对中较小值的总和最大。
返回这个最大总和。
示例:
示例 1:
输入:nums = [1,4,3,2]
输出:4
解释:所有可能的分法(忽略元素顺序)为:
1. (1, 4), (2, 3) -> min(1, 4) + min(2, 3) = 1 + 2 = 3
2. (1, 3), (2, 4) -> min(1, 3) + min(2, 4) = 1 + 2 = 3
3. (1, 2), (3, 4) -> min(1, 2) + min(3, 4) = 1 + 3 = 4
所以最大总和为 4
示例 2:
输入:nums = [6,2,6,5,1,2]
输出:9
解释:最优的分法为 (2, 1), (2, 5), (6, 6). min(2, 1) + min(2, 5) + min(6, 6) = 1 + 2 + 6 = 9
算法思路
核心
要使所有数对中较小值的总和最大,需要尽量减少大数被"浪费"为较大值的情况。
贪心策略
- 排序数组:将数组按升序排列
- 贪心配对:每次取最小的两个数配对
- 这样较小的数不会被更大的数"压制"
- 避免小数与大数配对导致小数被选中的情况
数学证明
- 排序后数组为:
a₁ ≤ a₂ ≤ ... ≤ a₂ₙ
- 最优策略是配对:
(a₁,a₂), (a₃,a₄), ..., (a₂ₙ₋₁,a₂ₙ)
- 结果为:
a₁ + a₃ + ... + a₂ₙ₋₁
- 为什么最优?
a₁
必须被选中(它在任何配对中都是较小值)- 在剩余数中,
a₃
是能被选中的最小可能值 - 以此类推…
代码实现
import java.util.Arrays;
class Solution {
/**
* 计算数组拆分后数对中较小值的最大总和
*
* @param nums 长度为2n的整数数组
* @return 所有数对中较小值的最大总和
*/
public int arrayPairSum(int[] nums) {
// 步骤1:对数组进行升序排序
// 这是贪心算法的基础,确保能按最优方式配对
Arrays.sort(nums);
int maxSum = 0; // 记录最大总和
// 步骤2:贪心配对策略
// 遍历排序后的数组,每隔一个元素取一个(取偶数位置的元素)
// 即取索引为 0, 2, 4, ..., 2n-2 的元素
for (int i = 0; i < nums.length; i += 2) {
maxSum += nums[i]; // 每个数对中的较小值
}
return maxSum;
}
}
算法分析
-
时间复杂度:O(n log n)
- 主要开销在排序操作
- 后续遍历为 O(n)
-
空间复杂度:O(1)
- 只使用常数额外空间
- (假设排序算法使用原地排序)
-
优化:
- 贪心策略避免了复杂的动态规划
- 一次排序解决全局最优问题
算法过程
nums = [1,4,3,2]
:
1:排序
原始数组: [1,4,3,2]
排序后: [1,2,3,4]
2:贪心配对
- 配对1: (1,2) → 较小值 = 1
- 配对2: (3,4) → 较小值 = 3
3:计算总和
总和 = 1 + 3 = 4
验证其他配对方式:
- (1,3),(2,4) → 1 + 2 = 3
- (1,4),(2,3) → 1 + 2 = 3
- (2,3),(1,4) → 2 + 1 = 3
最大值确实是 4。
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:标准示例
int[] nums1 = {1,4,3,2};
System.out.println("Test 1: " + solution.arrayPairSum(nums1)); // 4
// 测试用例2:重复元素
int[] nums2 = {6,2,6,5,1,2};
System.out.println("Test 2: " + solution.arrayPairSum(nums2)); // 9
// 排序后: [1,2,2,5,6,6]
// 配对: (1,2),(2,5),(6,6) → 1+2+6 = 9
// 测试用例3:已排序数组
int[] nums3 = {1,2,3,4,5,6};
System.out.println("Test 3: " + solution.arrayPairSum(nums3)); // 9
// 配对: (1,2),(3,4),(5,6) → 1+3+5 = 9
// 测试用例4:逆序数组
int[] nums4 = {6,5,4,3,2,1};
System.out.println("Test 4: " + solution.arrayPairSum(nums4)); // 9
// 测试用例5:所有元素相同
int[] nums5 = {1,1,1,1};
System.out.println("Test 5: " + solution.arrayPairSum(nums5)); // 2
// 配对: (1,1),(1,1) → 1+1 = 2
// 测试用例6:两个元素
int[] nums6 = {1,2};
System.out.println("Test 6: " + solution.arrayPairSum(nums6)); // 1
}
关键点
-
贪心选择的正确性:
- 最小的数必须被选中(它在任何配对中都是较小值)
- 为了让总和最大,应该让次小的数不被选中
- 所以将最小和次小的数配对
-
配对策略:
- 排序后取偶数索引位置的元素(0,2,4,…)
- 这些位置的元素就是每个数对中的较小值
-
边界情况处理:
- 数组长度保证为偶数
- 元素范围:-10⁴ ≤ nums[i] ≤ 10⁴
-
为什么不能用其他配对方式?
- 任何其他配对都会导致某个较小值被"浪费"
- 或者让较大值被迫成为较小值
常见问题
-
为什么排序后取偶数索引?
- 因为排序后
nums[0] ≤ nums[1]
,所以(nums[0],nums[1])
的较小值是nums[0]
- 同理,
(nums[2],nums[3])
的较小值是nums[2]
- 所以取索引 0,2,4,… 的元素
- 因为排序后
-
如果数组长度不是偶数怎么办?
- 题目保证长度为 2n,所以一定是偶数
- 不需要额外处理
-
能否用动态规划解决?
- 可以,但时间复杂度会更高(O(n²))
- 贪心算法是最优解
-
负数如何处理?
- 同样适用,排序后负数会排在前面
- 贪心策略仍然成立
- 例如
[-2,-1,1,2]
→ 配对(-2,-1),(1,2)
→ 和 = -2+1 = -1
-
算法的直观理解:
- 想象有2n个人按身高排队
- 要两两配对跳舞,每对取较矮者的身高求和
- 为了让总和最大,应该让身高相近的人配对
- 这样不会让很矮的人拉低很高的人