作为一名算法竞赛选手,我最近集中攻克了几道经典的扫描线问题。扫描线算法是计算几何中的重要技巧,尤其在处理矩形面积、周长等问题时表现出色。本文将详细解析四道典型题目,分享我的解题思路和代码实现。
Atlantis问题要求计算多个矩形在平面上的总面积(重叠部分只计算一次)。这是经典的矩形面积并问题,扫描线算法是最佳解决方案。
扫描线算法的核心思想是:
cpp复制struct node{
double y, lx, rx;
int inout; // 1表示入边,-1表示出边
node(){}
node(double y, double x1, double x2, int io):
y(y), lx(x1), rx(x2), inout(io){}
}t[N];
线段树的pushup操作需要特殊处理:
cpp复制void pushup(int p, int l, int r){
if (tag[p]){ // 当前区间被完全覆盖
len[p] = X[r] - X[l]; // 直接计算长度
}
else if (l + 1 == r){ // 叶子节点且未被覆盖
len[p] = 0;
}
else{ // 部分覆盖,合并子节点信息
len[p] = len[p << 1] + len[p << 1 | 1];
}
}
与面积并不同,周长并需要计算所有矩形边界的总长度。这需要同时维护:
cpp复制bool lvis[N << 2], rvis[N << 2]; // 左右端点是否被覆盖
int num[N << 2]; // 连续区间段数
int tag[N << 2]; // 覆盖次数
int len[N << 2]; // 被覆盖的总长度
关键pushup逻辑:
cpp复制if (tag[p]){ // 当前区间被完全覆盖
lvis[p] = rvis[p] = true;
len[p] = r - l + 1;
num[p] = 1;
}
else if (l == r){ // 叶子节点且未被覆盖
len[p] = num[p] = 0;
lvis[p] = rvis[p] = false;
}
else{ // 合并子节点信息
lvis[p] = lvis[p << 1];
rvis[p] = rvis[p << 1 | 1];
len[p] = len[p << 1] + len[p << 1 | 1];
num[p] = num[p << 1] + num[p << 1 | 1];
if (lvis[p << 1 | 1] && rvis[p << 1])
num[p]--; // 合并相邻区间
}
题目要求统计满足三个条件的元素对,可以转化为三维偏序问题。CDQ分治是解决此类问题的有效方法。
cpp复制void CDQ(int l, int r) {
if (l >= r) return;
int mid = (l + r) >> 1;
CDQ(l, mid);
CDQ(mid + 1, r);
// 归并处理
vector<node> v;
int p1 = l, p2 = mid + 1;
while (p1 <= mid && p2 <= r){
if (inp[p1].t >= inp[p2].t){
if (!inp[p1].k) add(inp[p1].sum, 1); // 插入元素
v.push_back(inp[p1]);
p1++;
}
else{
if (inp[p2].k) ans[inp[p2].id] += query(len) - query(inp[p2].sum - 1);
v.push_back(inp[p2]);
p2++;
}
}
// 处理剩余元素
// ...
// 清空树状数组
for (int i = l; i <= mid; i++){
if (!inp[i].k) add(inp[i].sum, -1);
}
}
这道题要求统计满足特定条件的子区间数量。采用分治算法,将问题分解为左半区间、右半区间和跨越中点的区间。
cpp复制for (int i = mid; i >= l; i--){
mxl[i] = max(p[i], mxl[i + 1]);
mnl[i] = min(p[i], mnl[i + 1]);
}
for (int i = mid + 1; i <= r; i++){
mxr[i] = max(p[i], mxr[i - 1]);
mnr[i] = min(p[i], mnr[i - 1]);
}
cpp复制while (jr + 1 <= r && mxr[jr + 1] < mxl[i]){
jr++;
add(jr + mnr[jr], 1);
}
while (jl + 1 <= min(jr, r) && mnr[jl + 1] > mnl[i]){
jl++;
add(jl + mnr[jl], -1);
}
cpp复制solve(l, r, mid);
reverse(p + l, p + r + 1);
solve(l, r, r - mid + l - 1);
reverse(p + l, p + r + 1);
通过这几道题的练习,我总结了以下经验:
在算法竞赛中,理解算法原理比单纯记忆模板更重要。每道题都有其独特之处,需要根据具体情况调整解决方案。建议从简单案例入手,逐步验证算法正确性,再处理复杂情况。