T4-食堂打饭-题解
Solution
题意
本蒟蒻自认为形式化已经比较直接了,这里就只复制一遍了。
在平面内有 \(N\) 个权为正的矩形区域,\(M\) 个权为负的矩形区域,矩形之间可能包含、重合。每个矩形对答案区域 \(x \in (x_0,x_0+w),y \in (y_0,y_0+h)\) 产生贡献当且仅当矩形有任意一点在答案区域内,设答案区域内权值和的最大值为 \(M\),求 \(\max(M,0)\)。
其实这个题几乎是扫描线的板子,建议阅读洛谷 P1502。本文主要介绍一下扫描线,也给出部分分做法。
分析
首先,开区间的处理十分麻烦,我们先把它转化为闭区间:
首先,由于矩形边界点为整数,我们可以认为答案区域的坐标都只会是整数或半整数(其他小数情况都一定可以找到一个整数的对应代替)。于是我们等价变化,将答案区域四个方向都向内收 \(0.5\),即 \(w \gets w-1,h \gets h-1\),即可将原问题转化为闭区间问题。
现在原问题变为:答案区间为闭区间,且答案区域边界点只可在整数或半整数处。
特殊性质 A、B
区间覆盖问题过于麻烦,我们尝试简化一下:
现在,我们只关注答案区域的右上角点 \((x_0,y_0)\),则一个矩形 \(x \in (x_1,x_2),y \in (y_1,y_2)\) 可以对答案区间做贡献当且仅当 \(x_0 \in [x_1,x_2+w-1]\) 且 \(y_0 \in [y_1,y_2+h-1]\)(下图给出了一个边界的情况,图中标注的 \(w,h\) 实为 \(w-1,h-1\)。另一个边界是答案区域右上角和矩形左下角重合的情况,结论是显然的)。

这里需要注意一下:如果 \(w\) 或 \(h\) 在原题目中是 \(0\),需要特判,不然答案区间的意义会出问题。
看到数据这么小,我们马上有了一个十分暴力的想法:我们枚举所有可能的答案区域右上角(所有整数和半整数),遍历每个矩形,考察当前矩形是否做贡献并更新答案,最后输出一下最大值就可以了。
有 \(10pts\) 的“答案区间一定在整数部分”,这里不单独讲。如果你非要只得 \(10pts\) 的话,改一下坐标的枚举就行(乐)。
#include <iostream>
#include <cstdio>
#include <cctype>
#include <cstring>
typedef long long ll;
ll fr() {
ll x=0,f=1;char c=getchar();
while(!isdigit(c)) {
if(c=='-') f=-1;
c=getchar();
}
while(isdigit(c)) {
x=(x<<3)+(x<<1)+(c^48);
c=getchar();
}
return x*f;
}
const int maxn=55;
int x1[maxn],x2[maxn],y1[maxn],y2[maxn],v[maxn];
ll ans,T;
int main() {
T=fr();
while(T--) {
std::memset(x1,0,sizeof(x1));
std::memset(x2,0,sizeof(x2));
std::memset(y1,0,sizeof(y1));
std::memset(y2,0,sizeof(y2));
std::memset(v,0,sizeof(v));
ans=0;
int n=fr(),m=fr(),w=fr(),h=fr();
int stx=0,edx=100,sty=0,edy=100;
for(int i = 1; i <= n+m; i++) {
x1[i]=fr(),y1[i]=fr(),x2[i]=fr()+w-1,y2[i]=fr()+h-1;
v[i]=(i<=n)?fr():-fr();
}
if(!(w&&h)) {
printf("0\n");
continue;
}//记得特判
for(float x = stx; x <= edx; x+=0.5) {
for(float y = sty; y <= edy; y+=0.5) {//注意答案区间的右上角可能取到半整数
ll now=0;
for(int i = 1; i <= n+m; i++) {
if(x>=x1[i]&&x<=x2[i]&&y>=y1[i]&&y<=y2[i]) {
now+=v[i];
}
}
ans=std::max(ans,now);
}
}
printf("%lld\n",ans);
}
return 0;
}
特殊性质 B & 正解
扫描线
为了解决这个问题,我们不妨先介绍一下扫描线。
扫描线是一种用于解决图形面积、周长、二维数点问题的技巧,本质上其实和求逆序对的思路相似。
先看看这个模板题,题意非常简单,就是求平面上若干矩形的面积并。本题看起来十分困难,实际也不简单(笑)。其实思路比较直接:把重叠的矩形拆成不重的,分别计算(如图,图来自 oi-wiki)。

