【洛谷4348】[CERC2015] Cow Confinement(扫描线+线段树优化DP)
- 一张\(10^6\times10^6\)的网格图上有\(f\)个无交的矩形围栏(围栏在格边上)。
- 格子中有\(n\)头牛和\(m\)朵花,牛只能向右或向下走且不能越过围栏,求每头牛能到达的花的数量。
- \(n,m,f\le2\times10^5\)
暴力\(DP\)
考虑\(DP\),设\(f_{i,j}\)表示\((i,j)\)中的牛能到达多少朵花。
如果它右方没有围栏,就可以从\(f_{i,j+1}\)转移;如果它下方没有围栏,就可以从\(f_{i+1,j}\)转移。
暴力转移肯定\(T\)飞,需要优化。
扫描线+线段树优化\(DP\)
我们可以利用扫描线,从后往前枚举每一列,只要知道这一列上的所有围栏,就知道能否从\(f_{i,j+1}\)转移了。
而要知道能否从下方转移,可以开个\(set\)维护一下所有下方有围栏的行,相当于一列被划分成了若干段,同一段中是可以从\(f_{i+1,j}\)转移的。
考虑用一棵线段树来维护一列的所有\(DP\)值,从右转移是非常简单的:先全部继承,然后把有围栏的部分清零。
而从下转移就非常困难了,因此我们决定放弃转移,直接用\(f_{i,j}\)记录从\((i,j)\)开始往右走的答案,询问时只要查询从询问点开始向下一整段的\(f_{i,j}\)之和。
但真的只往右走肯定也是不行的,有些时候下方存在障碍格,最迟需要从这一行出发向下才能走到,我们把这部分的格子数也计入这一行的答案之中。
具体地,对于一个围栏\((x,y)-(u,v)\),我们在进入它和离开它的时候分别需要下述操作:
- 在\(v\)进入:段被划开,产生\(x-1\)和\(u\)两个新的划分点。对于第\(x-1\)行,原先段中\(x\)及其下方的格子需要从第\(x-1\)行出发到达,因此把这一部分的答案计入\(f_{x-1,j}\)。
- 在\(y-1\)离开:段被重新合并。对于第\(x-1\)行,进入围栏之前\(x\sim u\)右边的格子依旧需要从第\(x-1\)行出发到达,而\(y+1\)及其下方的格子可以第\(y+1\)行及其下方的格子出发到达,需要把这一部分从\(f_{x-1,j}\)中减去(可以在进入时记录)。
代码:\(O(nlogn)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 1000000
using namespace std;
int n,g[N+5],ans[N+5];set<int> p;
struct OP
{
int op,x,y,ty;I OP(CI i=0,CI a=0,CI b=0,CI c=0):op(i),x(a),y(b),ty(c){}
I bool operator < (Con OP& o) Con {return op^o.op?op<o.op:x<o.x;}
};vector<OP> q[N+5];vector<OP>::iterator it;
namespace FastIO
{
#define FS 100000
#define tc() (FA==FB&&(FB=(FA=FI)+fread(FI,1,FS,stdin),FA==FB)?EOF:*FA++)
#define pc(c) (FC==FE&&(clear(),0),*FC++=c)
int OT;char oc,FI[FS],FO[FS],OS[30],*FA=FI,*FB=FI,*FC=FO,*FE=FO+FS;
I void clear() {fwrite(FO,1,FC-FO,stdout),FC=FO;}
Tp I void read(Ty& x) {x=0;W(!isdigit(oc=tc()));W(x=(x<<3)+(x<<1)+(oc&15),isdigit(oc=tc()));}
Tp I void writeln(Ty x) {W(OS[++OT]=x%10+48,x/=10);W(OT) pc(OS[OT--]);pc('\n');}
}using namespace FastIO;
class SegmentTree
{
private:
#define PT CI l=1,CI r=N+1,CI rt=1
#define LT l,mid,rt<<1
#define RT mid+1,r,rt<<1|1
#define PU(x) (V[x]=V[x<<1]+V[x<<1|1])
#define PD(x) (F[x]&&(D(x<<1),D(x<<1|1),F[x]=0))
#define D(x) (V[x]=0,F[x]=1)
int V[N<<2],F[N<<2];
public:
I void U(CI x,CI v,PT)//单点加法
{
if(l==r) return (void)(V[rt]+=v);RI mid=l+r>>1;PD(rt);
x<=mid?U(x,v,LT):U(x,v,RT),PU(rt);
}
I void E(CI L,CI R,PT)//区间清零
{
if(L<=l&&r<=R) return (void)D(rt);RI mid=l+r>>1;PD(rt);
L<=mid&&(E(L,R,LT),0),R>mid&&(E(L,R,RT),0),PU(rt);
}
I int Q(CI L,CI R,PT)//区间求和
{
if(L<=l&&r<=R) return V[rt];RI mid=l+r>>1;PD(rt);
return (L<=mid?Q(L,R,LT):0)+(R>mid?Q(L,R,RT):0);
}
}S;
int main()
{
RI i,x,y,u,v;p.insert(N+1);
for(read(n);n;--n) read(x),read(y),read(u),read(v),q[y-1].push_back(OP(1,x,u,-1)),q[v].push_back(OP(1,x,u,1));//把围栏拆成两部分
for(read(n);n;--n) read(x),read(y),q[y].push_back(OP(2,x));//花
for(read(n),i=1;i<=n;++i) read(x),read(y),q[y].push_back(OP(3,x,i));//牛
for(i=N;i;--i) for(sort(q[i].begin(),q[i].end()),it=q[i].begin();it!=q[i].end();++it) switch(x=it->x,y=it->y,it->op)//扫描线从后往前枚举每一列
{
#define R(x) *p.lower_bound(x)//x所在段的右端点
case 1:if(!~it->ty) S.E(x,y),p.erase(p.find(y)),x^1&&(p.erase(p.find(x-1)),S.U(x-1,-g[x-1]),0);//离开时x-1减去y+1及其后的贡献
else x^1&&(S.U(x-1,S.Q(x,y)+(g[x-1]=S.Q(y+1,R(y)))),p.insert(x-1),0),p.insert(y),S.E(x,y);break;//进入时x-1加上x及其后的贡献(记录y+1及其后的贡献)
case 2:S.U(x,1);break;case 3:ans[y]=S.Q(x,R(x));break;//单点修改;询问所在段后缀和
}
for(i=1;i<=n;++i) writeln(ans[i]);return clear(),0;
}
待到再迷茫时回头望,所有脚印会发出光芒