Luogu P5304 [GXOI/GZOI2019]旅行者|最短路

链接

题目大意:有一个\(n\)个点,\(m\)单向边的图,有\(k\)个特殊点,求\(k\)个特殊点间的最短路的最小值。

\(n\le 10^5,m\le 5\times 10^5\)

题目思路:

这个数据大小,用\(k\)次单源最短路的方法是不可行的。这里需要一个船新的方法:二进制分组

方法是:每次将编号中某位二进制位不同点的分为两组,一组连源点,一组连汇点,边权均为\(0\),以源点为起点跑一次最短路,得出源点到汇点的最短路,再反着做一次(因为对于某个点对\((x,y)\)\((y,x)\)。它们的最短路可能不同)。每一位均操作一次后,得到的结果中的最小值即为答案。这样,我们只需要做约\(log_2 k\)次最短路,可以通过本题。

我们来证明一下解法的正确性:
对于所有的点对,每两个点必然有至少一位二进制位不同。也就是说,至少有一次分组中,两个点会被分到不同组。
因此,上文解法相当于每个点对都做了次最短路,更准确的说,是一堆点对同时做一次最短路。

上代码

#include<bits/stdc++.h>
#define S n+1
#define T n+2
#define N 100100
#define M 700100
using namespace std;
int cc,to[M],net[M],fr[N],l[M],f[N],h[M],ha[M],p[N];bool vis[N];
int g,n,m,k,t,u,v,len;
void addedge(int u,int v,int len)
{
	cc++;
	if (u==v) return ;
	to[cc]=v;net[cc]=fr[u];fr[u]=cc;l[cc]=len;
}
void add(int x,int xx)
{
	g++;
	h[g]=x;ha[g]=xx;
	int fa=g/2,so=g;
	while (h[fa]>h[so]&&fa)
	{
		swap(h[fa],h[so]);
		swap(ha[fa],ha[so]);
		so=fa;fa=fa/2;
	}
}
int del()
{
	int ans=ha[1];
	h[1]=h[g];ha[1]=ha[g];g--;
	int fa=1,so=2;
	if (h[so]>h[so+1]&&so+1<=g) so++;
	while (h[fa]>h[so]&&so<=g)
	{
		swap(h[fa],h[so]);
		swap(ha[fa],ha[so]);
		fa=so;so*=2;
		if (h[so]>h[so+1]&&so+1<=g) so++;
	}
	return ans; 
} 
void dij()
{
	for (int i=1;i<=n+2;i++) f[i]=2147483647,vis[i]=false;
	f[S]=0;add(f[S],S);
	while (g)
	{
		int x=del();
		if (vis[x]) continue;
		for (int i=fr[x];i;i=net[i])
		{
			if (f[to[i]]>f[x]+l[i])
			{
				f[to[i]]=f[x]+l[i];
				add(f[to[i]],to[i]);
			}
		}
	}
}
int main()
{
	cin>>t;
	for (int tt=1;tt<=t;tt++)
	{
		cin>>n>>m>>k;
		cc=0;int ans=2147483647;
		for (int i=1;i<=n;i++) fr[i]=0;
		for (int i=1;i<=m;i++)
		{
			cin>>u>>v>>len;
			addedge(u,v,len);
		}
		for (int i=1;i<=k;i++)
		  cin>>p[i];
		for (int i=1;i<=k;i=i<<1)
		{
			cc=m;
			for (int j=1;j<=k;j++)
			{
				if (i&j) addedge(S,p[j],0);else addedge(p[j],T,0);//二进制分组
			}
			dij();
			ans=min(ans,f[T]);
			for (int j=1;j<=k;j++)//复原
			{
				if (fr[p[j]]>m) fr[p[j]]=net[fr[p[j]]];
			}
			fr[S]=0;
			cc=m;
			for (int j=1;j<=k;j++)//反着做一次
			{
				if (!(i&j)) addedge(S,p[j],0);else addedge(p[j],T,0);
			}
			dij();
			ans=min(ans,f[T]);
			for (int j=1;j<=k;j++)
			{
				if (fr[p[j]]>m) fr[p[j]]=net[fr[p[j]]];
			}
			fr[S]=0;
		}
		cout<<ans<<endl;
	}
	return 0;
}

当然,还有另外的解法。该解法是更优的。似乎锤了std?

考虑边\((u,v)\),经过这条边的,特殊点的最短路的最小值是\(u\)最近特殊点的最短路+\((u,v)\)长度+离\(v\)最近的特殊点的最短路

那么,正反做两遍dij(从源点开始)即可。

没写代码

posted @ 2020-11-27 20:10  fmj_123  阅读(61)  评论(0编辑  收藏  举报