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]\) 已经被覆盖。因为覆盖点是没有意义的(毕竟点没有长度)。所以具体的操作如下:

  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\) 坐标的线段还没全部加到数组中就已经开始减的问题,对于此问题也可以直接不要第四维,根据第五维排序,但是对于下面的转化问题不行);
  2. 依次处理每个五元组,如果把 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))\)

实现

  1. 读入所有矩形,转化为五元组 \((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\) 的特判);
  2. 维护一棵区间加、全局最大值线段树,依次处理五元组:对于每个五元组 \((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。

posted @ 2025-06-15 17:44  Tenil  阅读(11)  评论(0)    收藏  举报