那么具体怎么做呢?我们假设有一条平行于 \(x\) 轴的直线,从下向上扫描(这就是它名字的由来),不断更新覆盖到的矩形平行于 \(x\) 轴的边的长度,同时记录上次更新时扫描线的 \(y\) 坐标,每次更新时用当前的长度乘上 \(y\) 坐标的变化量,就可以算出矩形的面积了。具体地,我们可以想到维护一个初始全为 \(0\) 的 int 数组,并把每个矩形拆成两条边:下面那条和上面那条。当扫描线碰到下面那条时,在 int 数组对应位置的值加一;碰到上面那条时减一。然后每次更新时,矩形平行于 \(x\) 轴的边的长度就是数组中不为 \(0\) 的数的数量。
需要注意的是,上述“对应位置”其实已经相当于做了一次变换:数组角标为 \(i\) 的位置上大于一表示 \([i,i+1]\) 已经被覆盖。因为覆盖点是没有意义的(毕竟点没有长度)。所以具体的操作如下:
- 读入所有矩形 \((x_1,y_1,x_2,y_2)\),处理为 \((x_1,x_2,y_1,1,1)\) 和 \((x_1,x_2,y_2,0,-1)\),根据第三维、第四维双关键字排序(第三维优先,第四维是为了保证扫描时不出现当前 \(y\) 坐标的线段还没全部加到数组中就已经开始减的问题,对于此问题也可以直接不要第四维,根据第五维排序,但是对于下面的转化问题不行);
- 依次处理每个五元组,如果把 int 数组角标 \(x_1\) 到 \(x_2-1\) 的位置全部加上第五维,每次处理的 \(y\) 坐标变化后,暴力数整个数组中大于一的数的个数,更新面积即可。
基于以上说明,离散化后可以得到一个 \(O(N^2)\) 的算法,对于本题可以通过特殊性质 B,具体转化方法见下,此处不附代码。但是,这对于正解明显不够优秀。而且时间瓶颈明显在第二步。
思考一下我们上面第二步需要做的操作:区间加,全局对大于零求和,自然可以想到线段树。可能线段树码量令人生畏,但是扫描线要求的操作比较特殊:要加的区间成对出现,相互抵消,所以甚至不需要维护 lazytag,相对会好写很多。
具体地,我们在离散化后的矩形 \(x\) 坐标数组上建树,每个节点上维护 \(cnt\) 和 \(len\) 两个信息,分别表示本区间被覆盖的次数和本区间的有效长度。每次更新时,假设我们需要在原数组 \([x_1,x_2-1]\) 区间上进行操作,记 \(x\) 离散化后的对应值是 \(val(x)\),则我们在 \([val(x_1),val(x_2)-1]\) 上对 \(cnt\) 区间加。(你会发现标记的意义与实际好像不符,但是请看下下面的处理,又会对了)
数值的维护如下:若树上区间 \([l,r]\) 的 \(cnt\) 大于一,记离散化后 \(x\) 对应原坐标 \(raw(x)\),则 \(len=raw(r+1)-raw(l)\),否则本节点 \(len\) 等于它的两个儿子的 \(len\) 之和。更新时取根的 \(len\) 即可。
于是你获得了一个 \(O(n \log n)\) 的算法,赢!
转化
对于本题,思路其实与上面介绍的扫描线板子思路差不多,只是需要换个角度。
现在的问题就是:二维平面内有若个带权矩形区域,重叠部分权值是各区域权值和,找一点使权值和最大。类似地,我们把这些矩形拆成若干不重叠的区域,依次更新即可。具体地,我们读入每个矩形 \((x_1,y_1,x_2,y_2,v)\)(\(v\) 表示权值),把它转化成 \((x_1,x_2+w-1,y_1,1,v)\) 和 \((x_1,x_2+w-1,y_2+h-1,0,-v)\)(坐标减一的原因与上面相同,这里的数表示的是长为一的线段,第四维作用同上)。
然后现在维护一棵区间加、全局最大值线段树,但是这个就要写 lazytag 了,稍微麻烦。
最后,还有两个大问题:
一、在按上述方式(扫所有五元组,更新最大)时,其实你的答案区间 \(y\) 坐标只能是整数。譬如如下情况:(对应特殊性质 B)

