力扣--3625. 统计梯形的数目 II 代码解析(Java,详解附注释附图)

前言:很久没有更新博客了,大概在4月份就没有更新了,那时候忙着实习,后面忙着比赛,还要处理班级事务(班长),没有太多时间更新,就断更了,现在会慢慢更新的,拾起前面断更的内容,同时也会写一下力扣周赛比较难的题,或者蓝桥杯难的算法题!谢谢大家支持!

题目:

给你一个二维整数数组 points,其中 points[i] = [xi, yi] 表示第 i 个点在笛卡尔平面上的坐标。

返回可以从 points 中任意选择四个不同点组成的梯形的数量。

梯形 是一种凸四边形,具有 至少一对平行边。两条直线平行当且仅当它们的斜率相同。

示例 1:

输入: points = [[-3,2],[3,0],[2,3],[3,2],[2,-3]]

输出: 2

解释:

有两种不同方式选择四个点组成一个梯形:

  • [-3,2], [2,3], [3,2], [2,-3] 组成一个梯形。
  • [2,3], [3,2], [3,0], [2,-3] 组成另一个梯形。

示例 2:

输入: points = [[0,0],[1,0],[0,1],[2,1]]

输出: 1

解释:

只有一种方式可以组成一个梯形。

提示:

  • 4 <= points.length <= 500
  • --1000 <= xi, yi <= 1000
  • 所有点两两不同。

3623. 统计梯形的数目 I不同,但是想法差不多。

题目分析:

第一步:这个题和3623. 统计梯形的数目 I不一样,但是首先肯定可以想到一个斜率,按斜率来分类线段,但是这里有一个问题,就是可能会选同一个斜率,但是共线,这显然不能构成梯形,所有再次引入一个截距b,就是与y轴的交点,这样可以排除同斜率共线的情况,所以定义哈希表时,需要再嵌套一个哈希表,把斜率k,截距b,和这样的线段个数,记录一下。

第二步:线段我们找出来了,然后算总数还是和3623. 统计梯形的数目 I一样的,这里可以参考 灵神(灵茶山艾府)的 枚举右,维护左 的思想,但是这里有一个问题,因为我们按斜率和截距来统计的,所以如果构成的是平行四边形,是计算了两次,因为有两条平行线,所以如果是平行四边形,数量就得减1,但是再判断是不是平行四边形,这里又需要想一下,用平行四边形的特征,两对角线的中点重合,即mid,然后是同一个斜率k,再求个数,所有这里还需要定义哈希表,需要再嵌套一个哈希表,把斜mid,截距k,和这样个数。记录一下。

题目分析完了,就来看代码怎么写。

代码(Java)附注释:

class Solution {

public int countTrapezoids(int[][] points) {

Map<Double,Map<Double,Integer>> cnt = new HashMap<>();

Map<Integer,Map<Double,Integer>> cnt2 = new HashMap<>();

int n = points.length;

for(int i = 0;i<n;i++){

int x = points[i][0],y = points[i][1];

for(int j = 0;j<i;j++){

int x2 = points[j][0],y2 = points[j][1];

int dy = y - y2;

int dx = x - x2;

double k = dx != 0 ? 1.0 * dy / dx : Double.MAX_VALUE;

double b = dx != 0 ? 1.0 * (y*dx - dy*x)/dx : x;

if(k == -0.0){

k = 0.0;

}

if(b == -0.0){

b = 0.0;

}

cnt.computeIfAbsent(k,_ -> new HashMap<>()).merge(b,1,Integer::sum);

int mid = (x + x2 + 2000) * 10000 + (y + y2 + 2000);

cnt2.computeIfAbsent(mid,_ -> new HashMap<>()).merge(k,1,Integer::sum);

}

}

int ans = 0;

for(Map<Double,Integer> m : cnt.values()){

int s = 0;

for(int c : m.values()){

ans += s * c;

s += c;

}

}

for(Map<Double,Integer> m :cnt2.values()){

int s = 0;

for(int c : m.values()){

ans -= s * c;

s += c;

}

}

return ans;

}

}

解析(附图片):

1、哈希表定义

//定义哈希表cnt 斜率 -> 截距 -> 个数

Map<Double,Map<Double,Integer>> cnt = new HashMap<>();

//定义哈希表cnt2 中点 -> 斜率 -> 个数

Map<Integer,Map<Double,Integer>> cnt2 = new HashMap<>();

2、统计斜率和截距,中点和对应的个数

int n = points.length;//数组长度

for(int i = 0;i<n;i++){

int x = points[i][0],y = points[i][1];//点1

for(int j = 0;j<i;j++){

int x2 = points[j][0],y2 = points[j][1];//点2

int dy = y - y2;

int dx = x - x2;

//斜率,三目运算符,不说,dx等于0,就是垂直x轴,这个时候斜率是无穷大,

//其余情况就是两点式计算斜率,此时截距就以x为标识

double k = dx != 0 ? 1.0 * dy / dx : Double.MAX_VALUE;

double b = dx != 0 ? 1.0 * (y*dx - dy*x)/dx : x;

// 归一化 把- 0.0 转换为 0.0

//这里是因为哈希表会把这两个作为不同的值记录

//但实则是同一个,只是点 谁前谁后的问题

if(k == -0.0){

k = 0.0;

}

if(b == -0.0){

b = 0.0;

}

//存值

cnt.computeIfAbsent(k,_ -> new HashMap<>()).merge(b,1,Integer::sum);

//中点计算

int mid = (x + x2 + 2000) * 10000 + (y + y2 + 2000);

//存值

cnt2.computeIfAbsent(mid,_ -> new HashMap<>()).merge(k,1,Integer::sum);

}

}

