1. 程式人生 > 實用技巧 >Luogu P5304 [GXOI/GZOI2019]旅行者|最短路

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(從源點開始)即可。

沒寫程式碼