洛谷P5471 NOI2019弹跳

1~8号测试点,32分做法

\qquad 暴力加边,跑 D i j s t r a Dijstra Dijstra

9~13号测试点 20分做法

\qquad 加边的时候二分或直接把点存进一个 m a p map map,暴力跑 D i j s t r a Dijstra Dijstra

14~18号测试点 20分做法

\qquad 出门左转线段树优化建图模板题

或许是实现起来最简单还不用卡常的100分做法:线段树套 s e t set set

\qquad 首先这题需要发现2个很有用的性质:

  • 性质 1 1 1,用 D i j s t r a Dijstra Dijstra跑最短路时,对于每个点,都只会在堆中弹出1次

  • 性质 2 2 2,对于每个弹跳装置,通过这一个弹跳装置到达的矩形中所有城市需要的时间相同(不是矩形中每个城市的最短路相同,而是通过这个装置到达它们的时间相同

\qquad 观察性质 2 2 2,我们发现可以把整个矩形放入 D i j s t r a Dijstra Dijstra中,对于每个堆顶弹出的矩形,我们只需要取其中任意一个之前没出现过的城市,都能保证这是该点的最短路径。那么,我们就把这题转换成了实现如下操作:

找到特定矩形中任意一个没有出现过的城市,跑最短路

\qquad 至于为什么只取矩形中的一个点,因为本蒟蒻太菜了,感觉不好操作,也只是常数问题。

\qquad 那么这题就被我们转换成了一道支持插入、删除任意一点、在某特定范围中查找第一个点的的二维数据结构+ D i j s t r a Dijstra Dijstra的题目。

\qquad 因为我们需要找到第一个未出现过的城市的确切位置,那么明显需要二分,所以外面就选择线段树。里面的比较随意,支持删除、可以动态开空间就行。而这题卡空间,不能用二维线段树,于是我们可以选择线段树套平衡树,为了方便,直接上 s e t set set

时空复杂度分析

\qquad 因为小蒟蒻的写法很不优秀,可能要查询(m+n)次,线段树查询最多 l o g log log个区间,每个区间用set查询,时间复杂度 O ( ( m + n ) l o g 2 2 n ) O((m+n)log_2^2n) O((m+n)log22n)。插入城市时,深度为 l o g ( n ) log(n) log(n),往线段树插入n个城市,所以空间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)

丑陋的代码:

// luogu-judger-enable-o2
#include<bits/stdc++.h>
using namespace std;
inline int read(){
    int a=0;char c=getchar();
    while(c>57 or c<48)c=getchar();
    while(47<c and c<58){
        a=a*10+c-48;
        c=getchar();
    }
    return a;
}//快读
#define Ls (x<<1)
#define Rs (x<<1|1)
#define MN 70005
#define INF 1e9
struct P{
    int id,x,y;
    bool friend operator<(P a,P b){
        if(a.y==b.y)return a.x<b.x;
        return a.y<b.y;
        //为什么用y作为第一关键字下面解释
    }
    P(int ID=0,int X=0,int Y=0){
        id=ID;x=X;y=Y;
    }
};//关于城市的结构体,记录城市编号和坐标
set<P>num[MN<<2];
int sum[MN<<2],f[MN],x[MN],y[MN],n,m,W,H,cnt;
//sum:线段树中某个区间的城市总数
//f:各个点的最短路
//cnt:松弛完成的的点的数量
struct Mat{
    int t,l,r,d,u;
    Mat(int T=0,int L=0,int R=0,int D=0,int U=0){
        t=T;l=L;r=R;d=D;u=U;
    }
    bool friend operator<(Mat a,Mat b){
        return a.t>b.t;
    }
};//矩阵的结构体,记录时间和大小
vector<Mat>edge[MN];//记录每个城市的弹跳装置指向的矩形
priority_queue<Mat>Q;//优先队列
void Insert(int x,int l,int r,int loc,int id,int y){
	//x坐标和线段树的编号重了,改成了loc
    int mid=(l+r)>>1;
    num[x].insert(P(id,loc,y));
    sum[x]++;
    if(l==r)return;
    if(mid>=loc)Insert(Ls,l,mid,loc,id,y);
        else Insert(Rs,mid+1,r,loc,id,y);
}//在线段树中插入城市
void del(int x,int l,int r,int loc,int id,int y){
    int mid=(l+r)>>1;
    num[x].erase(num[x].find(P(id,loc,y)));
    sum[x]--;
    if(l==r)return;
    if(mid>=loc)del(Ls,l,mid,loc,id,y);
        else del(Rs,mid+1,r,loc,id,y);
}//在线段树中删除城市
int ask(int x,int l,int r,int b,int e,int L,int R){
    if(!sum[x]||l>e||r<b)return 0;
    if(b<=l&&r<=e){
        set<P>::iterator it=num[x].lower_bound(P(0,b,L));
        //这里,因为我们的横坐标肯定符合要求,但是纵坐标可能不符合
        //所以我们以纵坐标为第一关键字,这样就不会出现明明有合法的城市却找出了1个不合法的来
        if(it==num[x].end()) return 0;//记得判断是否是在set中的,不然后面删除会RE
        int tmp=(*num[x].lower_bound(P(0,b,L))).id;
        if(y[tmp]>R||y[tmp]<L)return 0;//判断是否合法
        return tmp;
    }
    int mid=(l+r)>>1;
    int tmp=ask(Ls,l,mid,b,e,L,R);
    if(tmp) return tmp;
    return ask(Rs,mid+1,r,b,e,L,R);
}//找出区间中第一个点
void Dij(){
    while(!Q.empty())Q.pop();
    for(int i=1;i<=n;++i)f[i]=INF;
    f[1]=0;
    Q.push(Mat(0,x[1],x[1],y[1],y[1]));
    while(!Q.empty()){
        Mat w=Q.top();
        int cur=ask(1,1,W,w.l,w.r,w.d,w.u);		
        if(!cur){Q.pop();continue;}//一个矩形里没有城市时再删除
        del(1,1,W,x[cur],cur,y[cur]);
        f[cur]=w.t;
        cnt++;
        if(cnt==n)break;//松弛完直接break,因为后面的查询也需要log^2的时间但是肯定不会对结果造成影响
        for(int i=0;i<edge[cur].size();++i){
            edge[cur][i].t+=f[cur];
            Q.push(edge[cur][i]);
            edge[cur][i].t-=f[cur];
        }
    }
}//Dijstra
int main(){
    n=read();m=read();W=read();H=read();
    for(int i=1;i<=n;++i){
        x[i]=read();y[i]=read();
        Insert(1,1,W,x[i],i,y[i]);
    }//插入城市
    Mat w;
    int p;
    for(int i=1;i<=m;++i){
        p=read();w.t=read();w.l=read();w.r=read();w.d=read();w.u=read();
        edge[p].push_back(w);
    }
    Dij();
    for(int i=2;i<=n;++i)printf("%d\n",f[i]);
    return 0;
} 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值