回旋镖的数量 图解题解
这道题到底在问什么
- 输入
- points = [(1,1),(0,1),(2,1),(1,0),(1,2)]
- 输出
- 20
最优解:为什么这么做
一句话答案:LeetCode 447 回旋镖的数量用哈希计数距离:固定每个点当中心,按平方距离把其余点分组,某距离上 m 个点贡献 m×(m−1) 个有序对,时间 O(n²)、空间 O(n)。
平面上一堆点,回旋镖 (i,j,k) 讲顺序怎么数
给平面上 n 个互不相同的点 points,回旋镖是三元组 (i, j, k),只要求中心点 i 到 j 的距离等于 i 到 k 的距离。三元组讲顺序,(i,j,k) 和 (i,k,j) 算两个不同回旋镖。题面 points=[(1,1),(0,1),(2,1),(1,0),(1,2)] 摆成十字形,答案是 20。
把每个三元组都摆出来验,点一多就跑不动
最直接的想法是三层循环,把每个 (i, j, k) 都列出来,逐个验证 i 到 j、i 到 k 距离是否相等。可 n 个点选三个位置就是 n×n×n 量级,时间 O(n³);点数到几百上千就慢得没法接受。
回旋镖的相等条件,其实全挂在中心那个点上
回旋镖的约束都写在中心 i 头上:i 到 j 的距离等于 i 到 k 的距离,而 j、k 彼此无关。既然只认中心,就固定一个点当中心,量它到其余各点的距离,把等距的点归为一组。某距离上落了 m 个点,从中讲顺序挑出 j、k 两个就凑成一个回旋镖——有序挑两个共 m×(m−1) 种,不除以 2,因为回旋镖分先后。
分组用一张哈希表记「距离 → 点数」,按距离当键直接取值、查一次几乎不花时间,扫一遍就把每个距离的 m 数清。比距离不必开平方根,直接用平方距离(横纵坐标差的平方之和)当键:浮点小数判相等有精度风险,整数平方又快又不比错。
固定中心、量距离、按组累加,三步怎么走
流程钉死三步:挑一个点当中心;另起一张空表遍历其余各点,算它到中心的平方距离、往对应距离的计数加一;中心量完把每个距离的点数 m 取出,累加 m×(m−1) 到答案。再换下一个点当中心重复。每换一个中心都得另起新表,距离相对当前中心算,沿用旧表会把别人计数混进来。n 个点各当一次中心,贡献全加起来就是回旋镖总数。
五个十字点逐个当中心,各贡献多少
先让 (1,1) 当中心:到 (0,1)、(2,1)、(1,0)、(1,2) 的平方距离都是 1,四个点全挤在距离 1 上,m=4,贡献 4×3=12。换 (0,1) 当中心:到 (1,1) 平方距离 1、到 (2,1) 是 4、到 (1,0) 和 (1,2) 都是 2,只有距离 2 上凑了 2 个点,贡献 2×1=2,其余距离各 1 点配不成对。剩下 (2,1)、(1,0)、(1,2) 与 (0,1) 对称,也各在距离 2 上落 2 点、各贡献 2。五个中心加起来 12+2+2+2+2=20,这就是回旋镖的总数。
换中心忘了清表,别人的账全算到你头上
复杂度上,外层每点当中心、内层扫一遍所有点,时间 O(n²);每个中心一张距离表最多 n 项,空间 O(n),比枚举三元组的 O(n³) 快一个量级。最阴的坑是换中心时沿用没清空的旧表,前一中心的点数会串进来、把答案抬高。把 m×(m−1) 误写成 m×(m−1)/2,讲顺序的回旋镖就漏掉一半。改用开方后的浮点距离当键,一般情形下相等判断会因精度抖动误分组。点数少于三个时任何中心都配不出一对,答案为 0;各点到中心距离两两不同、每组仅 1 点时 m×(m−1)=0,同样不贡献。
▶ 动画逐步走查(共 24 步)——想跟着动画一帧帧对照就展开
- 3口诀记牢:每个点轮流当中心,按距离把其余点分组,某距离上有 m 个点就贡献 m×(m−1)。下面每一帧都在套它。
- 4先固定第 0 个点 (1,1) 当中心(紫色)。接下来量它到另外 4 个点的距离,按距离分组记到右边的距离表里。
- 5量到第 1 个点 (0,1)(绿色),平方距离是 1。距离 1 第一次出现,登记 1 个点。
- 6量到第 2 个点 (2,1)(绿色),平方距离是 1。距离 1 又来一个,现在这条距离上累计 2 个点,它们彼此能和中心配成回旋镖。
- 7量到第 3 个点 (1,0)(绿色),平方距离是 1。距离 1 又来一个,现在这条距离上累计 3 个点,它们彼此能和中心配成回旋镖。
- 8量到第 4 个点 (1,2)(绿色),平方距离是 1。距离 1 又来一个,现在这条距离上累计 4 个点,它们彼此能和中心配成回旋镖。
- 9中心 (1,1) 量完:四个点全落在距离 1 上,m = 4。讲顺序地从这 4 个里挑两个,共 4×3 = 12 个回旋镖。答案累计到 12。
- 10换第 1 个点 (0,1) 当中心。注意每换一个中心,距离表都要清空重来,距离是相对当前中心算的。
- 11量到 (1,1),平方距离 1。距离 1 记 1 个。
- 12量到 (2,1),平方距离 4。距离 4 记 1 个。
- 13量到 (1,0),平方距离 2。距离 2 记 1 个。
- 14量到 (1,2),平方距离 2。距离 2 这下凑到 2 个点了,它们能和中心 (0,1) 组成回旋镖。
- 15中心 (0,1) 量完:只有距离 2 上落了 2 个点,贡献 2×1 = 2,别的距离各只 1 个点配不成对。答案累计到 14。
- 16第 3 个中心 (2,1),流程一样:清空距离表,逐个量距离。
- 17量到 (1,1),平方距离 1,距离 1 累计 1 个点。
- 18量到 (0,1),平方距离 4,距离 4 累计 1 个点。
- 19量到 (1,0),平方距离 2,距离 2 累计 1 个点。
- 20量到 (1,2),平方距离 2,距离 2 累计 2 个点。
- 21中心 (2,1) 同样在距离 2 上凑到 2 个点,贡献 2。答案累计到 16。
- 22第 4 个中心 (1,0) 和前面对称,过程一样,直接看分组结果:距离 1: 1 个点,距离 2: 2 个点,距离 4: 1 个点。
- 23中心 (1,0) 在距离 2 上有 2 个点,贡献 2。答案累计到 18。
- 24第 5 个中心 (1,2) 和前面对称,过程一样,直接看分组结果:距离 1: 1 个点,距离 2: 2 个点,距离 4: 1 个点。
- 25中心 (1,2) 在距离 2 上有 2 个点,贡献 2。答案累计到 20。
- 26五个点各当一次中心,把贡献全加起来:中心 (1,1) 贡献 12,另外四个各贡献 2,合计 20。这就是答案。
⚠️ 容易写错的地方
✗ 错:按 m×(m−1)/2 算(当成无序对)
✓ 对:用 m×(m−1)(有序对)
回旋镖 (i,j,k) 与 (i,k,j) 算两个,讲顺序就不除以 2
✗ 错:一般情况下用开方后的欧式浮点距离当键
✓ 对:推荐用平方距离的整数当键
浮点距离的相等判断在一般情形有精度风险,平方整数键又快又稳(本题整点坐标恰好让浮点比较也安全)
✗ 错:换中心时忘了清空距离表
✓ 对:每个中心独立建一张新表
距离是相对当前中心算的,混用会把别的中心的计数算进来
完整代码(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 numberOfBoomerangs(self, points: List[List[int]]) -> int:
ans = 0
for p1 in points:
cnt = Counter()
for p2 in points:
d = dist(p1, p2)
ans += cnt[d]
cnt[d] += 1
return ans << 1C++
#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 numberOfBoomerangs(vector<vector<int>>& points) {
int ans = 0;
for (auto& p1 : points) {
unordered_map<int, int> cnt;
for (auto& p2 : points) {
int d = (p1[0] - p2[0]) * (p1[0] - p2[0]) + (p1[1] - p2[1]) * (p1[1] - p2[1]);
ans += cnt[d];
cnt[d]++;
}
}
return ans << 1;
}
};Java
import java.util.*;
class Solution {
public int numberOfBoomerangs(int[][] points) {
int ans = 0;
for (int[] p1 : points) {
Map<Integer, Integer> cnt = new HashMap<>();
for (int[] p2 : points) {
int d = (p1[0] - p2[0]) * (p1[0] - p2[0]) + (p1[1] - p2[1]) * (p1[1] - p2[1]);
ans += cnt.getOrDefault(d, 0);
cnt.merge(d, 1, Integer::sum);
}
}
return ans << 1;
}
}复杂度
时间
O(n²)
每个点当一次中心,各自再扫一遍所有点
空间
O(n)
每个中心一张距离哈希表,最多 n 项
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 回旋镖的数量 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么固定中心点枚举,就能把 O(n³) 压到 O(n²)?+
回旋镖的两个相等条件全挂在中心 i 上:i 到 j、i 到 k 的距离相等,而 j、k 彼此无关。固定中心 i 后,问题就缩成「在到 i 等距的点里讲顺序挑两个」,用一张哈希表按距离分组、扫一遍就能数出所有合法的 j、k 搭配,省掉了对 j、k 的第三层枚举。外层换 n 个中心、内层各扫 n 个点,于是从 O(n³) 落到 O(n²)。
为什么用平方距离当键,而不是真实的欧氏距离?+
这题只需要判「两段距离相不相等」,并不需要距离本身的数值。真实欧氏距离要开平方根,得到的浮点小数在判相等时可能因舍入误差把本该相等的两段算成不等,反过来也可能误判相等。平方距离是横纵坐标差平方之和,全程整数运算,比较又快又精确,还省了一次开方,所以拿它当哈希键最稳妥。
同距离 m 个点为什么贡献 m×(m−1),不是 m×(m−1)/2?+
因为回旋镖 (i, j, k) 讲顺序,(i, j, k) 和 (i, k, j) 是两个不同的三元组。从 m 个等距点里挑 j、k 两个,若不计顺序是 C(m,2)=m×(m−1)/2 对,但每一对都能正反摆成两个回旋镖,所以要乘回 2,恰好是 m×(m−1)。写成 m×(m−1)/2 就把讲顺序的那一半漏掉,答案会正好少一半。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 回旋镖的数量 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。