1. 题目解析:3453. 分割正方形 I
这道题目要求我们在一个包含多个正方形的平面上,找到一条水平分割线y,使得这条线上方的正方形面积总和等于线下方的面积总和。题目特别说明,当多个正方形重叠时,重叠区域可以重复计算。
举个例子,假设平面上有两个正方形:
- 正方形A:左下角坐标(1,1),边长2
- 正方形B:左下角坐标(2,2),边长2
我们需要找到y=1.75这条线,使得线上和线下的面积各为2(总面积4的一半)。这个例子中,正方形A有0.25在线上,1.75在线下;正方形B完全在线上。
2. 算法原理与四种解法
2.1 解法一:浮点二分法
浮点二分是最直观的解法。我们通过不断缩小y的可能范围来逼近正确答案。
实现步骤:
- 计算所有正方形的总面积totS
- 确定二分区间:left=0,right=最上方正方形的顶边y坐标
- 进行47次二分迭代(根据误差要求计算得出)
- 每次迭代计算中点mid,并判断线下面积是否小于线上面积
- 根据判断结果调整left或right的值
关键点解析:
- 为什么是47次迭代?因为题目要求误差在10^-5以内,通过公式计算得出需要至少47次迭代才能保证精度
- 线下面积计算:对于每个正方形,如果y穿过它,则计算穿过的部分面积;否则计算整个正方形面积
注意:浮点二分可能遇到精度问题,特别是在面积接近相等时可能出现死循环,因此需要严格控制迭代次数。
2.2 解法二:放大整型二分法
为了规避浮点数精度问题,我们可以将所有数值放大10^5倍,用整数运算代替浮点运算。
优化思路:
- 定义放大倍数M=10^5
- 将所有y坐标和边长都乘以M转换为整数
- 使用整数二分查找(最左端点模型)
- 找到结果后再除以M还原为浮点数
优势分析:
- 完全避免了浮点数运算带来的精度问题
- 整数运算速度通常比浮点运算快
- 可以使用标准的整数二分模板,代码更简洁
常见误区:
- 忘记在判断函数中也进行放大处理
- 放大后没有统一所有计算,导致部分计算仍使用原始值
- 最后还原时忘记除以放大倍数
2.3 解法三:补差值整型二分法
这是一种更高效的整数二分方法,先找到整数解,再通过线性关系计算小数部分。
实现原理:
- 先用整数二分找到临界y值
- 计算y-1和y处的线下面积
- 通过线性插值计算需要回退的小数值
- 最终结果 = y - (多余面积 / 单位高度面积增量)
为什么能这样计算?
因为在y-1到y之间,线下面积是线性变化的。我们可以通过面积差和高度差计算出精确的小数偏移量。
实际案例:
假设:
- y=2时线下面积=6
- y=1时线下面积=4
- 总面积=8
那么:
多余面积=26-8=4
单位高度面积增量=6-4=2
需要回退的小数值=4/(22)=1
最终结果=2-1=1
2.4 解法四:扫描线差分法
这是一种基于事件处理的算法,模拟一条从下往上的扫描线。
核心思想:
- 使用TreeMap记录所有正方形的进入和退出事件
- 扫描线从下往上移动,遇到事件点时处理
- 维护当前扫描线以下的矩形底边总长度sumL
- 计算相邻事件点之间的面积增量
- 当累计面积超过一半总面积时,计算精确的y值
为什么用TreeMap?
- 需要按键(y坐标)自动排序
- 方便按顺序处理事件点
- 查找和插入操作的时间复杂度为O(logN)
差分处理技巧:
- 遇到正方形底边:sumL += 边长
- 遇到正方形顶边:sumL -= 边长
- 面积增量 = sumL × 高度差
3. Java代码实现详解
3.1 浮点二分法实现
java复制class Solution {
public double separateSquares(int[][] squares) {
long totS = 0;
int maxY = 0;
for(int[] sq : squares){
int l = sq[2];
totS += (long)l * l;
maxY = Math.max(maxY, sq[1] + l);
}
double left = 0, right = maxY;
for(int i = 0; i < 47; i++){
double mid = left + (right - left) / 2;
if(check(squares, mid, totS))
left = mid;
else
right = mid;
}
return left;
}
private boolean check(int[][] squares, double y, long totS) {
double area = 0;
for(int[] sq : squares){
double yi = sq[1];
if(yi < y){
int l = sq[2];
double h = Math.min(y - yi, l);
area += l * h;
}
}
return area < totS - area;
}
}
3.2 放大整型二分法实现
java复制class Solution {
private static final int M = 100_000;
public double separateSquares(int[][] squares) {
long totS = 0;
int maxY = 0;
for(int[] sq : squares){
int l = sq[2];
totS += (long)l * l;
maxY = Math.max(maxY, sq[1] + l);
}
long left = 0, right = (long)maxY * M;
while(left < right){
long mid = left + (right - left) / 2;
if(check(squares, mid, totS))
left = mid + 1;
else
right = mid;
}
return (double)right / M;
}
private boolean check(int[][] squares, long y, double totS) {
long area = 0;
for(int[] sq : squares){
long yi = sq[1];
if(yi * M < y){
long l = sq[2];
area += l * Math.min(y - yi * M, l * M);
}
}
return area < totS * M - area;
}
}
3.3 补差值整型二分法实现
java复制class Solution {
public double separateSquares(int[][] squares) {
long totS = 0;
int maxY = 0;
for(int[] sq : squares){
int l = sq[2];
totS += (long)l * l;
maxY = Math.max(maxY, sq[1] + l);
}
int left = 0, right = maxY;
while(left < right){
int mid = left + (right - left) / 2;
if(2 * calc(squares, mid) < totS)
left = mid + 1;
else
right = mid;
}
int y = left;
long areaY = calc(squares, y);
long sumL = areaY - calc(squares, y - 1);
return y - (2 * areaY - totS) / (sumL * 2.0);
}
private long calc(int[][] squares, long y) {
long area = 0;
for(int[] sq : squares){
long yi = sq[1];
if(yi < y){
long l = sq[2];
area += (long)l * Math.min(y - yi, l);
}
}
return area;
}
}
3.4 扫描线差分法实现
java复制class Solution {
public double separateSquares(int[][] squares) {
long totS = 0;
TreeMap<Integer, Long> diff = new TreeMap<>();
for(int[] sq : squares){
int y = sq[1];
long l = sq[2];
totS += l * l;
diff.merge(y, l, Long::sum);
diff.merge(y + (int)l, -l, Long::sum);
}
long area = 0, sumL = 0;
int preY = 0;
for(var e : diff.entrySet()){
int y = e.getKey();
area += sumL * (y - preY);
if(area >= totS - area)
return y - (2 * area - totS) / (sumL * 2.0);
preY = y;
sumL += e.getValue();
}
return -1;
}
}
4. 性能对比与选择建议
4.1 四种解法性能对比
| 解法 | 时间复杂度 | 运行时间 | 击败比例 | 适用场景 |
|---|---|---|---|---|
| 浮点二分 | O(NlogN) | 166ms | 35.48% | 精度要求不高,简单实现 |
| 放大整型二分 | O(NlogN) | 81ms | 97.70% | 需要高精度,避免浮点误差 |
| 补差值整型二分 | O(NlogN) | 50ms | 99.08% | 最优性能,适合大规模数据 |
| 扫描线差分 | O(NlogN) | 169ms | 31.80% | 需要处理复杂重叠情况 |
4.2 选择建议
-
面试场景:推荐使用放大整型二分法,因为它:
- 避免了浮点数精度问题
- 代码相对简单
- 性能优秀
- 易于解释和证明正确性
-
竞赛场景:优先考虑补差值整型二分法,因为:
- 运行速度最快
- 能处理大规模数据
- 虽然实现稍复杂,但性能优势明显
-
学习场景:建议从浮点二分法开始:
- 最直观易懂
- 便于理解问题本质
- 可以作为其他优化解法的基础
实际工程中如果遇到类似问题,扫描线差分法可能更具扩展性,特别是当正方形数量很大或需要处理更复杂的几何关系时。
5. 常见问题与调试技巧
5.1 调试常见问题
-
死循环问题:
- 浮点二分可能因精度问题导致无限循环
- 解决方案:严格限制迭代次数(如47次)
-
整数溢出问题:
- 放大整型二分时可能发生溢出
- 解决方案:使用long类型,检查中间计算结果
-
面积计算错误:
- 忘记处理正方形完全在线下或完全在线上情况
- 解决方案:仔细检查面积计算逻辑,添加测试用例
5.2 测试用例设计建议
好的测试用例应该包含:
- 单个正方形的情况
- 多个不重叠正方形
- 多个重叠正方形
- 正方形排列成特殊形状(如阶梯状)
- 边界情况(如y=0或最大y值)
示例测试用例:
java复制@Test
public void testSeparateSquares() {
Solution solution = new Solution();
// 单个正方形
int[][] case1 = {{0,0,2}};
assertEquals(1.0, solution.separateSquares(case1), 1e-5);
// 两个不重叠正方形
int[][] case2 = {{0,0,1}, {0,1,1}};
assertEquals(1.0, solution.separateSquares(case2), 1e-5);
// 两个重叠正方形
int[][] case3 = {{0,0,2}, {1,1,2}};
assertEquals(1.75, solution.separateSquares(case3), 1e-5);
}
5.3 性能优化技巧
-
预处理数据:
- 提前计算并缓存每个正方形的顶边y坐标
- 对正方形按y坐标排序,可以加速某些算法的处理
-
并行计算:
- 在计算线下面积时,可以并行处理各个正方形
- 特别适合大规模数据场景
-
算法选择:
- 根据数据特征选择算法
- 例如,如果正方形分布稀疏,扫描线差分法可能更高效
6. 算法扩展与应用
6.1 类似问题
- 分割矩形问题:将题目中的正方形改为矩形,解法依然适用
- 三维空间分割:在三维空间中寻找分割平面,原理类似但实现更复杂
- 加权面积分割:每个形状有不同权重,求加权面积相等的分割线
6.2 实际应用场景
- 图像处理:分割图像区域,保持两侧"信息量"相等
- 城市规划:划分区域使两侧"价值"相当
- 资源分配:将资源公平分配到不同区域
6.3 进阶学习建议
- 深入学习计算几何:了解扫描线算法、平面扫描技术等
- 研究数值计算:特别是浮点数精度处理和数值稳定性
- 练习相关LeetCode题目:
-
- The Skyline Problem
-
- Rectangle Area II
-
- Minimum Area Rectangle
-