[洛谷] P1502 窗口的星星(扫描线)
传送门: 窗口的星星
思路:
网上多数题解思路都是将点(星星)转化为矩阵处理,这里讲一下我的思路。
首先考虑简单化的问题,如果窗口只有宽度而没有高度限制,我们可以将同一 x 坐标的点权加和,转化为一维问题,即给定一个一维数组 a[],求长度为 w 的连续子数组最大和为多少。
解决这个问题的方法有许多,最容易想到的便是双指针,当然也有其它的方法。考虑数组中每个元素的贡献范围,每个元素 a[pos] 贡献范围为 [pos, pos+w-1],即如果窗口的右边界落在这个范围内,元素 a[pos] 就会被计入窗口的区间和。
于是引出另外两种的求法:
-
使用优先队列维护元素的贡献范围的起始和终止位置以及权值,起始和终止位置分别对应正权和负权(差分思想),每次取出队列中起止位置最靠前的元素,累加进权值和 sum 中,sum 即为窗口右边界停在此位置时的区间和
-
使用线段树维护区间最大值,枚举 a[] 中所有元素对各元素对应的贡献范围进行区间加操作,全局的区间最大值便是答案,窗口的右边界为区间最大值所在位置
而原问题是这个问题的升级版,原问题的窗口是二维的,因此需要同时考虑 x、y 两个方向,可以通过将上述两种方法结合使用来解决原问题。用线段树来维护每个点在 x 方向上的区间贡献,用优先队列维护每个点在 y 方向上的贡献,按 y 大小从小到大枚举每个起始点,每个点 (x, y) 在 y 处开始对区间 [x, x+w-1] 产生贡献 val,在 y+h 处开始产生 -val 的贡献(相当于取消贡献)。
绕了一圈,最终按这个思路写出的代码与其它题解代码是一样的 =_=,那就顺便讲一下一般的解题思路。我们可以直接考虑每个点在平面上的贡献范围,每个点的贡献范围为 (x, y) 到 (x+w, y+h) 的矩形区域,即当二维窗口的右上端点落在这个区间内时,该点就会对窗口的区间和产生贡献。
求出每个点产生贡献的矩形区域,用扫描线求出平面最大值即可。
代码实现时,需注意边框上的点不能取,对于同一 y 位置处的点应同时处理,不能边处理边更新(数据水了,没同时处理居然也能过)。
最后附一组数据,输出结果自行研究:
3
9 1 1
1 1 4
1 2 9
1 3 1
2 1 8
2 2 2
2 3 7
3 1 3
3 2 6
3 3 5
1 1 1
1 1 1
4 4 4
1 1 1
1 3 100
1 5 1000
1 7 10
题解代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
typedef tuple<int, int, int> tup;
#define fsio ios::sync_with_stdio(false)
#define ls (t<<1)
#define rs (t<<1|1)
const int inf = 0x3f3f3f3f;
const int maxn = 4e6+10;
int s[maxn], rk[maxn];
ll seg[maxn], tag[maxn];
struct P
{
int y, x1, x2, l;
bool operator < (const P &b) const
{
if (y == b.y) return l > b.l;
return y < b.y;
}
}eg[maxn];
void pushup(int t)
{
seg[t] = max(seg[ls], seg[rs]);
}
void pushdown(int t, int lt, int rt)
{
seg[ls] += tag[t];
seg[rs] += tag[t];
tag[ls] += tag[t];
tag[rs] += tag[t];
tag[t] = 0;
}
void build(int t, int tl, int tr)
{
seg[t] = tag[t] = 0;
if (tl == tr) return;
int mid = (tl+tr)>>1;
build(ls, tl, mid);
build(rs, mid+1, tr);
}
void update(int t, int tl, int tr, int l, int r, int val)
{
if (tr < l || tl > r) return;
if (l <= tl && tr <= r)
{
seg[t] += val;
tag[t] += val;
return;
}
int mid = (tl+tr)>>1;
pushdown(t, tl, tr);
update(ls, tl, mid, l, r, val);
update(rs, mid+1, tr, l, r, val);
pushup(t);
}
int main()
{
fsio; //fast_IO 关闭同步流
int T;
cin>>T;
while(T--)
{
int n, w, h;
cin>>n>>w>>h;
for (int i = 0; i < n; i++)
{
int x, y, l;
cin>>x>>y>>l;
rk[i] = x; //保存所有x坐标,准备离散化
rk[n+i] = x+w-1;
eg[i] = {y, x, x+w-1, l}; //存边
eg[n+i] = {y+h-1, x, x+w-1, -l};
}
sort(rk, rk+2*n);
sort(eg, eg+2*n);
int cnt = unique(rk, rk+2*n) - rk; //去重
for (int i = 0; i < 2*n; i++)
{
eg[i].x1 = lower_bound(rk, rk+cnt, eg[i].x1) - rk; //离散化
eg[i].x2 = lower_bound(rk, rk+cnt, eg[i].x2) - rk;
}
build(1, 0, cnt-1); //建树
ll ans = 0;
for (int i = 0; i < 2*n; i++)
{
auto [y, x1, x2, l] = eg[i]; //取出当前枚举的边
update(1, 0, cnt-1, x1, x2, l); //更新该边对应的区间
ans = max(ans, seg[1]); //更新结果
}
cout<<ans<<endl;
}
return 0;
}