题解: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)\),但是瓶颈在交互库。
下面证明 \(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));
}

浙公网安备 33010602011771号