问题描述
给定一个n个点、m条边的带权无向图,其中有s个点是加油站。
每辆车都有一个油量上限b,即每次行走距离不能超过b,但在加油站可以补满。
q次询问,每次给出x,y,b,表示出发点是x,终点是y,油量上限为b,且保证x点和y点都是加油站,请回答能否从x走到y。
输入格式
第一行包含三个正整数n,s,m(2<=s<=n<=200000,1<=m<=200000),表示点数、加油站数和边数。
第二行包含s个互不相同的正整数c[1],c[2],...c[s] (1<=c[i]<=n),表示每个加油站。
接下来m行,每行三个正整数u[i],v[i],d[i] (1<=u[i],v[i]<=n,u[i]!=v[i],1<=d[i]<=10000),表示u[i]和v[i]之间有一条长度为d[i]的双向边。
接下来一行包含一个正整数q(1<=q<=200000),表示询问数。
接下来q行,每行包含三个正整数x[i],y[i],b[i] (1<=x[i],y[i]<=n,x[i]!=y[i],1<=b[i]<=2*10^9),表示一个询问。
输出格式
输出q行。第i行输出第i个询问的答案,如果可行,则输出TAK,否则输出NIE。
样例输入
6 4 5
1 5 2 6
1 3 1
2 3 2
3 4 3
4 5 5
6 4 5
4
1 2 4
2 6 9
1 5 9
6 5 8
样例输出
TAK
TAK
TAK
NIE
解析
如果全是加油站,问题就变成了对每一个询问,查找是否存在一条S到T的路径使该路径上的每一条边的长度均小于b。我们可以利用最小生成树中两点路径上的最大边最小的性质,对原图求最小生成树,利用倍增查询两点之间边权最大值,如果小于等于b就说明可行。
但问题是还有其他的点。我们需要将原图改为只有加油站构成的图。可以发现,改编图中两点之间的一条边对应原图中两点的一条路径。设\(p[i]\)表示离i最近的加油站,\(dis[i]\)表示到最近的加油站的距离。观察一条的路径,一定会存在一条边使得该边的两端点的p值不同。也就是说,在求出\(p\)和\(dis\)后,对于每一条边\((u,v)\),如果\(p[u]\)不等于\(p[v]\),说明\(p[u]\)到\(p[v]\)之间有一条路径,长度为\(dis[u]+dis[v]+len(u,v)\)。对应地在新图中连边即可。
还有几个需要注意的地方。
1.这样做会导致重边很多,要注意去重。
2.新图可能是不连通的,MST会是一个最小生成树森林。所以在倍增查询之前要判断两点是否在同一个连通块中。
代码
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> #include <queue> #include <cmath> #define N 200002 #define M 200002 using namespace std; struct Edge{ int u,v,w; }e[M]; int head[N],ver[M*2],nxt[M*2],edge[M*2],l; int n,s,m,q,i,j,c[N],p[N],dis[N],cnt,fa[N],dep[N],f[N][30],g[N][30],num,bcc[N]; bool in[N]; int read() { char c=getchar(); int w=0; while(c<'0'||c>'9') c=getchar(); while(c<='9'&&c>='0'){ w=w*10+c-'0'; c=getchar(); } return w; } void insert(int x,int y,int z) { l++; ver[l]=y; edge[l]=z; nxt[l]=head[x]; head[x]=l; } void SPFA() { queue<int> q; memset(dis,0x3f,sizeof(dis)); for(int i=1;i<=s;i++){ q.push(c[i]); dis[c[i]]=0;in[c[i]]=1; p[c[i]]=c[i]; } while(!q.empty()){ int x=q.front(); q.pop(); for(int i=head[x];i;i=nxt[i]){ int y=ver[i]; if(dis[y]>dis[x]+edge[i]){ dis[y]=dis[x]+edge[i]; p[y]=p[x]; if(!in[y]){ in[y]=1; q.push(y); } } } in[x]=0; } } int my_comp(const Edge &x,const Edge &y) { return x.w<y.w; } int find(int x) { if(fa[x]!=x) fa[x]=find(fa[x]); return fa[x]; } void Kruskal() { memset(head,0,sizeof(head)); l=0; int num=n; for(int i=1;i<=n;i++) fa[i]=i; sort(e+1,e+cnt+1,my_comp); for(int i=1;i<=cnt;i++){ if(num==1) break; int f1=find(e[i].u),f2=find(e[i].v); if(f1!=f2){ num--; fa[f1]=f2; insert(e[i].u,e[i].v,e[i].w); insert(e[i].v,e[i].u,e[i].w); } } } void dfs(int x,int pre) { bcc[x]=num; dep[x]=dep[pre]+1; f[x][0]=pre; for(int i=head[x];i;i=nxt[i]){ int y=ver[i]; if(y!=pre){ g[y][0]=edge[i]; dfs(y,x); } } } void init() { for(int i=1;i<=s;i++){ if(!dep[c[i]]){ num++; dfs(c[i],0); } } for(int j=0;(1<<(j+1))<s;j++){ for(int i=1;i<=s;i++){ if(f[c[i]][j]==0) f[c[i]][j+1]=0; else f[c[i]][j+1]=f[f[c[i]][j]][j]; g[c[i]][j+1]=max(g[c[i]][j],g[f[c[i]][j]][j]); } } } int ask(int u,int v) { if(dep[u]>dep[v]) swap(u,v); int tmp=dep[v]-dep[u],ans=0; for(int i=0;(1<<i)<=tmp;i++){ if(tmp&(1<<i)){ ans=max(ans,g[v][i]); v=f[v][i]; } } if(u==v) return ans; for(int i=log2(1.0*s);i>=0;i--){ if(f[u][i]!=f[v][i]){ ans=max(ans,max(g[u][i],g[v][i])); u=f[u][i],v=f[v][i]; } } ans=max(ans,max(g[u][0],g[v][0])); return ans; } int main() { n=read();s=read();m=read(); for(i=1;i<=s;i++) c[i]=read(); for(i=1;i<=m;i++){ int u=read(),v=read(),w=read(); insert(u,v,w); insert(v,u,w); } SPFA(); for(i=1;i<=n;i++){ for(j=head[i];j;j=nxt[j]){ if(p[i]<p[ver[j]]) e[++cnt]=(Edge){p[i],p[ver[j]],dis[i]+dis[ver[j]]+edge[j]}; } } Kruskal(); init(); q=read(); for(i=1;i<=q;i++){ int x=read(),y=read(),b=read(); if(bcc[x]!=bcc[y]||ask(x,y)>b) puts("NIE"); else puts("TAK"); } return 0; }
总结
- 这题提供了一个关于将原图转换为只有关键点的图的解法,需要积累。
- 做题时可以将想到的东西写下来,将这些碎片式的思路组合成一个解法。