A.每日一题——3625. 统计梯形的数目 II

题目链接:3625. 统计梯形的数目 II(困难)

算法原理:

👉对应力扣题解

击败64.15%

一、算法核心原理

1. 梯形的数学定义

梯形是「有且仅有一组对边平行且不共线」的四边形;而平行四边形是「有两组对边平行」的特殊四边形(会被误统计为梯形)。因此核心逻辑:纯梯形数量 = 有一组平行对边的四边形总数(含平行四边形) - 平行四边形数量

2. 关键几何特征

  • 平行对边:两条边所在直线「斜率相同、截距不同」(平行且不共线);
  • 平行四边形:一组对边「斜率相同 + 中点相同」(平行且中点重合,必然长度相等)。

二、整体解题思路

  1. 统计候选四边形(含平行四边形):遍历所有两点组成的边,按「斜率 k→截距 b」分组,统计同一斜率下不同直线的边数组合数(从不同直线各选 1 条边,构成一组平行对边);
  2. 统计平行四边形数量:遍历所有边,按「中点 mid→斜率 k」分组,统计同一中点 + 同一斜率的边数组合数(选 2 条边构成平行四边形的一组对边);
  3. 计算纯梯形:用候选四边形总数减去平行四边形数量,得到最终结果。

三、分步解法(对应代码执行流程)

步骤 1:预处理 - 遍历所有边,构建两个哈希表

遍历所有两两组合的边(j < i 避免重复统计同一条边),对每条边计算 3 个关键信息:斜率k、截距b、中点压缩值mid,并更新两个哈希表:

  • 哈希表 hash1Map<Double, Map<Double, Integer>>key1 = 斜率 k,key2 = 截距 b,value = 该直线(k+b)上的边数;作用:区分「平行且不共线」的直线(同 k 不同 b)。
  • 哈希表 hash2Map<Integer, Map<Double, Integer>>key1 = 中点压缩值 mid,key2 = 斜率 k,value = 该中点 + 该斜率的边数;作用:识别平行四边形的核心特征(同 mid + 同 k)。
关键计算细节:
  • 斜率 / 截距:非垂直边用k=dy/dxb=(y1*dx - x1*dy)/dx计算,垂直边用Double.MAX_VALUE标记斜率,x 坐标标记截距;
  • 中点压缩:将中点坐标的 2 倍和(midXSum=x1+x2midYSum=y1+y2)平移(+2000 避负)后压缩为 int(mid=(midXSum+2000)*10000 + (midYSum+2000)),避免浮点数精度问题;
  • 精度修正:将-0.0统一为0.0,避免 double 类型的 key 冲突。

步骤 2:统计「有一组平行对边的四边形总数」

遍历 hash1 的每个斜率 k(所有平行直线组):

  • 对每个斜率下的所有直线(不同截距 b),用累加和计算组合数:sum * edge(sum 是之前所有直线的边数和,edge 是当前直线的边数);
  • 组合数含义:从之前的直线选 1 条边 + 当前直线选 1 条边,构成一组平行对边,对应 1 个候选四边形;
  • 累加所有组合数,得到候选总数。

步骤 3:统计「平行四边形数量」并扣除

遍历 hash2 的每个中点 mid(平行四边形的中点特征):

  • 对每个中点下的所有斜率 k,同样用累加和计算组合数:sum * edge
  • 组合数含义:同一中点 + 同一斜率的边选 2 条,构成平行四边形的一组对边,对应 1 个平行四边形;
  • 从候选总数中扣除该组合数(平行四边形被候选统计了 2 次,需扣 1 次)。

步骤 4:返回结果

扣除后的数值即为「纯梯形数量」。

四、关键技巧与注意事项

  1. 避免重复统计边 :遍历边时j < i,确保每条边只统计 1 次;
  2. 浮点数精度处理 :中点压缩为 int、-0.0归一化,减少精度损失(斜率用 Double 仍有潜在风险,可优化为 "约分分数" 标识斜率);
  3. 组合数高效计算 :用累加和sum替代公式C(n,2)=n*(n-1)/2,遍历一次即可统计所有两两组合,时间复杂度更低;
  4. 垂直边特殊处理 :用Double.MAX_VALUE标记垂直边斜率,避免斜率不存在的逻辑漏洞。

五、算法复杂度

  • 时间复杂度:O (n²),n 为点的数量(核心是遍历所有两两边,次数为 n*(n-1)/2,后续哈希表遍历为 O (n²) 级别);
  • 空间复杂度:O (n²),哈希表存储所有边的特征信息,最坏情况下每条边对应唯一的 k/b/mid。

总结

该算法的核心是「转化问题」:将 "统计梯形" 转化为 "统计平行对边组合 - 扣除平行四边形",利用哈希表分组统计几何特征(斜率、截距、中点),结合组合数计算完成统计,是 "几何特征抽象 + 哈希表计数" 的典型应用。

Java代码:

