题目信息
- 平台:LeetCode
- 题目:3625. 统计梯形的数目 II
- 难度:Hard
- 题目链接:3625. Count Number of Trapezoids II
题目描述
给定一个二维整数数组
points,其中points[i] = [xi, yi]表示第i个点在笛卡尔平面上的坐标。你需要从points中任意选择四个互不相同的点,计算可以组成的梯形的数量。梯形是一种至少有一对平行边的凸四边形。所有点都是两两不同的。
初步思路
- 枚举所有线段 : 遍历所有可能的点对
(p1, p2),将它们视为线段。 - 计算斜率和截距 : 对于每条线段,计算其斜率
k和截距b。- 需要特殊处理垂直线(斜率无穷大)的情况,例如用
inf或一个足够大的值表示。 - 截距的计算也要考虑垂直线的情况。
- 需要特殊处理垂直线(斜率无穷大)的情况,例如用
- 统计平行线对 :
- 使用一个
defaultdict(lambda: defaultdict(int))来存储斜率 -> 截距 -> 该截距上的线段数量。 - 对于每个斜率
k,遍历其对应的截距b集合。如果某个截距b上有c条线段,那么从这些线段中选择两条可以构成一个梯形的一条边。 - 通过组合数
c * (c - 1) / 2统计形成平行线的组合数。
- 使用一个
- 处理平行四边形重复计数 :
- 梯形定义为"至少一对平行边",这意味着平行四边形(有两对平行边)会被计算两次。
- 通过计算线段中点来识别平行四边形。如果两条线段具有相同的中点,且它们不共线,则它们构成一个平行四边形。
- 使用另一个
defaultdict(lambda: defaultdict(int))来存储中点坐标 -> 斜率 -> 经过该中点且具有该斜率的线段数量。 - 对于每个中点,遍历其对应的斜率集合。如果某个中点处,某个斜率
k上有c条线段,那么从中选择两条可以构成一个平行四边形。 - 从之前统计的梯形总数中减去平行四边形的数量(因为平行四边形被算了两次)。
算法分析
- 核心思想: 通过计算线段的斜率和截距来统计平行线对,然后通过计算线段中点来消除平行四边形的重复计数。
- 时间复杂度 :
- 枚举所有线段并计算斜率、截距、中点:O(N^2),其中 N 是点的数量。
- 统计平行线对和中点信息:O(N^2)。
- 最后遍历
cnt和cnt2字典:在最坏情况下,每个斜率或中点可能对应 O(N) 条线段,所以也是 O(N^2)。 - 总时间复杂度为 O(N^2)。
- 空间复杂度: O(N^2),主要用于存储斜率-截距和中点-斜率的字典。
- 技巧 :
- 使用
defaultdict简化计数逻辑。 - 处理浮点数精度问题:在比较斜率时,如果直接用浮点数可能导致精度问题。这里通过
dx,dy的比值来表示斜率,并使用inf处理垂直线,可以避免部分精度问题。但在存储b值时,仍可能存在浮点数精度问题,实际实现中可能需要考虑将斜率和截距表示为分数形式或通过gcd简化。该方案中,由于截距的计算涉及到浮点数,因此在Python中直接使用浮点数作为字典键可能会有精度风险。 - 将中点坐标表示为元组
(x+x2, y+y2)作为字典键。
- 使用
代码实现(Python)- 方案一:嵌套 defaultdict
python
from collections import defaultdict
from math import inf
from typing import List
class Solution:
def countTrapezoids(self, points: List[List[int]]) -> int:
cnt = defaultdict(lambda:defaultdict(int))#斜率->截距->个数
cnt2 = defaultdict(lambda:defaultdict(int)) #中点->斜率->个数
for i,(x,y) in enumerate(points):
for x2,y2 in points[:i]:
dy = y - y2
dx = x - x2
k = dy/dx if dx != 0 else inf
b = (y*dx - x*dy)/dx if dx != 0 else x # 截距
cnt[k][b] += 1
cnt2[(x+x2,y+y2)][k] += 1
ans = 0
# 统计所有平行线对形成的梯形 (包括平行四边形被计算两次)
for m in cnt.values():
s = 0 # 累计之前线段的数量
for c in m.values():
ans += c*s # 当前截距上的c条线段与之前s条线段组成梯形
s += c
# 减去平行四边形的重复计数
for m in cnt2.values():
s = 0 # 累计之前经过相同中点但不同斜率的线段数量
for c in m.values():
ans -= s*c # 当前斜率的c条线段与之前s条线段组成平行四边形
s+=c
return ans
代码实现(Python)- 方案二:使用 Counter
python
from collections import defaultdict, Counter
from math import inf
from typing import List
class Solution:
def countTrapezoids(self, points: List[List[int]]) -> int:
groups = defaultdict(list)
group2 = defaultdict(list)
for i,(x,y) in enumerate(points):
for x2,y2 in points[:i]:
dy = y - y2
dx = x - x2
k = dy/dx if dx != 0 else inf
b = (y*dx - x*dy)/dx if dx != 0 else x
groups[k].append(b)
group2[(x+x2,y+y2)].append(k)
ans = 0
for g in groups.values():
if len(g) == 1:
continue
s = 0
for c in Counter(g).values():
ans += c*s
s += c
for g in group2.values():
if len(g) == 1:
continue
s = 0
for c in Counter(g).values():
ans -= s*c
s += c
return ans
总结与反思
- 几何问题转换为代数计数: 将二维几何图形的计数问题巧妙地转化为线段斜率、截距和中点的计数问题。
- 避免浮点数精度问题 : 优先使用整数运算或分数表示来处理斜率和截距,以避免浮点数比较的精度问题。虽然示例代码直接使用了浮点数,但在实际竞赛中,通常建议将斜率表示为
(dy, dx)的元组,并通过gcd简化,或者将截距也进行类似处理。 - 容斥原理: 通过"统计所有平行线对形成的梯形(包括重复计算的平行四边形)"再"减去平行四边形的重复计数"来得到最终结果,这是容斥原理的体现。
- 中点性质: 平行四边形的对角线互相平分,即中点重合。利用这个性质来识别平行四边形是关键。
- Python
defaultdict与Counter的妙用 : 两种方案都展示了 Pythoncollections模块的强大功能。方案一使用嵌套的defaultdict直接进行计数,逻辑清晰;方案二先收集所有截距/斜率,再用Counter统一计数,是另一种有效的思路。