优势洗牌 图解题解
这道题到底在问什么
- 输入
- nums1=[2,7,11,15], nums2=[1,10,4,11]
- 输出
- [2,11,7,15](优势=4)
最优解:为什么这么做
一句话答案:LeetCode 870 优势洗牌用排序加贪心(田忌赛马):nums1 从弱到强,能赢对手当前最弱就赢、赢不了就拿最弱的去垫,答案按对手原下标填回。时间 O(n log n)、空间 O(n)。
优势洗牌要我们排出什么顺序
给两个等长数组 nums1 和 nums2。把 nums1 重新排一个顺序,让 nums1[i] > nums2[i] 成立的位置尽量多,这个「我方在多少位上更大」的个数就叫优势。nums2 不动,只有 nums1 能调。题面 nums1=[2,7,11,15]、nums2=[1,10,4,11] 时,最好的排法之一是 [2,11,7,15],四个位置全部我方更大,优势 4。
把所有排列都试一遍为什么根本跑不动
把 nums1 的每种排列都摆出来、逐一数优势、留最大的,是照定义写的笨办法。但 n 个数有 n! 种排列,n 才 10 就三百多万种、20 就是天文数字,光枚举就卡死。这条路只有 n 是个位数时能走,题目规模下完全不现实。
田忌赛马的老主意怎么搬到这道题上
田忌赛马讲的是:自己的马从弱到强排好,对手的也从弱到强看。轮到我方当前最弱的一匹,先跟对手当前最弱的比。能赢就赢下这场最省力的胜利;连对手最弱都赢不了,这匹就是废棋,与其浪费,不如拿去对付对手最强那匹。反正要输一场,就用最弱的去输、把能打的省下来赢别人。赢的判定是严格大于:相等不算优势,题目要的是 nums1[i] > nums2[i]。
两个指针怎么夹,答案又怎么填回原位
先给 nums1 排序;再把 nums2 连同原始下标打包排序(存成 (值, 原下标) 配对),排完序仍知道每匹对手马原本站哪一位。给对手排好的数组架两个指针,一个指最弱、一个指最强,从两头往中间夹。从弱到强遍历 nums1 每匹马 v:赢不了对手最弱那匹(v ≤ 对手最弱的值),就填进对手最强那匹的原始下标、最强指针左移;能赢就填进最弱那匹的原始下标、最弱指针右移。为什么非记原始下标?答案要和原 nums2 一一对位,排序打乱了位置,得把每匹马填回对手原本站的那格。
[2,7,11,15] 配对手,两指针把答案填回原位
nums1 排序后仍是 [2,7,11,15]。把 nums2=[1,10,4,11] 打包排序,按值升序排成 (1,0)、(4,2)、(10,1)、(11,3),最弱指针指 (1,0)、最强指针指 (11,3),ans 四格全空。逐匹上场:2 > 对手最弱 1,能赢,填进原下标 0;7 > 剩下的最弱 4,填进原下标 2;11 > 10,填进原下标 1;15 > 11,填进原下标 3。填完得 ans=[2,11,7,15],优势 4。这个例子四场全赢,「赢不了就送去垫」那支没触发;想看它发威,去动画换个更胶着的例子。
相等也当赢,优势会凭空多算几场
复杂度上,两次排序是瓶颈 O(n log n),双指针只扫一趟 O(n);空间 O(n),用在存对手的 (值, 原下标) 配对和答案数组。几个容易写反的地方:判赢是严格大于,若相等也算赢,优势会多算几场;赢不了时废马要送给对手当前最强那匹,随手丢给最弱的,就白扔一次本能赢的机会;排序前若没把原始下标打包,填答案时就不知道该塞回哪格,对位全乱。边界上,全部能赢时最强指针不动、最弱指针走到底;全赢不了时反过来;只剩一个元素时比一次,要么赢要么送,都对。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3口诀:能赢就赢、赢不了就用最弱的去送。下面每帧都在套它。
- 4这是我方马的原始顺序,乱的。第一步先把它们从弱到强排序,方便「先派弱马」。
- 5我方马排好了,从弱到强是 [ 2, 5, 9, 11, 14, 20 ]。最弱的 2 排最前,最强的 20 排最后。
- 6这是对手马,顺序是固定的,因为答案要一一对位。我们要记住每匹对手马的原始位置,赢谁、送谁都要把我方马填回对手那个位置上。
- 7把对手也从弱到强排一份(原始位置另记着)。架两个指针:i 指对手当前最弱的马、j 指当前最强的马,从两头往中间夹。答案数组 ans 先全空着。
- 8开战!两队都从弱到强排好,对手两端架上 i、j 指针。我方按从弱到强的顺序一匹匹上场,第一匹是最弱的 2。
- 9轮到我方目前最弱的马 2(侧栏高亮)。先看对手当前最弱的马 6。2 连对手最弱的 6 都赢不了,说明它谁也赢不了。既然是废棋,就拿去送给对手最强的马。
- 10把 2 送给对手最强的 19(它的原始位置 3),这场我们认输、标红,但用一匹废马换掉了对手的王牌。j 左移,下次对手的「最强」就换成更小的了。ans 第 3 位填上 2。
- 11轮到我方目前最弱的马 5(侧栏高亮)。先看对手当前最弱的马 6。5 连对手最弱的 6 都赢不了,说明它谁也赢不了。既然是废棋,就拿去送给对手最强的马。
- 12把 5 送给对手最强的 15(它的原始位置 5),这场我们认输、标红,但用一匹废马换掉了对手的王牌。j 左移,下次对手的「最强」就换成更小的了。ans 第 5 位填上 5。
- 13轮到我方目前最弱的马 9(侧栏高亮)。先看对手当前最弱的马 6。9 比 6 大,能赢!那就别浪费,拿它赢下这场最划算的胜利。
- 14成交!把 9 安排去赢对手的 6(它的原始位置 2),对手这匹标绿表示被我们赢了。i 右移,接着看对手下一个最弱的。ans 第 2 位填上 9。
- 15轮到我方目前最弱的马 11(侧栏高亮)。先看对手当前最弱的马 7。11 比 7 大,能赢!那就别浪费,拿它赢下这场最划算的胜利。
- 16成交!把 11 安排去赢对手的 7(它的原始位置 4),对手这匹标绿表示被我们赢了。i 右移,接着看对手下一个最弱的。ans 第 4 位填上 11。
- 17轮到我方目前最弱的马 14(侧栏高亮)。先看对手当前最弱的马 8。14 比 8 大,能赢!那就别浪费,拿它赢下这场最划算的胜利。
- 18成交!把 14 安排去赢对手的 8(它的原始位置 0),对手这匹标绿表示被我们赢了。i 右移,接着看对手下一个最弱的。ans 第 0 位填上 14。
- 19轮到我方目前最弱的马 20(侧栏高亮)。先看对手当前最弱的马 12。20 比 12 大,能赢!那就别浪费,拿它赢下这场最划算的胜利。
- 20成交!把 20 安排去赢对手的 12(它的原始位置 1),对手这匹标绿表示被我们赢了。i 右移,接着看对手下一个最弱的。ans 第 1 位填上 20。
- 21全部安排完毕。绿格是我们赢下的 4 场,从左边长出来;红格是送掉的 2 场,从右边长出来,两边在中间会合。优势 = 绿格数 = 4,已经是最大值。
- 22换个角度看我方马:绿色这几匹去赢了对手,红色这几匹是赢不了被拿去送的。注意被送的都是最弱的那几匹,强马一匹没浪费。
- 23这就是最终的 ans。它是按对手原始位置填出来的,所以直接和原始 nums2 对位:绿表示这一位我方更大、赢了。数一数,赢的正好是 4 场。
- 24最后回到对手的原始顺序 nums2。绿格是我方那一位更大、拿下优势的位置,一共 4 个,这就是最大优势。红格是认输的位置,已被我们的废马占着,一分没多送。
⚠️ 容易写错的地方
✗ 错:赢不了时随便丢,没丢给最强
✓ 对:赢不了就送给对手当前最强(j 那匹)
废马要消耗掉对手的王牌,留着弱马反而可能错过本可赢的场次
✗ 错:忘了记对手原始下标
✓ 对:排序时把(值,原始下标)一起打包
答案要一一对位,必须把我方马填回对手的原始位置
✗ 错:用 < 还是 ≤ 判赢
✓ 对:能赢的条件是严格大于,所以 v ≤ t[i] 视为赢不了
相等不算优势,题目要求严格大于
完整代码(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 advantageCount(self, nums1: List[int], nums2: List[int]) -> List[int]:
nums1.sort()
t = sorted((v, i) for i, v in enumerate(nums2))
n = len(nums2)
ans = [0] * n
i, j = 0, n - 1
for v in nums1:
if v <= t[i][0]:
ans[t[j][1]] = v
j -= 1
else:
ans[t[i][1]] = v
i += 1
return ansC++
#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:
vector<int> advantageCount(vector<int>& nums1, vector<int>& nums2) {
int n = nums1.size();
vector<pair<int, int>> t;
for (int i = 0; i < n; ++i) t.push_back({nums2[i], i});
sort(t.begin(), t.end());
sort(nums1.begin(), nums1.end());
int i = 0, j = n - 1;
vector<int> ans(n);
for (int v : nums1) {
if (v <= t[i].first)
ans[t[j--].second] = v;
else
ans[t[i++].second] = v;
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int[] advantageCount(int[] nums1, int[] nums2) {
int n = nums1.length;
int[][] t = new int[n][2];
for (int i = 0; i < n; ++i) {
t[i] = new int[] {nums2[i], i};
}
Arrays.sort(t, (a, b) -> a[0] - b[0]);
Arrays.sort(nums1);
int[] ans = new int[n];
int i = 0, j = n - 1;
for (int v : nums1) {
if (v <= t[i][0]) {
ans[t[j--][1]] = v;
} else {
ans[t[i++][1]] = v;
}
}
return ans;
}
}复杂度
时间
O(n log n)
两次排序是瓶颈,之后一趟线性扫描
空间
O(n)
存对手的(值,原始下标)配对以及答案数组
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 优势洗牌 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
赢不了的时候,为什么一定要送给对手最强的那匹,随便丢不行吗?+
盯着对手当前最强的马看:能赢它的,只有我方比它更强的马;要是连我方最强都赢不了它,这一局注定要输。既然注定输,就该用最没用、赢不了任何人的最弱马去对位它,把还能打的马全省下来。反过来,若随手把一匹本能赢别人的马丢去对付这个强敌,就白白亏掉一场胜利。所以送马也得挑对象,专把废马送去消耗对手王牌。
怎么一眼看出这题该用「排序 + 贪心」,而不是别的解法?+
信号是两组数要一一配对、我方顺序又能随便调,求匹配得分最大。这类题排序后往往能贪心:两边都排好,用指针从一端或两端推进,每步只做眼前最划算的配对。分发饼干、用最少的箭引爆气球都是同一副骨架——认出「排序后能贪心配对」,这一大类题就一起打开了。
题面例子四场全赢,那「赢不了送去垫」的分支到底啥时候才用得上?+
当我方最弱的马连对手最弱的马都赢不了时才触发。比如我方 [2,3]、对手 [4,5]:2 赢不了对手最弱的 4,就把 2 送去对付对手最强的 5、认输;再看 3 对上剩下的 4,还是赢不了,也送掉;最终优势 0。这一支的意义是止损,反正要输,就用最弱的马输、绝不拿有战斗力的马去填。题面那个例子刚好我方整体更强,才显不出它。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 优势洗牌 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。