java 复制代码
class Solution {
    public int countTrapezoids(int[][] points) {
        //hash1[k][b]=斜率为k,截距为b的直线上,两点组成的边数
        //作用:同一斜率k->直线平行;不同斜率b->直线不共线
        //从不同(b1,b2)的直线各选一条边,就能组成一组平行对边,构成候选四边形
        Map<Double,Map<Double,Integer>> hash1=new HashMap<>();
        //hash2[mid][k]=中点为mid、斜率为k的边的总数
        //作用:平行四边形的关键特征是一组对边平行且中点相同、长度相等
        //同一mid+同一k下的边,选两条就是平行四边形的一组对边,用于统计平行四边形总数
        //mid:将两点中点坐标(x+y)压缩成int,避免浮点数精度问题(+2000是防止变成负数)
        Map<Integer,Map<Double,Integer>> hash2=new HashMap<>();
        int n=points.length;//点的个数
        //遍历所有两点组成的边(j<i是防止重复统计同一条边)
        for(int i=0;i<n;i++){
            int x1=points[i][0],y1=points[i][1];//第1个点坐标
            for(int j=0;j<i;j++){
                int x2=points[j][0],y2=points[j][1];//第2个点坐标
                //计算两点斜率和截距(y=kx+b)
                int dy=y1-y2;
                int dx=x1-x2;
                double k;
                double b;
                if(dx!=0){//斜率存在
                    k=1.0*dy/dx;//1.0强制浮点数计算
                    //代入直线任意一点(x1,y1)通过y=kx+b计算b,通分避免精度损失
                    b=1.0*(y1*dx-x1*dy)/dx;
                }else{
                    k=Double.MAX_VALUE;//用最大值标记垂直边(唯一标识)
                    b=x1;//垂直边方程是x=常数,用x坐标作为截距标识
                }
                //修正double精度问题:-0.0和0.0逻辑上是同一个值,统一为0.0(避免作为不同key)
                if(k==-0.0) k=0.0;
                if(b==-0.0) b=0.0;
                // 更新hash1:按(斜率k → 截距b)分组,统计每条直线上的边数
                // computeIfAbsent:k不存在则创建新的HashMap,存在则直接获取
                // merge:b对应的计数+1(不存在则设为1)
                //Integer::sum引用Integer类中已存在的静态方法sum,::是引用的意思
                //Integer::new引用Integer(int value)这个构造器
                //HashMap::new引用HashMap()这个无参构造器
                //_是lambda表达式中的占位符,意思是这个参数用不上,但是语法格式必须写
                hash1.computeIfAbsent(k,_->new HashMap<>()).merge(b,1,Integer::sum);
                // 计算边(i,j)的中点坐标,并压缩为int(避免浮点数中点的精度问题)
                // 中点横坐标:(x1+x2)/2,纵坐标:(y1+y2)/2 → 用(x1+x2)和(y1+y2)替代(乘以2不影响中点是否相同)
                // 加2000是为了避免x1+x2或y1+y2为负数(题述坐标范围可能是负数,最小是-2000,
                //因为负数相除可能会和正数冲突),导致压缩后int冲突
                int midXSum = x1 + x2;
                int midYSum = y1 + y2;
                // 压缩为单个int(10000是足够大的系数,避免冲突)
                //因为midXSum最大是4000,起码要×10000才能保证"横和部分"和"纵和部分"在int中不重叠,否则比如
                //当×2000时:a=2,×2000+b=1000=a=1,×2000+b=3000,应为两个结果,但计算结果却相同,导致以为是一个结果
                int mid = (midXSum + 2000) * 10000 + (midYSum + 2000); 
                // 更新哈希表2:按(中点mid → 斜率k)分组,统计该组合下的边数
                hash2.computeIfAbsent(mid, _ -> new HashMap<>()).merge(k, 1, Integer::sum);
            }
        }
        int ret=0;
        //第一步:统计所有一组对边平行且不共线的四边形(包含平行四边形)
        //遍历每个斜率k(所有平行直线组)
        for(Map<Double,Integer> line:hash1.values()){
            int sum=0;//累加当前斜率下,之前所有截距对应的边数
            //遍历该斜率下的所有直线(不同截距b,平行且不共线)
            for(int edge:line.values()){
                //组合数:之前遍历过的边数×当前边数,从之前的直线和当前直线各选一条边,组成平行边
                ret+=sum*edge;
                sum+=edge;//累加当前直线的边数,用于后续组合
            }
        }
        //第二步:减去平行四边形的数量(平行四边形有两组平行对边,被第一步统计了两次,需要减一次)
        //遍历每个中点mid(平行四边形对角线中点相同->两平行边中点相同)
        for(Map<Double,Integer> slope:hash2.values()){
            int sum=0;//累加当前中点下,之前所有斜率对应的边数
            //遍历该中点下的所有斜率k
            for(int edge:slope.values()){
                //组合数:之前遍历过的边数×当前边数->同一中点+同一斜率的边,选两条构成平行四边形的对边
                ret-=sum*edge;
                sum+=edge;//累加当前直线的边数,用于后续组合
            }
        }
        //最终数量:纯梯形数量
        return ret;
    }
}
相关推荐
Dolphin_Home38 分钟前
接口字段入参出参分离技巧:从注解到DTO分层实践
java·spring boot·后端
松涛和鸣40 分钟前
24、数据结构核心:队列与栈的原理、实现与应用
c语言·开发语言·数据结构·学习·算法
卿雪40 分钟前
MySQL【存储引擎】:InnoDB、MyISAM、Memory...
java·数据库·python·sql·mysql·golang
即随本心0.o44 分钟前
大模型springai,Rag,redis-stack向量数据库存储
java·数据库·redis
豐儀麟阁贵44 分钟前
9.1String类
java·开发语言·算法
okseekw1 小时前
Java内部类实战指南:4种类型+5个经典场景,开发效率直接拉满!
java·后端
wjm0410061 小时前
秋招ios面试 -- 真题篇(三)
ios·面试·职场和发展
三炭先生1 小时前
计算机视觉算法--第一章:概述
人工智能·算法·计算机视觉
嘟嘟w1 小时前
POST和GET的区别
java