数组拆分 图解题解
这道题到底在问什么
- 输入
- nums=[1,4,3,2]
- 输出
- 4 (分成 (1,2) 和 (3,4),min 之和 1 + 3 = 4)
- 输入
- nums=[6,2,6,5,1,2]
- 输出
- 9 ((1,2)(2,5)(6,6),min 之和 1 + 2 + 6 = 9)
最优解:为什么这么做
一句话答案:LeetCode 561 数组拆分:把 2n 个数配成 n 对、求每对较小值之和的最大值,排序后取偶数下标相加即最优,让每个大数只和接近它的数配、少拉低别人,时间 O(n log n)。
把 2n 个数配成 n 对,求较小值之和最大
给一个长度是 2n 的数组 nums,把里面的数两两分成 n 对,每一对 (a, b) 只算较小的那个 min(a, b),再把这 n 个较小值加起来。怎么配对能让总和最大?题面给 nums=[1,4,3,2],最好的配法是 (1,2) 和 (3,4),较小值 1 加 3 得 4。
配对方式呈指数级,暴力算不动
要让总和最大,直觉是把所有配对方式都试一遍、挑最大的一种。可 2n 个数两两配对的方案数是 (2n−1)×(2n−3)×…×1,随 n 涨成阶乘级:8 个数就有 105 种配法,20 个数已过六亿,只能应付几个数的玩具例子,数组一大就彻底卡死。
排序后相邻配对,为什么就是最优
先把 nums 从小到大排好,再让相邻的两个数配成一对:排序后第 0 和第 1 个一对、第 2 和第 3 个一对,依此类推。每一对里左边那个,也就是下标为偶数的那个,一定不大于右边,所以它就是这对的较小值,偶数下标上的数加起来就是答案。
为什么这样配最优?先看最大的那个数,它跟谁配都是那对里较大的一个,注定拿不到分;那就让它去和紧挨着的次大数配,把次大数保下来当别人的对手。它对不对,可以用交换论证来验:假设最优配法里有个数没跟排序后的邻居配,总能找到两对、对调配对方式后总的较小值之和不会变小;反复对调就能整理成相邻配对的样子,说明它不比任何配法差。
两步走:先排序,再隔一个取一个
算法落到代码只有两步。第一步,把 nums 从小到大排序;第二步,从下标 0 出发,每隔一个取一个,也就是 0、2、4… 这些偶数位,一路加到末尾。Python 一行 sum(sorted(nums)[::2]) 就够;C++、Java 排完序用一个循环、下标每次加 2 累加。取偶数位而不是奇数位,是因为排序后每对的较小值都落在左边、也就是偶数下标那一格。
拿题面两组数据逐组核对
先走 [1,4,3,2]。排序后成 [1,2,3,4],相邻配对是 (1,2) 和 (3,4),取偶数下标 0 和 2 上的 1 和 3,相加得答案 4。
再走 [6,2,6,5,1,2]。排序后成 [1,2,2,5,6,6],相邻配对 (1,2)(2,5)(6,6),取偶数下标 0、2、4 上的 1、2、6,相加得 9。要是不排序、直接在原数组上取偶数位,拿到的是 6、6、1 加起来 13,看着更大却是错的——那三个数根本配不成三对合法的 (a, b)。
复杂度 O(n log n) 与几个容易写反的地方
整个算法的时间几乎全花在排序上,是 O(n log n);排完之后取偶数位只是一趟线性扫描,不改变量级。额外空间只用一个累加变量,是 O(1),排序本身的栈开销不计入。
两个写反点最常见。一是忘了排序,直接在乱序数组上取偶数位,这时偶数位并不是每对的较小值,加出来会偏大;二是排了序却取成奇数下标,拿的是每对较大值,同样偏大一截。至于只有一对、所有数相等、数组含负数这些情况,规则一律不变,排序取偶数位照样成立。
▶ 动画逐步走查(共 21 步)——想跟着动画一帧帧对照就展开
- 3记住「排序后取偶数下标之和」这套口诀,下面每一帧都在套它。
- 4这是原始乱序数组 [13,6,1,28,5,17,4,9]。贪心的第一步是排序,先把它从小到大理顺,之后相邻的数才能正好配成一对。
- 5第 1 轮:在还没排序的那段里挑出最小的 1,放到下标 0(绿色)。它左边的蓝色部分都已经从小到大排好了。
- 6第 2 轮:在还没排序的那段里挑出最小的 4,放到下标 1(绿色)。它左边的蓝色部分都已经从小到大排好了。
- 7第 3 轮:在还没排序的那段里挑出最小的 5,放到下标 2(绿色)。它左边的蓝色部分都已经从小到大排好了。
- 8第 4 轮:在还没排序的那段里挑出最小的 6,放到下标 3(绿色)。它左边的蓝色部分都已经从小到大排好了。
- 9第 5 轮:在还没排序的那段里挑出最小的 9,放到下标 4(绿色)。它左边的蓝色部分都已经从小到大排好了。
- 10第 6 轮:在还没排序的那段里挑出最小的 13,放到下标 5(绿色)。它左边的蓝色部分都已经从小到大排好了。
- 11第 7 轮:在还没排序的那段里挑出最小的 17,放到下标 6(绿色)。它左边的蓝色部分都已经从小到大排好了。
- 12排序完成,现在数组是 [1,4,5,6,9,13,17,28],从左到右一个不大于下一个。接下来按相邻两个一对来配。
- 13先体会贪心的核心:最大的 28 不管和谁配,它都是那一对里较大的,永远拿不到。既然它一定被舍弃,就让它去和紧挨着的次大 17 配对,这样次大的 17 就能被保留成较小值。排序正是为此服务。
- 14排好序后,从左到右相邻两个配成一对。每一对里左边的数一定不大于右边,所以较小值就是左边、也就是偶数下标的那个。下面逐对来取。
- 15看第 1 对:1 和 4。排过序,左边的 1 不会比右边的 4 大,所以这一对的较小值就是 1。
- 16把较小的 1 收进答案(变绿),右边较大的 4 注定当不了较小值,灰掉舍弃。现在 ans 累计到 1。
- 17看第 2 对:5 和 6。排过序,左边的 5 不会比右边的 6 大,所以这一对的较小值就是 5。
- 18把较小的 5 收进答案(变绿),右边较大的 6 注定当不了较小值,灰掉舍弃。现在 ans 累计到 6。
- 19看第 3 对:9 和 13。排过序,左边的 9 不会比右边的 13 大,所以这一对的较小值就是 9。
- 20把较小的 9 收进答案(变绿),右边较大的 13 注定当不了较小值,灰掉舍弃。现在 ans 累计到 15。
- 21看第 4 对:17 和 28。排过序,左边的 17 不会比右边的 28 大,所以这一对的较小值就是 17。
- 22把较小的 17 收进答案(变绿),右边较大的 28 注定当不了较小值,灰掉舍弃。现在 ans 累计到 32。
- 23四对都取完了,绿色这四个偶数位 1、5、9、17 加起来正好 32,这就是最大总和。灰色那几个大数在各自的对里都被舍弃了。
⚠️ 容易写错的地方
✗ 错:不排序直接取偶数位累加
✓ 对:必须先从小到大排序
乱序时偶数位不是每对的较小值,结果会错
✗ 错:取奇数下标(每对较大值)
✓ 对:取偶数下标(每对较小值)
排序后每对左边即偶数位才是 min,奇数位是 max
✗ 错:想枚举所有配对方式找最优
✓ 对:贪心一步到位,排序后取偶数位即最优
枚举是阶乘级,贪心已被交换论证证明最优
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def arrayPairSum(self, nums: List[int]) -> int:
nums.sort()
return sum(nums[::2])C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int arrayPairSum(vector<int>& nums) {
sort(nums.begin(), nums.end());
int ans = 0;
for (int i = 0; i < nums.size(); i += 2) {
ans += nums[i];
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int arrayPairSum(int[] nums) {
Arrays.sort(nums);
int ans = 0;
for (int i = 0; i < nums.length; i += 2) {
ans += nums[i];
}
return ans;
}
}复杂度
时间
O(n log n)
排序主导,之后一次遍历取偶数位
空间
O(1)
只用一个累加变量(排序栈开销不计入)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 数组拆分 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这个贪心为什么一定最优,能严格证明吗?+
能,用交换论证。假设某个最优配法里有一个数没跟它排序后的邻居配对,那总能找到相关的两对,把它们的配对方式对调一下,对调后所有对的较小值之和不会变小。对每一处不满足相邻配对的地方反复对调,最终就能把这个最优配法整理成排序后相邻配对的样子,说明相邻配对的总和不比任何配法差,于是它就是最大值。
如果反过来求每对较大值之和的最小值呢?+
思路对称,同样先排序,但改取奇数下标 1、3、5… 也就是每对的较大值。要让每个较大值尽量小,就让大数彼此靠近配对、互相当对方那一对里的较大值,最小的那批数则被当作较小值舍弃。认出排序后按固定奇偶下标取数这个结构,一类配对求和题都能照着套。
不排序、直接在原数组上取偶数位为什么不行?+
因为偶数下标是每对较小值这个结论,只在排序之后才成立。数组乱着的时候,下标 0、2、4… 上的数跟大小没关系,取出来既不一定是较小值、也凑不成合法配对。拿 [6,2,6,5,1,2] 说,不排序直接取偶数位是 6、6、1 加起来 13,比正确答案 9 还大,可这三个数在原数组里根本配不成三对合法的 (a, b),13 是个取不到的假数。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 数组拆分 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。