题解:P13519 [KOI 2025 #2] 通行费
题意简述
原题干中的大写 \(N,M\) 在题解中均用对应的小写字母 \(n,m\) 表示。
对于 \(n\) 个点,\(m\) 条边的无向图,初始每条边的边权都为 \(0\),按照输入的顺序依次将边权改为 \(1\)。求每次修改边权后图上任意两点之间最短路的长度。
思路
注意到 \(2 \leq n \leq 500\),不难想到用 Floyd 来写。
-
首先最暴力的写法,对每次的修改边权跑一遍 Floyd 就可以了。然后写了一个 \(calc()\) 函数来计算每一时刻的答案。
为什么要跑 \(m\) 遍 Floyd 呢?显然后面增加了边权,最短路是会增加,而如果直接用目前的数据继续跑最短路就会导致答案偏小。对于每一条边都跑一次 Floyd 的复杂度是 \(\Theta (m \times n^3)\),肯定是会超时被创飞
(但我也不知道为什么会 RE 最后一个子任务)。 -
接下来考虑优化。很容易就会有一个想法,“时光倒流”,从后面往前面操作,也是一个很经典的 Trick,将原来的边权加一变为边权减一。显然,这个时候最短路的长度是单调递减的,我们就不用再全部重新跑一遍 Floyd 了,而只用在一开始就预处理之后对部分进行修改即可。
对于每一次边权减一的操作,需要修改这两点之间的距离,同时还要对其他点进行修改——如果有利用这条边作为其最短路中的某一条边的两点,这两点就也需要更新。因此相当于将 Floyd 的 \(k\) 循环定下来再跑里面的 \(i,j\) 循环。
这一段的代码可以得到 \(55\) 分:
#include<bits/stdc++.h> #define ll long long #define fi first #define se second #define pii pair<int,int> #define ios ios::sync_with_stdio(0),cin.tie(0),cout.tie(0) using namespace std; const int N=505; const int inf=0x3f3f3f3f; int n,m,dis[N][N]; stack<pii>st; stack<int>ans; int calc(){ int res=0; for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ res+=dis[i][j]; } } return res; } int main(){ ios;cin>>n>>m; int u,v; for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ if(i!=j)dis[i][j]=inf; else dis[i][j]=0; } } for(int i=1;i<=m;i++){ cin>>u>>v; st.push({u,v}); // 先进后出,对应时光倒流 dis[u][v]=dis[v][u]=1; } for(int k=1;k<=n;k++){ for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]); } } } while(!st.empty()){ ans.push(calc()); // 答案自然也要倒过来 int u=st.top().fi; int v=st.top().se; st.pop(); if(st.empty())break; dis[u][v]=dis[v][u]=0; for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ dis[i][j]=min(dis[i][j],dis[i][u]+dis[u][j]); } } for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ dis[i][j]=min(dis[i][j],dis[i][v]+dis[v][j]); } } } while(!ans.empty()){ cout<<ans.top()<<'\n'; ans.pop(); } return 0; }但是这样的代码,复杂度主要在于修改的部分,\(\Theta (m\times n^2)\) 在 \(m \leq \frac{n(n-1)}{2}\) 的数据范围下依旧无法接受。继续考虑优化。
-
既然复杂度主要在于修改的部分就优化这一段。既然优化复杂了,那就是说有的时候不需要优化。例如下图:

在这个图中,如果在修改了 \((1,4),(1,5)\) 这两条边后,则修改 \((4,5)\) 这条边不会影响答案。也就是说,若两点之间已经存在长度为 \(0\) 的路径,则将这两点之间的边权修改为 \(0\) 时不会对答案产生影响,则可以跳过此操作。
在进行了这样一步优化之后,显然我们要操作的次数变成了 \(n-1\) 次,相当于形成一颗全部边权为 \(0\) 的树的过程。
到这里思路就差不多了。代码实现也就是把上面的代码中的 \(while\) 循环中添加跳过的条件,复杂度变为 \(\Theta (n^3)\),于是就可以通过了。
代码实现:
#include<bits/stdc++.h>
#define ll long long
#define fi first
#define se second
#define pii pair<int,int>
#define ios ios::sync_with_stdio(0),cin.tie(0),cout.tie(0)
using namespace std;
const int N=505;
const int inf=0x3f3f3f3f;
int n,m,dis[N][N];
stack<pii>st;
stack<int>ans;
int calc(){
int res=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
res+=dis[i][j];
}
}
return res;
}
int main(){
ios;cin>>n>>m;
int u,v;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(i!=j)dis[i][j]=inf;
}
}
for(int i=1;i<=m;i++){
cin>>u>>v;
st.push({u,v});
dis[u][v]=dis[v][u]=1;
}
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
}
}
}
int res=calc();
while(!st.empty()){
ans.push(res);
int u=st.top().fi;
int v=st.top().se;
st.pop();
if(st.empty())break;
if(!dis[u][v])continue;
dis[u][v]=dis[v][u]=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
dis[i][j]=min(dis[i][j],dis[i][u]+dis[u][j]);
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
dis[i][j]=min(dis[i][j],dis[i][v]+dis[v][j]);
}
}
res=calc();
}
while(!ans.empty()){
cout<<ans.top()<<'\n';
ans.pop();
}
return 0;
}
完结撒花花!
本文来自博客园,作者:Circle_Table,转载请注明原文链接:https://www.cnblogs.com/Circle-Table/articles/19573967

浙公网安备 33010602011771号