适龄的朋友 图解题解
这道题到底在问什么
- 输入
- ages=[16,16]
- 输出
- 2 (两人互发)
- 输入
- ages=[20,30,100,110,120]
- 输出
- 3 (110→100,120→110,120→100)
先想最直接的笨办法
回放一遍:全程没有两两枚举,只是按年龄分桶 + 对每个年龄查一次有效区间。年龄种类最多 121 种,所以无论多少人,统计都很快。最终答案 7。(动画第 24 步)
最优解:为什么这么做
一句话答案:LeetCode 825 适龄的朋友求一共发出多少条好友请求:把三条排除线并成区间 (0.5·x+7, x],年龄只有 1~120 种,用计数桶让年龄两两统计,把 O(n²) 压到 O(n+120²)。
每个人按年龄发好友请求,这道题到底在数什么
给一个年龄数组 ages,ages[i] 是第 i 人的年龄。用户 x 会向另一人 y 发好友请求,除非踩中三条排除线之一:ay ≤ 0.5·ax+7、ay > ax、或 ay > 100 且 ax < 100(ax、ay 是两人年龄)。三条都不踩才发得出,问全网共发多少条。题面 ages=[16,16] 答案 2,两个 16 岁的人互相都发得出。
两两都比一遍,人一多就比到停不下来
最直白的做法是把每一对 (x, y) 都取出来、逐对套那三条排除线判一次能不能发。可 ages 长度能到两万,两两配对就是四亿次判断,根本跑不动。慢就慢在我们盯着「人」在配对,而人数 n 没有上限可言。
三条排除线其实只圈出一个区间,年龄又只有 120 种
先把三条线并成一句话:x 能发给 y,只需 y 落在区间 (0.5·x+7, x] 里,下界要严格大于、不能取等。第③条 ay > 100 且 ax < 100 其实白给:这种情形 ay 必然大于 ax,已被第②条挡掉。真正省事的在另一头:年龄只落在 1 到 120 之间,共 120 种。既然能不能发只跟年龄有关、跟是谁无关,就别盯着两万个人,改成按年龄分组、让年龄两两配对,把 O(n²) 换成 O(120²) 的固定开销。
按年龄开一个计数桶,再让年龄两两结算
先扫一遍 ages 填计数桶 cnt——cnt[a] 是年龄正好为 a 的人数。再两层循环遍历发送方年龄 ax 和接收方年龄 ay(1 到 120):ay 落在有效区间就能发,累加 cnt[ax] × cnt[ay] 条。只需小心 ax 等于 ay 那格:同龄可互发但不能发给自己,接收人减掉本人,累加 cnt[ax] × (cnt[ax] − 1)。合起来就是 cnt[ax] × (cnt[ay] − int(ax == ay))。
题面两组数据,各自把请求数结算出来
先看 ages=[16,16]:两人都 16 岁,cnt[16]=2。发送方 16 的有效区间 (0.5·16+7, 16] = (15, 16],接收方 16 岁落在里面。同龄减本人,每人发给另外 1 个,共 2 条。
再看 ages=[20,30,100,110,120]。20、30、100 岁的区间里都只有自己,各发 0 条;110 岁区间 (62,110] 里还有 100 岁,发 1 条(110→100);120 岁区间 (67,120] 里有 100 和 110 岁,发 2 条(120→100、120→110)。合计 0+0+0+1+2 = 3,正是题面给的 3。
下界那条线严格大于,同龄那格记得抠掉自己
复杂度上,统计人数 O(n)、两层年龄循环固定 O(120²),合起来 O(n + 120²),空间只一个长度 121 的桶。两处最伤答案的细节都在边界:下界是严格大于 0.5·x+7,等于这个值也要挡在外面,若写成 ay ≥ 0.5·x+7 才排除,就会把边界那档人错放进来、多数出请求;ax 等于 ay 那格必须减本人,漏了同龄组每人都算成能发给自己,人数虚高。单人时区间里除自己没别人,答案是 0。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3三个条件简化成一个区间:第②条说不能发给比自己大的,第③条其实被第②条包住了(年龄超 100 的一定比小于 100 的大,已经被②挡掉)。所以真正起作用的就是 (0.5·x+7, x]。
- 4先把人按年龄分组:这就是计数桶。上方一排是不同年龄,右侧表是每个年龄各有几个人。接下来对每个年龄当发送方,逐一算它能发出多少请求。
- 5轮到年龄 18 当发送方(紫色)。它只能发给年龄在 (16, 18] 里的人:既要大于 16,又不能超过自己 18。下面逐个桶检查谁落在这个区间里。
- 6检查到自己这组年龄 18:同龄之间互发是允许的,但不能发给自己,所以 2 个人里只能收 1 个(减掉本人)。绿色标记区间内有效。
- 7结算年龄 18 这组:每个人能发 1 条,这组有 2 人,一共贡献 2 条请求。总数累计到 2。
- 8轮到年龄 25 当发送方(紫色)。它只能发给年龄在 (19.5, 25] 里的人:既要大于 19.5,又不能超过自己 25。下面逐个桶检查谁落在这个区间里。
- 9检查年龄 18:它没有超过下界 19.5,会被第①条「ages[y] ≤ 0.5·x+7」挡掉,发不出去(标灰)。
- 10检查到自己这组年龄 25:同龄之间互发是允许的,但不能发给自己,所以 1 个人里只能收 0 个(减掉本人)。绿色标记区间内有效。
- 11年龄 25 这组:有效区间里除了自己没别人可发,每人能发 0 条,这组贡献 0 条。总数仍是 2。
- 12轮到年龄 30 当发送方(紫色)。它只能发给年龄在 (22, 30] 里的人:既要大于 22,又不能超过自己 30。下面逐个桶检查谁落在这个区间里。
- 13检查年龄 18:它没有超过下界 22,会被第①条「ages[y] ≤ 0.5·x+7」挡掉,发不出去(标灰)。
- 14检查年龄 25:它大于下界 22,落在有效区间里(标绿)。这组 1 人每个都能成为接收方。
- 15检查到自己这组年龄 30:同龄之间互发是允许的,但不能发给自己,所以 1 个人里只能收 0 个(减掉本人)。绿色标记区间内有效。
- 16结算年龄 30 这组:每个人能发 1 条,这组有 1 人,一共贡献 1 条请求。总数累计到 3。
- 17轮到年龄 40 当发送方(紫色)。它只能发给年龄在 (27, 40] 里的人:既要大于 27,又不能超过自己 40。下面逐个桶检查谁落在这个区间里。
- 18检查年龄 18:它没有超过下界 27,会被第①条「ages[y] ≤ 0.5·x+7」挡掉,发不出去(标灰)。
- 19检查年龄 25:它没有超过下界 27,会被第①条「ages[y] ≤ 0.5·x+7」挡掉,发不出去(标灰)。
- 20检查年龄 30:它大于下界 27,落在有效区间里(标绿)。这组 1 人每个都能成为接收方。
- 21检查到自己这组年龄 40:同龄之间互发是允许的,但不能发给自己,所以 2 个人里只能收 1 个(减掉本人)。绿色标记区间内有效。
- 22结算年龄 40 这组:每个人能发 2 条,这组有 2 人,一共贡献 4 条请求。总数累计到 7。
- 23把四个年龄组各自贡献的请求数加起来:2 + 0 + 1 + 4 = 7。这就是全网产生的好友请求总数。
- 24回放一遍:全程没有两两枚举,只是按年龄分桶 + 对每个年龄查一次有效区间。年龄种类最多 121 种,所以无论多少人,统计都很快。最终答案 7。
⚠️ 容易写错的地方
✗ 错:把区间下界当成可取(写成 ages[y] ≥ 0.5·x+7)
✓ 对:是严格大于,等于也排除
第①条是 ages[y] ≤ 0.5·x+7 才排除,所以有效是 ages[y] > 0.5·x+7,边界值要被挡掉
✗ 错:同龄统计忘了减自己
✓ 对:ax==ay 时人数要减 1
人不会向自己发请求,cnt[x] 个同龄人里每人只能发给另外 cnt[x]−1 个
✗ 错:以为第③条独立要单独处理
✓ 对:它被第②条完全包含
ages[y] > 100 且 ages[x] < 100 时必有 ages[y] > ages[x],已被第②条排除,可忽略
完整代码(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 numFriendRequests(self, ages: List[int]) -> int:
cnt = [0] * 121
for x in ages:
cnt[x] += 1
ans = 0
for ax, x in enumerate(cnt):
for ay, y in enumerate(cnt):
if not (ay <= 0.5 * ax + 7 or ay > ax or (ay > 100 and ax < 100)):
ans += x * (y - int(ax == ay))
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 numFriendRequests(vector<int>& ages) {
const int m = 121;
vector<int> cnt(m);
for (int x : ages) {
++cnt[x];
}
int ans = 0;
for (int ax = 1; ax < m; ++ax) {
for (int ay = 1; ay < m; ++ay) {
if (!(ay <= 0.5 * ax + 7 || ay > ax || (ay > 100 && ax < 100))) {
ans += cnt[ax] * (cnt[ay] - (ax == ay ? 1 : 0));
}
}
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int numFriendRequests(int[] ages) {
final int m = 121;
int[] cnt = new int[m];
for (int x : ages) {
++cnt[x];
}
int ans = 0;
for (int ax = 1; ax < m; ++ax) {
for (int ay = 1; ay < m; ++ay) {
if (!(ay <= 0.5 * ax + 7 || ay > ax || (ay > 100 && ax < 100))) {
ans += cnt[ax] * (cnt[ay] - (ax == ay ? 1 : 0));
}
}
}
return ans;
}
}复杂度
时间
O(n + A²)
n 统计人数,A=121 是年龄上限,两层年龄循环与人数无关
空间
O(A)
一个长度 121 的计数桶
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 适龄的朋友 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么非得用计数桶,直接两两枚举不行吗?+
两两枚举要 O(n²) 次判断,而 ages 长度能到两万,最坏四亿次配对跑不动。计数桶抓住的是一个关键事实:能不能发只由两人的年龄决定,跟他们具体是谁无关。年龄只有 1 到 120 共 120 种取值,所以先按年龄把人数统计进 cnt 数组,再让 120 种年龄两两配对,配对量固定在 120²,与人数无关。把「元素个数 n」换成「值域大小 120」来降复杂度,是计数类题的通用手法。
第③条 ay > 100 且 ax < 100 真的可以直接不管吗?+
可以,它整个被第②条包住。若 ay > 100 而 ax < 100,那么 ay 一定大于 ax,这种配对早就被第②条 ay > ax 排除了。也就是说第③条挡下的配对,第②条无一例外都已经挡下,它不会多排除任何一对,属于纯冗余。写代码时把它一并写进排除判断当然也对,只是删掉不影响结果——这道题最爱考的观察点就是它。
同龄那一格为什么要减 1,不减会错在哪?+
同龄之间是允许互发的,比如两个 16 岁的人可以互相发。但一个人不会向自己发请求,所以 cnt[16] 个同龄人里,每人的可选接收方只有另外 cnt[16] − 1 个。累加时这一格要写成 cnt[16] × (cnt[16] − 1)。若忘了减、写成 cnt[16] × cnt[16],等于让每个人都能发给自己一条,同龄组人越多、虚增的请求越多,答案会系统性偏大。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 适龄的朋友 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。