3、哈希表存值这里细说一下

两个存值的过程是Java 8+ 中非常简洁高效 的写法,用于在嵌套 Map(即 Map<K, Map<V, Integer>>)中安全地累加计数。

第一步:cnt.computeIfAbsent(k, _ -> new HashMap<>())
  • 如果 cnt不存在 key k ,就执行 new HashMap<>(),并把结果存入 cnt[k]
  • 如果已存在 ,就直接返回 cnt.get(k)
  • 返回值是一个 Map<Double, Integer>(即内层 map)

✅ 这避免了手动判断 if (!cnt.containsKey(k)) cnt.put(k, new HashMap<>());

第二步:.merge(b, 1, Integer::sum)

对上一步返回的内层 map 执行:

  • 如果 b 不存在 ,就插入 b → 1
  • 如果 b 已存在 ,就执行 oldValue + 1(因为 Integer::sum 等价于 (a, b) -> a + b

✅ 这等价于:map.put(b, map.getOrDefault(b, 0) + 1);,但更函数式、更安全

为什么加 2000?

题目中坐标范围是:

−1000≤x,y≤1000

那么:

  • x + x2 的范围是:-20002000
  • y + y2 同理

但数组下标或哈希 key 不能为负数,所以我们加上偏移量 2000,将其映射到非负区间:

原始值 加 2000 后
-2000 0
0 2000
2000 4000

现在 sumX + 2000 ∈ [0, 4000],共 4001 种可能

为什么乘以 10000?

我们要把二维坐标 (a, b) 压缩成一个整数,且保证不冲突(唯一性)

常用方法是:

key=a×M+b

其中 M 必须 大于 b 的最大可能值

这里:

  • a = x + x2 + 2000 ∈ [0, 4000]
  • b = y + y2 + 2000 ∈ [0, 4000]

所以只要 M > 4000,就不会冲突。

我选了 M = 10000(足够大,且是整万,便于阅读)。

⚠️ 注意:这不是真正的"中点",而是"两倍中点"的偏移编码

//存值

cnt.computeIfAbsent(k,_ -> new HashMap<>()).merge(b,1,Integer::sum);

//中点计算

int mid = (x + x2 + 2000) * 10000 + (y + y2 + 2000);

//存值

cnt2.computeIfAbsent(mid,_ -> new HashMap<>()).merge(k,1,Integer::sum);

4、计数逻辑详解

第一步:统计所有"斜率相同"的线段对

  • 对每个斜率 k,遍历其所有截距组(即不同直线)。
  • s 是之前所有直线上的线段总数。
  • s * c 表示:当前直线上的 c 条线段,与之前所有非共线(不同 b)的线段配对
  • ✅ 这正好统计了所有不共线的平行线段对

为什么?因为共线的线段在同一 (k,b) 组内,而这里 c 是按 b 分开的,所以 sc 来自不同直线 → 不共线 → 能构成梯形!

🎉 所以这一步的结果就是:所有梯形数量(包括平行四边形)


第二步:减去多算的平行四边形

  • 对每个中点 mid,遍历其所有斜率组。
  • s * c 表示:中点相同、斜率相同的线段对数量
  • 而"中点相同 + 平行" ⇨ 这两条线段是对边,构成平行四边形

但平行四边形有两对平行边 ,所以在第一步中被算了 2 次

而我们只需要算 1 次 ,所以要 减去 1 次

因此,ans -= 平行四边形数量,最终结果正确。

再 return 返回结果。

int ans = 0;

for(Map<Double,Integer> m : cnt.values()){

int s = 0;

for(int c : m.values()){

ans += s * c;

s += c;

}

}

for(Map<Double,Integer> m :cnt2.values()){

int s = 0;

for(int c : m.values()){

ans -= s * c;

s += c;

}

}

结语:这是力扣周赛一道有难度的算法题,可能没有思路或者没有想清楚,当时有部分人没有写出来或答案不对,思考出现偏差,希望看完之后可以解决你的困惑!一什么问题可以私信或者评论讨论丶,同时有问题或者改进的欢迎指出!

相关推荐
练习时长一年1 小时前
LeetCode热题100(岛屿数量)
算法·leetcode·职场和发展
无限进步_1 小时前
基于单向链表的C语言通讯录实现分析
c语言·开发语言·数据结构·c++·算法·链表·visual studio
老鱼说AI1 小时前
算法初级教学第四步:栈与队列
网络·数据结构·python·算法·链表
FMRbpm1 小时前
栈练习--------有效的括号(LeetCode 20)
数据结构·c++·leetcode·新手入门
ReinaXue1 小时前
快速认识图像生成算法:VAE、GAN 和 Diffusion Models
图像处理·人工智能·神经网络·算法·生成对抗网络·计算机视觉·语言模型
再睡一夏就好1 小时前
进程调度毫秒之争:详解Linux O(1)调度与进程切换
linux·运维·服务器·c++·算法·哈希算法
无限进步_1 小时前
C语言双向循环链表实现详解:哨兵位与循环结构
c语言·开发语言·数据结构·c++·后端·算法·链表
wljun7391 小时前
五、OrcaSlicer 切片
算法·切片软件 orcaslicer
罗湖老棍子2 小时前
宠物小精灵之收服(信息学奥赛一本通- P1292)
算法·动态规划·01背包