三数之和的多种可能 图解题解
这道题到底在问什么
- 输入
- arr=[1,2,3,3,3,4,4], target=9
- 输出
- 8
先想最直接的笨办法
记住口诀:先数清每个值几次,再枚举三个值,按相不相等用组合数算搭配。下面每帧都在套它。(动画第 3 步)
最优解:为什么这么做
一句话答案:LeetCode 923 三数之和的多种可能:值域只有 0 到 100,先用计数表数清每个值几次,再枚举三个值 a≤b≤c、按相等情况用组合数累加下标搭配,边加边对 1e9+7 取模。时间 O(n+K²)、空间 O(1)。
数的是下标元组,不是不同的值组合
给数组 arr 和目标 target,数出多少组下标 (i, j, k) 满足 i < j < k、且 arr[i]+arr[j]+arr[k]=target,答案对 10^9+7 取模。数的是下标元组个数,不是不同值组合个数:同一个值出现多次,用不同下标挑出来算不同答案。题面 arr=[1,2,3,3,3,4,4]、target=9 答案 8,因为 3 有三个、4 有两个,一种值搭配摊出好几组。取模是防组合太多溢出。
三重循环枚举下标,长度到 3000 就跑不完
上来先写三重循环:i、j、k 各扫下标,凑齐就看和等不等于 target。可 arr 长度到 3000,O(n³) 量级 2.7×10¹⁰,判题机就超时。它把每组下标单独试一遍,可值就那么几个——数组里的数全落在 0 到 100 这段值域里,满打满算才 101 种。没必要盯着几千个下标枚举。
值域只有 101,把枚举从下标搬到值上
改成盯着值来数。先扫一遍 arr,用计数表数清每个值出现几次(记作 cnt,cnt[3] 就是 3 的个数)。之后枚举三个值 a≤b≤c,只要 a+b+c=target 就是一组命中的值搭配。这组值对应多少种下标取法,看三值是否相等——相等的值只能从同一批下标里挑,得用组合数 C(m,r),也就是从 m 个里挑 r 个有几种挑法,不能简单相乘。
三个值相不相等,组合数分四种情形算
按 a、b、c 相不相等分四种。三个都不同,各自挑一个乘起来 cnt[a]×cnt[b]×cnt[c]。前两个相同 a=b≠c,从 cnt[a] 里挑 2 个再挑一个 c,是 C(cnt[a],2)×cnt[c]。后两个相同 a≠b=c,对称地 cnt[a]×C(cnt[c],2)。三个全相同,挑 3 个即 C(cnt[a],3)。每算一组就累加,每次都对 10^9+7 取模,别攒到最后。
题面这个数组,8 组是怎么数出来的
顺着题面 arr=[1,2,3,3,3,4,4]、target=9 走一遍。先数出 cnt:1 有 1 个、2 有 1 个、3 有 3 个、4 有 2 个。枚举 a≤b≤c 里和为 9 的,只三组命中。(1,4,4) 后两位相同:cnt[1]×C(cnt[4],2)=1×C(2,2)=1。(2,3,4) 三个都不同:cnt[2]×cnt[3]×cnt[4]=1×3×2=6。(3,3,3) 三个全相同:C(cnt[3],3)=C(3,3)=1。三组相加 1+6+1=8,正是题面给的 8。(1,1,2)、(2,4,4) 等和不为 9 的跳过。
相同值用乘法,会把同一个下标选两回
过程先 O(n) 建 cnt,再枚举 K 种不同值(K≤101)做 O(K²) 配对,合起来 O(n+K²),值域固定时近似线性;计数表 101 格,空间 O(1)。相同值那支图省事写 cnt×cnt,会把同一下标当两个元素配、把顺序数两遍,多出不存在的搭配。取模若只在返回前做一次,累加途中的和会超出整型范围、算出乱数,得边加边取。target 凑不齐时答案自然是 0,不用特判。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3记住口诀:先数清每个值几次,再枚举三个值,按相不相等用组合数算搭配。下面每帧都在套它。
- 4先一遍扫数组数清每个值出现几次。现在读到 arr[0] = 1,把它在计数表里的次数加到 1。
- 5先一遍扫数组数清每个值出现几次。现在读到 arr[1] = 2,把它在计数表里的次数加到 1。
- 6先一遍扫数组数清每个值出现几次。现在读到 arr[2] = 3,把它在计数表里的次数加到 1。
- 7先一遍扫数组数清每个值出现几次。现在读到 arr[3] = 3,把它在计数表里的次数加到 2。
- 8先一遍扫数组数清每个值出现几次。现在读到 arr[4] = 3,把它在计数表里的次数加到 3。
- 9先一遍扫数组数清每个值出现几次。现在读到 arr[5] = 4,把它在计数表里的次数加到 1。
- 10先一遍扫数组数清每个值出现几次。现在读到 arr[6] = 4,把它在计数表里的次数加到 2。
- 11计数表全部建好:值 1 有 1 个、2 有 1 个、3 有 3 个、4 有 2 个。接下来枚举三个值 a ≤ b ≤ c,看哪些组合相加正好等于 9。
- 12开始枚举。先看 (1, 1, 2),三个值相加是 4,不等于 9,这组跳过。
- 13再看 (1, 2, 3),和是 6,没到 9,差一点,跳过。
- 14再看 (1, 3, 4),和是 8,没到 9,差一点,跳过。
- 15(1, 4, 4) 三个值相加正好是 9,命中!高亮的就是这几个值所在的「桶」。下一帧算它能凑出多少种下标搭配。
- 16这组是「两值相同」。一个 1 加两个 4:cnt[1] × C(2,2) = 1 × 1 = 1。把这 1 种搭配加进答案,ans 现在是 1。
- 17再看 (2, 2, 4),和是 8,没到 9,差一点,跳过。
- 18(2, 3, 4) 三个值相加正好是 9,命中!高亮的就是这几个值所在的「桶」。下一帧算它能凑出多少种下标搭配。
- 19这组是「三值都不同」。三个值都不同:cnt[2] × cnt[3] × cnt[4] = 1 × 3 × 2 = 6。把这 6 种搭配加进答案,ans 现在是 7。
- 20再看 (2, 4, 4),和是 10,比 9 大,跳过。
- 21(3, 3, 3) 三个值相加正好是 9,命中!高亮的就是这几个值所在的「桶」。下一帧算它能凑出多少种下标搭配。
- 22这组是「三值相同」。三个值都是 3:从 3 个 3 里挑 3 个,组合数 C(3,3) = 1。把这 1 种搭配加进答案,ans 现在是 8。
- 23再看 (3, 3, 4),和是 10,比 9 大,跳过。
- 24这里只展示部分代表性组合,但所有命中 9 的组合都完整演了:三组分别贡献 1、6、1,合计 8。动画用的是「按值枚举 a≤b≤c + 组合数」的直观版:枚举全部三值组合,cnt 不足或和不等于 9 的直接跳过。下一帧 step 25 的参考代码则是等价的「下标版」(枚举中间下标 j、左侧下标 i,再用 cnt 查右侧数量),两者结果一致,但复杂度表达不同:动画按值枚举版是 O(n + K²),参考代码下标版是 O(n²)。这就是满足条件的下标元组总数。
⚠️ 容易写错的地方
✗ 错:直接三重循环枚举下标
✓ 对:先计数,再按值组合
数组可达 3000,三重循环 O(n³) 会超时;值域只有 101,按值枚举快得多
✗ 错:两个值相同也用 cnt×cnt 乘
✓ 对:同一个值挑两个要用 C(cnt,2)
cnt×cnt 会把「同一个下标选两次」和「顺序」重复算,组合数 C(cnt,2) 才对
✗ 错:忘了取模或只在最后取一次
✓ 对:每次累加都取模 1e9+7
中间和可能溢出,边加边取模才安全
完整代码(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 threeSumMulti(self, arr: List[int], target: int) -> int:
mod = 10**9 + 7
cnt = Counter(arr)
ans = 0
for j, b in enumerate(arr):
cnt[b] -= 1
for a in arr[:j]:
c = target - a - b
ans = (ans + cnt[c]) % mod
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:
int threeSumMulti(vector<int>& arr, int target) {
const int mod = 1e9 + 7;
int cnt[101]{};
for (int x : arr) {
++cnt[x];
}
int n = arr.size();
int ans = 0;
for (int j = 0; j < n; ++j) {
--cnt[arr[j]];
for (int i = 0; i < j; ++i) {
int c = target - arr[i] - arr[j];
if (c >= 0 && c <= 100) {
ans = (ans + cnt[c]) % mod;
}
}
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int threeSumMulti(int[] arr, int target) {
final int mod = (int) 1e9 + 7;
int[] cnt = new int[101];
for (int x : arr) {
++cnt[x];
}
int n = arr.length;
int ans = 0;
for (int j = 0; j < n; ++j) {
--cnt[arr[j]];
for (int i = 0; i < j; ++i) {
int c = target - arr[i] - arr[j];
if (c >= 0 && c < cnt.length) {
ans = (ans + cnt[c]) % mod;
}
}
}
return ans;
}
}复杂度
时间
O(n + K²)
动画按值枚举:先扫一遍 arr 建 cnt 是 O(n),再枚举不同值种类 K≤101 是 O(K²);参考代码下标版是 O(n²)
空间
O(1)
计数数组大小固定 101(值域 0 到 100)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 三数之和的多种可能 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题和经典 3Sum 有什么联系和区别?+
联系是都在找三个数凑成目标;区别是经典 3Sum 求不同的值组合、要去重,本题求下标元组的数量、要计数。本题值域很小(0 到 100),所以走「计数+组合数」最省事。如果值域很大、计数表铺不开,也可以先排序,固定一个数后在剩下区间用对撞双指针,遇到相等的一段时按段长套组合数算贡献,同样能不重不漏地计数。
不用组合数分四种,能不能像参考代码那样两层循环?+
可以,而且更好写。参考代码里外层固定中间那个值 b、往它左边扫每个 a,要凑成 target 就需要第三个值 c=target−a−b,直接查计数表 cnt[c] 累加就行;关键一步是扫到 b 时先把 cnt[b] 减 1,保证 b 自己不会被当第三个值重复选。这就是按值枚举思路的下标版实现,结果完全一致,还省掉分四种情形的判断,代码短很多。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 三数之和的多种可能 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。