[题解] P4259 [Code+#3]寻找车位
[题解] P4259 [Code+#3]寻找车位
题目描述
给你一张 \(n*m\) 的 \(0/1\) 点阵,要求支持下面两种操作:
- 修改一个点为它的异或值
- 查询以 \((l,s)\) 和 \((r,t)\) 为对角的矩阵中,最大的全 \(0\) 正方形的边长
数据范围
- \(n*m\leq4*10^6\)
- \(n\leq40000\)
题解
为什么这个题解要列出数据范围?因为好康
因为我们可以发现 \(m\) 是非常小的,于是就有了下面的线段树的解法:
- 因为 \(n\) 比较大,对它进行 \(log\) 处理,也就是使得线段树维护的区间为第一维区间
下面想一想我们怎么通过维护线段树每个结点的值来得到最大正方形。
3
2
1
\(\cdots\)
需要维护两个变量来得到答案:
对于每一列,维护:
- 从下边界往上数的最长全 \(0\) 个数
- 从上边界往下数的最长全 \(0\) 个数
所以我们维护出了每一列的上边和下边的最长全 \(0\) 个数,可以发现这是好 \(pushup\) 的
先把问题简化:不考虑列的限制,通过上面维护的两个变量如何求得以每一列为右边界的最大全 \(0\) 正方形?
不难发现一个性质:
- 对于一个 \(0\) 区域,最大正方形取决于最短全 \(0\) 线段。
所以维护其单调递增的性质,由于是正方形,不难发现这是一个滑动窗口:
维护两个单调队列,对于一个确定的右边界 \(i\) 进行拼接操作:
-
如果两个队列的队头加起来 \(<i-L+1\) ,那么 \(L++\) 并弹掉不合法队头
-
更新 \(Ans\) 数组
void update(int *up,int *down,int *Ans){//通过两片0更新tAns数组
int L=1;h1=h2=1,t1=t2=0;
for(int i=1;i<=m;i++){
while(h1<=t1 && up[Q1[t1]]>=up[i])t1--;
while(h2<=t2 && down[Q2[t2]]>=down[i])t2--;//维护单调递减
Q1[++t1]=i;Q2[++t2]=i;
while(L<=i && up[Q1[h1]]+down[Q2[h2]]<i-L+1){
++L;
h1=Q1[h1]<L?h1+1:h1;h2=Q2[h2]<L?h2+1:h2;
}
Ans[i]=i-L+1;
}
}
下面最难理解的就是 \(query\) 操作了。
我们要把查询区间划分为线段树上的 \(log\) 个区间,怎么对答案进行合并?
3
2
1
\(\cdots\)
- 并不是取 \(max\) 这么简单
可以发现,被覆盖区间在图上是从上到下依次进行遍历的
为什么不能每个取 \(max\)?
因为上一个区间的 \(up\) 数组可能影响到当前区间的最大正方形
因此需要开一个临时数组 \(Cup\) 来存储上一个区间的 \(up\) ,再对当前区间的 \(down\) 和上一个区间的 \(Cup\) 进行合并答案操作
- 可以发现,这样取 \(max\) 是合法的
int query(int p,int l,int r,int x,int y,int LL,int RR){
if(p==1)for(int i=LL;i<=RR;i++)Cup[i]=0;
if(x<=l && r<=y){
int Res=solve(Cup,down[p],LL,RR);
for(int i=LL;i<=RR;i++){
Cup[i]=up[p][i]==r-l+1?Cup[i]+up[p][i]:up[p][i];//如果连上了,很好。否则更新Cup数组为当前区间的 Up数组,为下一个区间做准备
}
for(int i=LL;i<=RR;i++){
Res=max(Res,min(i-LL+1,tAns[p][i]));
}
return Res;
}
int mid=(l+r)>>1,Res=0;
if(x<=mid)Res=max(Res,query(p<<1,l,mid,x,y,LL,RR));
if(y>mid)Res=max(Res,query(p<<1|1,mid+1,r,x,y,LL,RR));
return Res;
}
因此代码长这样:(参考了 \(Zhang\_RQ\) 学长)
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 4e6 + 100 ;
int *down[maxn<<2],*up[maxn<<2],*tAns[maxn<<2];
int Cup[2005];
int Pool[maxn*40],*cur=Pool,*a[maxn];
int n,m,q,Q1[maxn],Q2[maxn],h1,h2,t1,t2;
int solve(int *up,int *down,int LL,int RR){//对于拼接的两个全0通过单调队列来查询从LL到RR列的最大全0边长
int L=LL;h1=h2=1,t1=t2=0;
int Res=0;
for(int i=LL;i<=RR;i++){
while(h1<=t1 && up[Q1[t1]]>=up[i])t1--;
while(h2<=t2 && down[Q2[t2]]>=down[i])t2--;//维护单调递减
Q1[++t1]=i;Q2[++t2]=i;
while(L<=i && up[Q1[h1]]+down[Q2[h2]]<i-L+1){
++L;
h1=Q1[h1]<L?h1+1:h1;h2=Q2[h2]<L?h2+1:h2;
}
Res=max(Res,i-L+1);
}
return Res;
}
void update(int *up,int *down,int *Ans){//通过两片0更新tAns数组
int L=1;h1=h2=1,t1=t2=0;
for(int i=1;i<=m;i++){
while(h1<=t1 && up[Q1[t1]]>=up[i])t1--;
while(h2<=t2 && down[Q2[t2]]>=down[i])t2--;//维护单调递减
Q1[++t1]=i;Q2[++t2]=i;
while(L<=i && up[Q1[h1]]+down[Q2[h2]]<i-L+1){
++L;
h1=Q1[h1]<L?h1+1:h1;h2=Q2[h2]<L?h2+1:h2;
}
Ans[i]=i-L+1;
}
}
void pushup(int p,int l,int r){
int mid=(l+r)>>1;
for(int i=1;i<=m;i++){
down[p][i]=down[p<<1][i]==mid-l+1?down[p<<1][i]+down[p<<1|1][i]:down[p<<1][i];
up[p][i]=up[p<<1|1][i]==r-mid?up[p<<1][i]+up[p<<1|1][i]:up[p<<1|1][i];
}
update(up[p<<1],down[p<<1|1],tAns[p]);
for(int i=1;i<=m;i++)tAns[p][i]=max(tAns[p][i],max(tAns[p<<1][i],tAns[p<<1|1][i]));//最大正方形可能不在边界处
}
void build(int p,int l,int r){
up[p]=cur;cur+=m+1;
down[p]=cur;cur+=m+1;
tAns[p]=cur;cur+=m+1;
if(l==r){
for(int i=1;i<=m;i++)
up[p][i]=down[p][i]=tAns[p][i]=a[l][i];
return ;
}
int mid=(l+r)>>1;
build(p<<1,l,mid);build(p<<1|1,mid+1,r);
pushup(p,l,r);
}
void change(int p,int l,int r,int posx,int posy){
if(l==r){
up[p][posy]=down[p][posy]=tAns[p][posy]=(a[posx][posy]^=1);
return ;
}
int mid=(l+r)>>1;
if(posx<=mid)change(p<<1,l,mid,posx,posy);
else change(p<<1|1,mid+1,r,posx,posy);
pushup(p,l,r);
}
int query(int p,int l,int r,int x,int y,int LL,int RR){
if(p==1)for(int i=LL;i<=RR;i++)Cup[i]=0;
if(x<=l && r<=y){
int Res=solve(Cup,down[p],LL,RR);
for(int i=LL;i<=RR;i++){
Cup[i]=up[p][i]==r-l+1?Cup[i]+up[p][i]:up[p][i];//如果连上了,很好。否则更新Cup数组为当前区间的 Up数组
}
for(int i=LL;i<=RR;i++){
Res=max(Res,min(i-LL+1,tAns[p][i]));
}
return Res;
}
int mid=(l+r)>>1,Res=0;
if(x<=mid)Res=max(Res,query(p<<1,l,mid,x,y,LL,RR));
if(y>mid)Res=max(Res,query(p<<1|1,mid+1,r,x,y,LL,RR));
return Res;
}
int main(){
scanf("%d%d%d",&n,&m,&q);
for(int i=1;i<=n;i++){
a[i]=cur;cur+=m+1;
for(int j=1;j<=m;j++)scanf("%d",&a[i][j]);
}
build(1,1,n);
for(int i=1,op,x,y,a,b;i<=q;i++){
scanf("%d",&op);
if(!op){
scanf("%d%d",&x,&y);
change(1,1,n,x,y);
}
else {
scanf("%d%d%d%d",&x,&a,&y,&b);
printf("%d\n",query(1,1,n,x,y,a,b));
}
}
return 0;
}
后记
细节
-
\(tAns\) 数组更新时不仅要考虑拼接部分,还要考虑子区间的 \(tAns\),因为拼接起来的不一定更大
-
考虑到列的限制,不可以盲目对 \(tAns\) 取 \(max\)
总结
-
数据范围非常关键,决定了我们的思考方向
-
线段树的合并答案是难点,前提是维护了和题目相干的变量,一定先想好我要维护哪些东西。

浙公网安备 33010602011771号