题解:P13614 [IOI 2018] highway 高速公路收费

前言

本文同步自洛谷专栏,题目传送门

在学习 WC2026 讲课时前来做此题。

部分题解中对于 \(50\) 次的限制并没有详细说明,只是认为卡不满,这里会给出一个数学上的证明。

解题

初步观察

给出了 \(50\) 次操作,看起来很有可能是若干个 \(\log\) 加在一起。

\(A,B\) 是题目给定的,若 \(B=2\times A\),那么在知道最短路长度的情况下,也极难确定有哪些可能的边。

如果把 \(A\) 作为默认边权,那么对于一次询问,我们可以知道的是,\(B\) 权边构成的边集是否将 \(S,T\) 割开,尝试基于此考虑。

分析

首先可以用一次操作得到默认边权下 \(S,T\) 的最短路,记作 \(minn\) 方便以后判断。

尝试二分边集的前缀,将编号为 \([0,x)\) 的边的边权设置为 \(B\)(下文称为删除操作),这样,可以用不超过 \(\lceil\log{m}\rceil\) 次操作,找到一个编号 \(x\),使得删除 \([0,x)\) 中的边后,最短路不变,但删除 \([0,x]\) 中的边后,最短路变化。

\(x\) 的两个端点为 \(C_1,C_2\),则以 \(C_1,C_2\) 为起点,同时跑 bfs,则可以把图剖分为两个部分,

此时 \(S,T\) 必然不在同一个部分中,否则不会经过边 \(x\)

然后把两个部分之间除 \(x\) 以外的边全部删去,确保 \(x\) 是必经边。

每个部分内部找点时,可以 bfs 一遍,只保留 bfs 生成树上的边,其余的删去,

同时获得 bfs 序,在 bfs 序上二分,每次把 bfs 序上前 \(x\) 和后 \(siz-x\) 个数之间的边删去,就可确定答案了。(\(siz\) 为该部分的大小)

这个二分的过程是 \(\lceil\log{siz}\rceil\)

复杂度与操作次数

设操作次数为 \(T\le 1+\lceil\log{m}\rceil+\lceil\log{x}\rceil+\lceil\log{(n-x)}\rceil\),其中 \(x\) 表示图剖开后,某一个部分的大小。

空间复杂度显然是 \(O(n+m)\),时间复杂度是 \(O(T(n+m))\),如果实现精细(二分时只处理变化的部分),可以做到除交互用时、交互库用时以外 \(O(n+m)\),但是瓶颈在交互库。

$\red{\text{注}}$

交互库复杂度是 \(O(T(n+m))\),在下发的 grader 中,其应用一般化的 01bfs 进行优化,做到了单次 \(O(n+m)\)

可以参考这篇博客


下面证明 \(T\le 50\),注意 \(m=1.3\times 10^5,n=9\times 10^4\)

\(\begin{aligned}T&\le 1+\lceil\log{m}\rceil+\lceil\log{x}\rceil+\lceil\log{(n-x)}\rceil\\&\le20+\lfloor\log{x}\rfloor+\lfloor\log{(n-x)}\rfloor\\&\le 20+\lfloor\log{x(n-x)}\rfloor\end{aligned}\)

由均值不等式:\(x(n-x)\le(\frac{x+n-x}{2})^2=\frac{n^2}{4}=2025000000\),取对数后下取整为 \(30\),则 \(T\le 20+30=50\)

事实上,此题给出的 \(n,m\) 限制都比较紧,如果开到 \(n=9.3\times 10^4\)\(m=1.4\times10^5\),就无法严格证明。

实现

$\red{\text{code}}$
#include<bits/stdc++.h>
using namespace std;
// #include "highway.h"
#define M 130005
#define ll long long

ll ask(const vector<int>&w);
void answer(int s,int t);

struct edge{
    int v,nxt;
}E[M<<1];

int m,vis[M],head[M],qp[M],cnt[M];
int C1,C2,maxn,n,tim;//lst 是最后一个到达的
ll minn;//tim 是时间戳
vector<int>Q;//查询边

inline void Qclear(){
    for(int i=0;i<m;i++) Q[i]=0;//先全部设定为 0
}

void init(vector<int>&U,vector<int>&V){//二分出一个必经边
    int L=0,R=m-1,mid,h=1,tail=1;
    for(;L<R;Qclear()){
        mid=(L+R)>>1;
        for(int i=0;i<=mid;i++) Q[i]=1; 
        if(ask(Q)==minn) L=mid+1;
        else R=mid;
    }
    C1=U[L],C2=V[L],vis[C1]=++tim,vis[C2]=++tim;//两个关键结点
    qp[tail++]=C1,qp[tail++]=C2;
    for(int u;h<tail;){//染色
        u=qp[h++];
        for(int i=head[u],v;i;i=E[i].nxt){
            if(!vis[v=E[i].v]) vis[v]=vis[u],qp[tail++]=v;
        }
    }
    for(int u=0;u<n;u++){
        for(int i=head[u],lst=0;i;i=E[i].nxt){//删去中间的分界边,便于实现
            if(vis[E[i].v]!=vis[u]){
                lst?(E[lst].nxt=E[i].nxt):(head[u]=E[i].nxt);
                if(u!=C1||E[i].v!=C2) cnt[i>>1]++;
            } 
            else lst=i;
        }
    }
}

void bfs(int rest,vector<int>&U,vector<int>&V,int t0){
    ++tim;
    for(int i=1;i<=rest;i++) vis[qp[i]]=tim;
    for(int i=0,x,y;i<m;i++){//至少一方
        x=vis[U[i]],y=vis[V[i]];
        if(x!=tim&&y!=tim) continue;
        if(x!=y) Q[i]=x>=t0&&y>=t0;//都大于 t0 才代表在同一侧
    }
    for(int i=0;i<m;i++) Q[i]|=cnt[i+1]==2;
}

int work(int S,vector<int>&U,vector<int>&V){
    maxn=0,qp[1]=S,vis[S]=++tim;//删去一些边后,两侧的部分可能各自不连通,要避免 dfs 出错
    for(int h=1,tail=2,u;h<tail;){
        u=qp[h++],maxn++;
        for(int i=head[u],v,lst=0;i;i=E[i].nxt){
            if(vis[v=E[i].v]!=tim) vis[v]=tim,qp[tail++]=v,lst=i;
            else lst?(E[lst].nxt=E[i].nxt):(head[u]=E[i].nxt),cnt[i>>1]++;
        }//只保留 bfs 生成树上的外向边,其余的全部标 1,同时生成 bfs 序
    }
    int L=1,R=maxn,mid,t0=tim;
    for(;L<R;Qclear()){
        mid=(L+R)>>1,bfs(mid,U,V,t0);
        ask(Q)==minn?R=mid:L=mid+1;//找到刚好包含目标位置的大小
    }
    return qp[L];
}

void find_pair(int nn,vector<int>U,vector<int>V,int A,int B){
    n=nn,m=U.size(),Q.resize(m),Qclear(),minn=ask(Q);//minn 是标准
    for(int i=0,esum=1,u,v;i<m;i++){
        u=U[i],v=V[i];
        E[++esum]={v,head[u]},head[u]=esum;
        E[++esum]={u,head[v]},head[v]=esum;
    }
    init(U,V),answer(work(C1,U,V),work(C2,U,V));
}

posted @ 2026-06-05 17:44  Wxb2010  阅读(5)  评论(0)    收藏  举报