1. 扫描线算法与线段树实战:正方形面积分割问题解析
最近在解决一个有趣的算法问题:给定平面上若干个正方形,需要找到一条平行于x轴的分割线,使得这条线将正方形总面积尽可能均等地分成上下两部分。这个问题看似简单,但实现起来涉及多个经典算法技巧的组合运用。下面我将分享这个问题的完整解决思路和优化过程。
2. 问题分析与算法选型
2.1 问题重述与核心挑战
我们需要处理的是这样的场景:给定N个正方形(每个正方形由左下角坐标(x,y)和边长l确定),要求找到一条水平线y=Y,使得这条线之上的正方形面积之和与线之下的面积之和尽可能接近。
这个问题的难点在于:
- 正方形之间可能存在大量重叠区域
- 需要高效计算任意水平线对应的上下面积
- 要求算法在时间复杂度上能够处理大规模输入
2.2 扫描线算法的适用性分析
扫描线算法是解决这类平面几何问题的利器。其核心思想是:
- 想象一条平行于x轴的直线从下往上扫描整个平面
- 在移动过程中维护当前线与正方形相交的状态
- 在关键事件点(正方形开始/结束的位置)更新状态
对于面积计算,我们可以:
- 预处理所有正方形的上下边界作为事件点
- 按y坐标排序这些事件点
- 在相邻事件点之间,面积增量 = (y2-y1) × 当前被覆盖的x轴总长度
2.3 线段树的角色与优化
为了高效维护x轴上的覆盖情况,我们使用线段树:
- 将x坐标离散化处理
- 线段树节点维护区间覆盖次数和有效长度
- 支持区间更新(正方形左右边界的加入/移除)
- 能够快速查询当前被覆盖的总长度
线段树的优势在于:
- 离散化后空间复杂度可控(O(N))
- 每次更新和查询的时间复杂度为O(logN)
- 可以高效处理大量重叠区间
3. 算法实现细节
3.1 数据预处理与离散化
首先我们需要处理输入数据:
cpp复制vector<vector<int>> squares; // 每个正方形表示为[x,y,l]
int n = squares.size();
// 准备事件点:每个正方形产生两个事件(下边和上边)
struct Event {
int y, x1, x2, delta; // delta=1表示下边(加入),-1表示上边(移除)
};
vector<Event> events;
for(auto& sq : squares) {
int x1 = sq[0], y1 = sq[1], l = sq[2];
int x2 = x1 + l, y2 = y1 + l;
events.push_back({y1, x1, x2, 1});
events.push_back({y2, x1, x2, -1});
}
// 对事件点按y坐标排序
sort(events.begin(), events.end(), [](auto& a, auto& b){
return a.y < b.y;
});
// x坐标离散化
vector<int> x_coords;
for(auto& e : events) {
x_coords.push_back(e.x1);
x_coords.push_back(e.x2);
}
sort(x_coords.begin(), x_coords.end());
x_coords.erase(unique(x_coords.begin(), x_coords.end()), x_coords.end());
3.2 线段树实现与面积计算
线段树需要维护两个核心信息:
- 区间被覆盖的次数(cnt)
- 区间的有效长度(len)
cpp复制struct SegmentNode {
int l, r;
int cnt = 0;
int len = 0;
};
vector<SegmentNode> seg_tree;
vector<int> length; // 存储离散化后每个x区间的原始长度
void build(int u, int l, int r) {
seg_tree[u] = {l, r, 0, 0};
if(l == r) {
seg_tree[u].len = length[l];
return;
}
int mid = (l + r) / 2;
build(u*2, l, mid);
build(u*2+1, mid+1, r);
seg_tree[u].len = seg_tree[u*2].len + seg_tree[u*2+1].len;
}
void update(int u, int l, int r, int delta) {
if(seg_tree[u].l >= l && seg_tree[u].r <= r) {
seg_tree[u].cnt += delta;
seg_tree[u].len = seg_tree[u].cnt > 0 ? length[seg_tree[u].r] - length[seg_tree[u].l-1] :
(seg_tree[u].l == seg_tree[u].r ? 0 : seg_tree[u*2].len + seg_tree[u*2+1].len);
return;
}
// ... 递归更新子节点
}
3.3 总面积计算与分割线查找
计算总面积后,我们再次扫描事件点,累计面积直到达到总面积的一半:
cpp复制// 第一次扫描:计算总面积
long long total_area = 0;
int prev_y = 0;
for(int i = 0; i < events.size(); ) {
int curr_y = events[i].y;
total_area += (curr_y - prev_y) * seg_tree[1].len;
// 处理同一y坐标的所有事件
while(i < events.size() && events[i].y == curr_y) {
auto& e = events[i];
int l = lower_bound(x_coords.begin(), x_coords.end(), e.x1) - x_coords.begin();
int r = lower_bound(x_coords.begin(), x_coords.end(), e.x2) - x_coords.begin();
update(1, l+1, r, e.delta);
i++;
}
prev_y = curr_y;
}
// 第二次扫描:查找分割线
long long target = total_area / 2;
long long accumulated = 0;
prev_y = 0;
for(int i = 0; i < events.size(); ) {
int curr_y = events[i].y;
long long delta = (curr_y - prev_y) * seg_tree[1].len;
if(accumulated + delta >= target) {
// 计算精确的分割线位置
double ratio = (double)(target - accumulated) / seg_tree[1].len;
return prev_y + ratio;
}
accumulated += delta;
// 处理同一y坐标的所有事件
while(i < events.size() && events[i].y == curr_y) {
auto& e = events[i];
int l = lower_bound(x_coords.begin(), x_coords.end(), e.x1) - x_coords.begin();
int r = lower_bound(x_coords.begin(), x_coords.end(), e.x2) - x_coords.begin();
update(1, l+1, r, e.delta);
i++;
}
prev_y = curr_y;
}
4. 性能优化与卡常技巧
4.1 静态数组优化
在算法竞赛中,使用vector等动态容器可能带来额外开销。我们可以改用静态数组:
cpp复制constexpr int MAX_N = 1e5 + 5;
struct Event {
int y, x1, x2, delta;
} events[MAX_N*2];
int x_coords[MAX_N*2];
4.2 线段树优化
- 去掉懒更新:这个问题中我们不需要区间修改的懒更新
- 使用位运算代替除法:
mid = (l + r) >> 1 - 预计算所有可能的区间长度
4.3 内存访问优化
- 将线段树节点改为结构体数组
- 确保相邻节点在内存中连续存储
- 减少不必要的函数调用
优化后的线段树实现:
cpp复制struct SegmentNode {
int cnt;
int len;
} seg_tree[MAX_N*4];
int length[MAX_N];
#define lson (u<<1)
#define rson (u<<1|1)
inline void pushup(int u, int l, int r) {
if(seg_tree[u].cnt) {
seg_tree[u].len = length[r] - length[l-1];
} else {
seg_tree[u].len = (l == r) ? 0 : seg_tree[lson].len + seg_tree[rson].len;
}
}
void update(int u, int l, int r, int ql, int qr, int delta) {
if(ql <= l && r <= qr) {
seg_tree[u].cnt += delta;
pushup(u, l, r);
return;
}
int mid = (l + r) >> 1;
if(ql <= mid) update(lson, l, mid, ql, qr, delta);
if(qr > mid) update(rson, mid+1, r, ql, qr, delta);
pushup(u, l, r);
}
5. 常见问题与调试技巧
5.1 边界条件处理
- 离散化后的坐标映射要一致
- 线段树区间是闭区间还是半开区间要明确
- 面积计算时注意整数溢出问题
5.2 精度问题
- 最终结果可能需要返回double类型
- 中间计算使用long long避免溢出
- 除法操作最后进行以减少精度损失
5.3 调试建议
- 先在小数据上验证算法正确性
- 打印中间结果检查离散化是否正确
- 验证线段树的更新和查询逻辑
6. 算法扩展与应用
这个算法框架可以解决更一般的问题:
- 处理矩形而不仅是正方形
- 计算多个分割线的情况
- 处理三维空间的类似问题(使用平面扫描+二维线段树)
在实际应用中,这种技术可以用于:
- 图像处理中的区域分割
- 地理信息系统中的区域划分
- 游戏开发中的碰撞检测和空间分区