P1967 [NOIP 2013 提高组] 货车运输【题解】
P1967 [NOIP 2013 提高组] 货车运输
题目背景
NOIP2013 提高组 D1T3
题目描述
A 国有 nnn 座城市,编号从 111 到 nnn,城市之间有 mmm 条双向道路。每一条道路对车辆都有重量限制,简称限重。
现在有 qqq 辆货车在运输货物,司机们想知道每辆车在不超过车辆限重的情况下,最多能运多重的货物。
输入格式
第一行有两个用一个空格隔开的整数 n,mn,mn,m,表示 A 国有 nnn 座城市和 mmm 条道路。
接下来 mmm 行每行三个整数 x,y,zx, y, zx,y,z,每两个整数之间用一个空格隔开,表示从 xxx 号城市到 yyy 号城市有一条限重为 zzz 的道路。
注意:x≠yx \neq yx=y,两座城市之间可能有多条道路。
接下来一行有一个整数 qqq,表示有 qqq 辆货车需要运货。
接下来 qqq 行,每行两个整数 x,yx,yx,y,之间用一个空格隔开,表示一辆货车需要从 xxx 城市运输货物到 yyy 城市,保证 x≠yx \neq yx=y。
输出格式
共有 qqq 行,每行一个整数,表示对于每一辆货车,它的最大载重是多少。
如果货车不能到达目的地,输出 −1-1−1。
输入输出样例 #1
输入 #1
4 3
1 2 4
2 3 3
3 1 1
3
1 3
1 4
1 3
输出 #1
3
-1
3
说明/提示
对于 30%30\%30% 的数据,1≤n<10001 \le n < 10001≤n<1000,1≤m<10,0001 \le m < 10,0001≤m<10,000,1≤q<10001\le q< 10001≤q<1000;
对于 60%60\%60% 的数据,1≤n<10001 \le n < 10001≤n<1000,1≤m<5×1041 \le m < 5\times 10^41≤m<5×104,1≤q<10001 \le q< 10001≤q<1000;
对于 100%100\%100% 的数据,1≤n<1041 \le n < 10^41≤n<104,1≤m<5×1041 \le m < 5\times 10^41≤m<5×104,$1 \le q< 3\times 10^4 $,0≤z≤1050 \le z \le 10^50≤z≤105。
申明:
在对 最小生成树Kruscal 和 最近公共祖先倍增法 达到相对熟练之前不建议写这题。
这是一篇建立在你对这些算法足够熟练基础上的一篇保姆级题解!
毕竟这是一道蓝题,涉及算法较杂
解析
题意分析
去掉所有废话,题干可精简为:一个有 nnn 个点 mmm 条边的无向图,给定每条边的起终点 (x,y)(x,y)(x,y) 和边权 zzz ,进行 qqq 次查询,每次给定一个 (x,y)(x,y)(x,y) ,求两点间路径中的最小边权最大是多少?
其中边权 zzz 指的就是道路限重大小 zzz ,qqq 个查询就是 qqq 辆卡车。
至于为啥这么简化,类似于短板效应,车的最大可行载重取决于路上重量限制最小的边。边的限重就可以看成边权!
做法分析
二分?
看到 “最小XX的最大值” 第一反应就是二分。随着最小边权的增加,可以走的路变少了,对其判断是否还能由 xxx 走到 yyy ,然而, qqq 次二分加上判断的时间复杂度 O(q⋅nlogn)O(q·nlogn)O(q⋅nlogn) 可能超时。 ( logloglog 中的 nnn 为图中最大边权,大小未知)
Floyed?
要询问任意两点间最大的最小边权,似乎可以用 FloyedFloyedFloyed 去做,但时间复杂度 (O(n3))(O(n^3))(O(n3)) 空间复杂度 (O(n2))(O(n^2))(O(n2)) 不可行( 转移方程:f[i][j]=max(f[i][j],min(f[i][k],f[k][j]))f[i][j]=max(f[i][j],min(f[i][k],f[k][j]))f[i][j]=max(f[i][j],min(f[i][k],f[k][j])) (f[i][j]f[i][j]f[i][j] 指 i→ji→ji→j 路径的最小边权))
Kruscal!
那么究竟是啥呢?要使路上的最小边权最大,就是要在 x→yx→yx→y 路上时尽量走边权大的边,这似乎与 KruscalKruscalKruscal 算法类似,KruscalKruscalKruscal 算法在运行时总是优先选择边权小的建树(前提不成环)。那么我们也可以用 KruscalKruscalKruscal 算法写一个最大生成树,而答案就是在最大生成树上两点间路径中的最小边权。
为啥答案就是这个呢?我们在算法中总是先走大边权这固然没问题,但当因成环而使某条边不能添加到树中,那么有同学会问:在这难道不会影响最终答案吗。实际上,因为成环,表明由某点 xxx 到某点 yyy 有两条路,而其中一条路 A1A_1A1 先被完成,另一条路 A2A_2A2 还差一条边 eee 完成时,判定发现边 eee 连上会有环而不连。而由于大边先连,所以 A1A_1A1 路上的所有边都一定大于等于 eee 的边权。我们只要尽可能大,所以较小 eee 并不会影响答案。
LCA!
求出最大生成树后就是对于每次查询进行找最大生成树路上的最小边权。如果我们用常规方法去求,就是把所有路上的边遍历一遍。然而这样的查询时间复杂度达到 O(nq)O(nq)O(nq) ,也不行啊!?
其实我们要用最近公共祖先( LCALCALCA )。这样的查询时间复杂度就为 O(qlogn)O(qlogn)O(qlogn) (倍增法),大优。在求出某两点 (x,y)(x,y)(x,y) 的最小公共祖先 fff 后,答案就是 min(min(min( x→fx→fx→f 路上最小边 ,y→fy→fy→f 路上最小边 )))。
关于 LCALCALCA 的倍增求法:P3379 【模板】最近公共祖先(LCA)【题解】(倍增法)
代码分析
总体时间复杂度:O(mlogm+qlogn)O(mlogm+qlogn)O(mlogm+qlogn)
#include<bits/stdc++.h>
using namespace std;
int n,m,q;
int d[10005];//点在最大生成树中的深度
int f[10005][21];//f[x][y]表示x点的2^j代祖先点
int w[10005][21];//w[x][y]表示x点到2^j代祖先点的路上的最小边的边权 struct EDGE{//用于存原图,即题目中输入的图 int fro,to,len;
}e[50005];
bool cmp(EDGE x,EDGE y){//sort的比较函数,把边按边权降序排列 return x.len>y.len;
}/*存用链式前向星法最大生成树*/
struct Edge{int to,nxt,len;
}edge[50005];
int fa[10005];
int tot;
int head[10005];
void add(int u,int v,int w){tot++;edge[tot].len=w;edge[tot].nxt=head[u];edge[tot].to=v;head[u]=tot;
}/*Kruscal算法建最大生成树*/
int find(int x){return x==fa[x]?x:fa[x]=find(fa[x]);
}
void kruscal(){for(int i=1;i<=n;i++) fa[i]=i;sort(e+1,e+1+m,cmp);for(int i=1;i<=m;i++){int eu=find(e[i].fro);int ev=find(e[i].to);if(eu==ev) continue;fa[eu]=ev;add(e[i].fro,e[i].to,e[i].len);add(e[i].to,e[i].fro,e[i].len);}
}/*LCA*/
bool vis[10005];
void dfs(int u){vis[u]=true;for(int i=head[u];i;i=edge[i].nxt){int v=edge[i].to;if(vis[v]) continue;f[v][0]=u;d[v]=d[u]+1;w[v][0]=edge[i].len;dfs(v);}
}
int ul;
void lca_build(){ul=log2(n)+1;for(int i=1;i<=n;i++){//由于题目没说给出的图一定为连通图,所以要考虑可能有多个联通快 if(!vis[i]){d[i]=1;f[i][0]=i;w[i][0]=0x3f3f3f3f;dfs(i);}}for(int i=1;i<=ul;i++){for(int j=1;j<=n;j++){f[j][i]=f[f[j][i-1]][i-1];w[j][i]=min(w[j][i-1],w[f[j][i-1]][i-1]);//一条长路中的最短边=组成它的两条短路中的最短边中的较小值 }}
}
int lca(int x,int y){ if(find(x)!=find(y)) return -1;//两点之间不联通 int ans=0x3f3f3f3f;if(d[x]>d[y]) swap(x,y);for(int i=ul;i>=0;i--){if(d[f[y][i]]>=d[x]){ans=min(ans,w[y][i]);//得到移动中路过的最小边 y=f[y][i];}}if(x==y) return ans;for(int i=ul;i>=0;i--){if(f[x][i]!=f[y][i]){ans=min(ans,min(w[x][i],w[y][i]));//得到移动中路过的最小边 x=f[x][i];y=f[y][i];}}ans=min(ans,min(w[x][0],w[y][0]));//最后的比较 return ans;
}int main(){cin>>n>>m;for(int i=1;i<=m;i++){int x,y,z;cin>>x>>y>>z;e[i]={x,y,z};}kruscal();lca_build();cin>>q;while(q--){int x,y;cin>>x>>y;cout<<lca(x,y)<<endl;}return 0;
}
全文历时4小时,文字反复斟酌尽力表达准确,若发现错误或不懂欢迎指出!