扫描时会出问题,因为按上述思路扫描,红色区域无法单独取得(事实上应该是可以的)。那怎么处理呢?简单,按照横、纵坐标扫两次就可以了。
for(int i = 1; i <= n+m; i++) {
int xf=lower_bound(v.begin(),v.end(),x[i])-v.begin()+1;
int xs=lower_bound(v.begin(),v.end(),x[i]+dx[i]+w-1)-v.begin()+1;
int yf=lower_bound(v.begin(),v.end(),y[i])-v.begin()+1;
int ys=lower_bound(v.begin(),v.end(),y[i]+dy[i]+h-1)-v.begin()+1;
c1[i*2-1]={xf,yf,ys,l[i],1};
c1[i*2]={xs,yf,ys,-l[i],0};
c2[i*2-1]={yf,xf,xs,l[i],1};
c2[i*2]={ys,xf,xs,-l[i],0};
}
std::sort(c1+1,c1+2*(n+m)+1);
std::sort(c2+1,c2+2*(n+m)+1);
int nowx=0,nowt=1;
for(int i = 1; i <= 2*(n+m); i++) {
if(c1[i].t!=nowt||c1[i].x!=nowx) ans=std::max(ans,st.node[1]);
nowt=c1[i].t,nowx=c1[i].x;
st.upd(1,1,v.size(),c1[i].y1,c1[i].y2,c1[i].k);
}
nowx=0,nowt=1;
for(int i = 1; i <= 2*(n+m); i++) {
if(c2[i].t!=nowt||c2[i].x!=nowx) ans=std::max(ans,st.node[1]);
nowt=c2[i].t,nowx=c2[i].x;
st.upd(1,1,v.size(),c2[i].y1,c2[i].y2,c2[i].k);
}
二、按照“坐标变化一次更新一次最大值”方式更新会有问题,譬如以下的情况:

