LGP4990 小埋与刺客传奇 学习笔记

LGP4990 小埋与刺客传奇 学习笔记

Luogu Link

前言

曾几何时我也是舞线吃啊,但是水平泰国第几,刺客传奇是不可能过的。

现在……舞线诈尸一次后又停更多久了……令人感叹。

以及,很难评价此题的数据强度。说它强还是弱感觉都不合适。只能说古早公开赛题的数据专业程度存疑。

题意简述

给定一个 \(n\)\(m\) 边的简单有向有权图,初始时刻为第 \(0\) 时刻。

之后会有 \(T\) 个事件,第 \(i\) 个事件会在 \(t\) 时刻发生,属于以下两种操作之一:

  • 新加入一条编号为 \(m+i\) 的边 \(\{u_i,v_i,w_i\}\)
  • 删除当前未消失的边中编号第 \(k\) 大的边。

问这个图最早在哪个时刻存在 \(1\to n\) 的路径,并求出这个时刻 \(1\to n\) 的最长路长度。

如果始终不存在 \(1\to n\) 的路径,则输出一行Continue from the last checkpoint

\(n,T\le 10^5,m\le 2\times 10^5\);设有 \(b\) 次删边操作,\(b\le 10^3\)。数据保证不存在正环。

时限 \(\text{3.00s}\)

做法解析

按时刻排序后,题目的操作序列应该形如:有 \(b\) 次删边操作,每两个删边操作间有若干加边操作。

我们不能忍受每次操作后都重新跑一遍最长路,但我们发现,加边只会让连通性“单调上升”,所以我们对于每一段连续的加边操作,二分地去找它最早连通的时刻。

(至于删除操作,删边后的图连通性肯定劣于删边前,所以执行所有删除操作后都不需要跑最长路)

不过,“当前未消失的第 \(k\) 条边”怎么处理?大致方法是,我们用树状数组:对于边 \(e_i\),存在则 \(v_i=1\),不存在则 \(v_i=0\),并用树状数组维护 \(v_i\) 的前缀和,这样我们就能在树状数组上二分找到第一个满足前缀和大于等于 \(k\) 的地方,其对应的下标就是“当前未消失的第 \(k\) 条边”的编号。

设有 \(b\) 次删边操作,复杂度在 \(O(b\log T\cdot m\log m)\) 左右。

代码实现

小优化:对于每个连续加边段可以先判断一下其加满的时候连不连通,如果连这时都不能联通,显然二分是不用做的。

优化之后复杂度是不是变成 \(O((b+\log T)\cdot m\log m)\) 了。

#include <bits/stdc++.h>
using namespace std;
using namespace obasic;
const int MaxN=1e6+5,MaxM=3e6+5;
const int Inf=0x3f3f3f3f;
int N,M,T,W,X,Y,Z,Opt;
int B[MaxM],flag,ans[2];
struct oper{
    int w,o,x,y,z;
    friend bool operator<(oper a,oper b){return a.w<b.w;}
}A[MaxM];
int ava[MaxM],dis[MaxN],vis[MaxN],ddis[MaxN];
vector<int> Gr[MaxN];
struct anod{
    int u,d;
    friend bool operator<(anod a,anod b){return a.d<b.d;}
};
priority_queue<anod> pq;
bool dijk(int tim){
    fill(dis,dis+N+1,-1);
    fill(vis,vis+N+1,0);
    dis[1]=0;pq.push({1,0}),vis[1]=true;
    while(!pq.empty()){
        auto [u,d]=pq.top();pq.pop();
        for(auto &x : Gr[u]){
            if(ava[x]==-1||ava[x]>tim)continue;
            int v=A[x].y,w=A[x].z;
            if(dis[v]<dis[u]+w){
                dis[v]=dis[u]+w;
                if(!vis[v])vis[v]=1,pq.push({v,dis[v]});
            }
        }
    }
    ddis[tim]=dis[N];
    return dis[N]>=0;
}
void proceres(int t){if(!flag)flag=1,ans[0]=B[t-M],ans[1]=ddis[t];}
struct BinidTree{
    int n,t[MaxM];
    void init(int x){n=x,fill(t,t+n+1,0);}
    int lowbit(int x){return x&(-x);}
    void add(int p,int x){for(;p<=n;p+=lowbit(p))t[p]+=x;}
    int gts(int p){int res=0;for(;p;res+=t[p],p-=lowbit(p));return res;}
}BiT;
int main(){
    readis(N,M);
    for(int i=1;i<=M;i++){
        readis(X,Y,Z);
        A[i]={0,0,X,Y,Z};
    }
    readi(T);
    for(int i=1;i<=T;i++){
        readis(W,Opt),B[i]=W;
        if(Opt==0)readis(X,Y,Z);
        if(Opt==1)readis(X);
        A[M+i]={W,Opt,X,Y,Z};
    }
    sort(A+M+1,A+M+T+1),sort(B+1,B+T+1),BiT.init(M+T);
    for(int i=1;i<=M;i++)BiT.add(i,1);
    for(int i=1;i<=T;i++)ava[M+i]=A[M+i].w=M+i;
    for(int i=1;i<=M;i++)if(A[i].o==0)Gr[A[i].x].push_back(i);
    if(dijk(0)){writil(0),writi(dis[N]);return 0;}
    for(int i=M+1,j;i<=M+T&&!flag;){//
        if(A[i].o==0){
            j=i;while(A[j+1].o==0&&j<=M+T)j++;
            for(int k=i;k<=j;k++)BiT.add(k,1),Gr[A[k].x].push_back(k);
            if(!dijk(j)){i=j+1;continue;}
            int sl=i,sr=j-1,smid,sres=j;
            while(sl<=sr){
                smid=(sl+sr)>>1;
                if(dijk(smid))sres=smid,sr=smid-1;
                else sl=smid+1;
            }
            if(sres)proceres(sres);
            i=j+1;
        }
        else{
            int sl=1,sr=i,smid,sres=0;
            while(sl<=sr){
                smid=(sl+sr)>>1;
                if(BiT.gts(smid)>=A[i].x)sres=smid,sr=smid-1;
                else sl=smid+1;
            }
            if(sres)BiT.add(sres,-1),ava[sres]=-1;
            i++;
        }
    }
    if(flag)writil(ans[0]),writi(ans[1]);
    else puts("Continue from the last checkpoint");
    return 0;
}

反思总结

“以删边操作为界分块,每一块内部不断加边是满足单调性的”,这其实还算好想。

更值得积累的一点在于,这题做“当前未消失的第 \(k\) 条边”的做法:树状数组配合二分。

posted @ 2025-07-25 17:28  矞龙OrinLoong  阅读(2)  评论(0)    收藏  举报