题目描述
思路解析
一句话答案: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),排序本身的栈开销不计入。
两个写反点最常见。一是忘了排序,直接在乱序数组上取偶数位,这时偶数位并不是每对的较小值,加出来会偏大;二是排了序却取成奇数下标,拿的是每对较大值,同样偏大一截。至于只有一对、所有数相等、数组含负数这些情况,规则一律不变,排序取偶数位照样成立。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记住「排序后取偶数下标之和」这套口诀,下面每一帧都在套它。
这是原始乱序数组 [13,6,1,28,5,17,4,9]。贪心的第一步是排序,先把它从小到大理顺,之后相邻的数才能正好配成一对。
第 1 轮:在还没排序的那段里挑出最小的 1,放到下标 0(绿色)。它左边的蓝色部分都已经从小到大排好了。
第 2 轮:在还没排序的那段里挑出最小的 4,放到下标 1(绿色)。它左边的蓝色部分都已经从小到大排好了。
第 3 轮:在还没排序的那段里挑出最小的 5,放到下标 2(绿色)。它左边的蓝色部分都已经从小到大排好了。
第 4 轮:在还没排序的那段里挑出最小的 6,放到下标 3(绿色)。它左边的蓝色部分都已经从小到大排好了。
第 5 轮:在还没排序的那段里挑出最小的 9,放到下标 4(绿色)。它左边的蓝色部分都已经从小到大排好了。
第 6 轮:在还没排序的那段里挑出最小的 13,放到下标 5(绿色)。它左边的蓝色部分都已经从小到大排好了。
第 7 轮:在还没排序的那段里挑出最小的 17,放到下标 6(绿色)。它左边的蓝色部分都已经从小到大排好了。
排序完成,现在数组是 [1,4,5,6,9,13,17,28],从左到右一个不大于下一个。接下来按相邻两个一对来配。
先体会贪心的核心:最大的 28 不管和谁配,它都是那一对里较大的,永远拿不到。既然它一定被舍弃,就让它去和紧挨着的次大 17 配对,这样次大的 17 就能被保留成较小值。排序正是为此服务。
排好序后,从左到右相邻两个配成一对。每一对里左边的数一定不大于右边,所以较小值就是左边、也就是偶数下标的那个。下面逐对来取。
看第 1 对:1 和 4。排过序,左边的 1 不会比右边的 4 大,所以这一对的较小值就是 1。
把较小的 1 收进答案(变绿),右边较大的 4 注定当不了较小值,灰掉舍弃。现在 ans 累计到 1。
看第 2 对:5 和 6。排过序,左边的 5 不会比右边的 6 大,所以这一对的较小值就是 5。
把较小的 5 收进答案(变绿),右边较大的 6 注定当不了较小值,灰掉舍弃。现在 ans 累计到 6。
看第 3 对:9 和 13。排过序,左边的 9 不会比右边的 13 大,所以这一对的较小值就是 9。
把较小的 9 收进答案(变绿),右边较大的 13 注定当不了较小值,灰掉舍弃。现在 ans 累计到 15。
看第 4 对:17 和 28。排过序,左边的 17 不会比右边的 28 大,所以这一对的较小值就是 17。
把较小的 17 收进答案(变绿),右边较大的 28 注定当不了较小值,灰掉舍弃。现在 ans 累计到 32。
四对都取完了,绿色这四个偶数位 1、5、9、17 加起来正好 32,这就是最大总和。灰色那几个大数在各自的对里都被舍弃了。
边界先想清:只有一对、全相等、含负数,规则都不变。
面试重点:会用交换论证说清贪心为什么对。
参考代码
from __future__ import annotationsfrom 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])复杂度
- 时间:O(n log n),排序主导,之后一次遍历取偶数位
- 空间:O(1),只用一个累加变量(排序栈开销不计入)
易错点
面试追问把动画讲成自己的话
追问这个贪心怎么严格证明?
追问如果题目改成求每对 max 之和的最小值呢?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
种花问题
LeetCode 605 · 简单 · 沿着 贪心套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题