扫到 \(x=1\) 时,按理说会把右侧两个矩形权值计入,得到最大值是 \(4\) 的答案,但是左侧两个矩形在 \(x=1\) 时也会把自己的权值减去,所以无法得到正确答案。因此更新的方式应为:五元组 \((x,y,z,t,v)\) 的第三维或第四维变化时,更新最大值(上面的代码中已经体现)。
终于,您 AC 了。
复杂度 \(O(T(N+M) \log (N+M))\)。
实现
- 读入所有矩形,转化为五元组 \((x_1,x_2+w-1,y_1,1,v),(x_1,x_2+w-1,y_2+h-1,0,-v)\) 和 \((y_1,y_2+h-1,x_1,1,v),(y_1,y_2+h-1,x_2+w-1,0,-v)\),把坐标全部离散化后,把两组分别依据第三、第四维双关键字排序(注意 \(w,h\) 为 \(0\) 的特判);
- 维护一棵区间加、全局最大值线段树,依次处理五元组:对于每个五元组 \((x,y,z,tag,v)\),在树上区间 \([x,y]\) 对应加上 \(v\)。记录每次的 \(z,tag\),当 \(z\) 或 \(tag\) 变化时,用根节点的值更新 \(ans\)。
有不懂的建议结合代码康康。
Code
#include <iostream>
#include <cctype>
#include <cstdio>
#include <algorithm>
#include <vector>
#include <cstring>
typedef long long ll;
ll fr() {
ll x=0,f=1;char c=getchar();
while(!isdigit(c)) {
if(c=='-') f=-1;
c=getchar();
}
while(isdigit(c)) {
x=(x<<3)+(x<<1)+(c^48);
c=getchar();
}
return x*f;
}
const int maxn=2.2e4;
std::vector<ll> v;
int l[maxn];
ll x[maxn],y[maxn],dx[maxn],dy[maxn];
int T,w,h,n,m;
ll ans;
struct cover{
ll x,y1,y2,k,t;
bool operator <(const cover &c) const &{
return (x==c.x)?t>c.t:x<c.x;
}
}c1[maxn*4],c2[maxn*4];
struct segt{
ll node[maxn*32],tag[maxn*32];
inline void tagd(int p) {
if(tag[p]) {
tag[p*2]+=tag[p],tag[p*2+1]+=tag[p];
node[p*2]+=tag[p];
node[p*2+1]+=tag[p];
tag[p]=0;
}
}
void upd(int p,int tl,int tr,int l,int r,ll k) {
if(tl>=l&&tr<=r) {
node[p]+=k;
tag[p]+=k;
return;
}
int mid=(tl+tr)>>1;
tagd(p);
if(l<=mid&&r>mid) upd(p*2,tl,mid,l,r,k),upd(p*2+1,mid+1,tr,l,r,k);
else if(r<=mid) upd(p*2,tl,mid,l,r,k);
else if(l>mid) upd(p*2+1,mid+1,tr,l,r,k);
node[p]=std::max(node[p*2],node[p*2+1]);
}
}st;
int main() {
T=fr();
while(T--) {
n=fr(),m=fr(),w=fr(),h=fr();
ans=0;
memset(st.node,0,sizeof(st.node));
memset(st.tag,0,sizeof(st.tag));
v.clear();
for(int i = 1; i <= n+m; i++) {
x[i]=fr(),y[i]=fr();
dx[i]=fr()-x[i],dy[i]=fr()-y[i];
l[i]=(i<=n)?fr():-fr();
v.push_back(x[i]);v.push_back(x[i]+dx[i]+w-1);
v.push_back(y[i]);v.push_back(y[i]+dy[i]+h-1);
}
std::sort(v.begin(),v.end());
v.erase(std::unique(v.begin(),v.end()),v.end());
for(int i = 1; i <= n+m; i++) {
int xf=lower_bound(v.begin(),v.end(),x[i])-v.begin()+1;
int xs=lower_bound(v.begin(),v.end(),x[i]+dx[i]+w-1)-v.begin()+1;
int yf=lower_bound(v.begin(),v.end(),y[i])-v.begin()+1;
int ys=lower_bound(v.begin(),v.end(),y[i]+dy[i]+h-1)-v.begin()+1;
c1[i*2-1]={xf,yf,ys,l[i],1};
c1[i*2]={xs,yf,ys,-l[i],0};
c2[i*2-1]={yf,xf,xs,l[i],1};
c2[i*2]={ys,xf,xs,-l[i],0};
}
std::sort(c1+1,c1+2*(n+m)+1);
std::sort(c2+1,c2+2*(n+m)+1);
if(!(w&&h)) {
printf("0\n");
continue;
}//记得特判
int nowx=0,nowt=1;
for(int i = 1; i <= 2*(n+m); i++) {
if(c1[i].t!=nowt||c1[i].x!=nowx) ans=std::max(ans,st.node[1]);
nowt=c1[i].t,nowx=c1[i].x;
st.upd(1,1,v.size(),c1[i].y1,c1[i].y2,c1[i].k);
}
nowx=0,nowt=1;
for(int i = 1; i <= 2*(n+m); i++) {
if(c2[i].t!=nowt||c2[i].x!=nowx) ans=std::max(ans,st.node[1]);
nowt=c2[i].t,nowx=c2[i].x;
st.upd(1,1,v.size(),c2[i].y1,c2[i].y2,c2[i].k);
}
printf("%lld\n",ans);
}
return 0;
}
闲话
本题不卡输入输出,不卡 int,不卡 memset,留 std 三倍时间、二十六倍空间,实乃业界良心,还望周知。
因为题目太阴了(最后的“两个大问题”),本人自己写 std 都被卡了两次,要是谁是场切的,本蒟蒻高低得给您磕一个 sto。

浙公网安备 33010602011771号