1. 程式人生 > 其它 >旅行者

旅行者

luoguP5304 [GXOI/GZOI2019]旅行者

大體題意:

給出一個有向圖,邊有邊權,以及 \(k\) 個關鍵點,求出所有關鍵點中任意兩個最短的距離是多少。

解法一:

我們考慮對每一個點,求出任意一個關鍵點到這個點的最短距離 \(dis_{1,i}\) 和這個點到任意一個關鍵點的最短距離 \(dis_{2,i}\) (最短距離以下統稱為最小值,次短距離稱為次小值),以及記錄以下這兩個最小值的來源是分別是哪個關鍵點 \(from_i,to_i\)

那麼對於答案的統計就是列舉每條邊。對於一條邊 \((u,v,w)\),如果說他的起點的來源 \(from_u\) 和他終點的來源 \(to_v\)

不同,那麼顯然 \(dis_{1,u}+w+dis_{2,v}\) 就是一種可用的答案,最後對這所有的答案取 \(\min\) 就可以了。

正確性證明:

對於任意兩個關鍵點 \(a,b\) 之間的路徑(包括答案的路徑),一定都可以分為兩部分。其中一部分屬於關鍵點 \(a\)(離關鍵點 \(a\) 比離關鍵點 \(b\) 更近),一部分屬於關鍵點 \(b\)(離關鍵點 \(b\) 比離關鍵點 \(a\) 更近)。其中的分界點就是這條路徑上的某一條邊,那麼對於一條路徑分成的兩段,前一段就是 \(dis_{1,u}\),後一段就是 \(dis_{2,v}\),再和這條邊的長度 \(w\) 求和,就是這條路徑上的答案。

並且如果說其中某個 \(dis_{1/2,i}\) 在更新過程中更新了一個最小值,那麼這個以前的值一定不會對我們統計的答案造成影響。

複雜度:這個題的複雜度瓶頸是堆優化後的Dijkstra的複雜度,所以複雜度為 \(O(Tn\log n)\)

Code

#include <iostream>
#include <cstring>
#include <queue>
using namespace std ;

#define int long long
const int N = 100005 , INF = 0x3f3f3f3f3f3f3f3f ;
int n , m , k , a[N] , ans ;
int dis[N] , from[N] , bis[N] , to[N] ;

int read ( )
{
	char ch = getchar ( ) ;
	int x = 0 ;
	while ( ch < '0' || ch > '9' )
		ch = getchar ( ) ;
	while ( ch >= '0' && ch <= '9' )
		x = x * 10 + ch - 48 , ch = getchar ( ) ;
	return x ;
}

struct Edge
{
	int nxt ;
	int to , len ;
} edge[N*5] , ed[N*5] ;

int cnt = 0 , head[N] ;
void insert ( int u , int v , int w )
{
	edge [ ++ cnt ] .nxt = head [ u ] ;
	edge [ cnt ] .to = v ;
	edge [ cnt ] .len = w ;
	head [ u ] = cnt ;
}

int ct = 0 , hd[N] ;
void insert1 ( int u , int v , int w )
{
	ed [ ++ ct ] .nxt = hd [ u ] ;
	ed [ ct ] .to = v ;
	ed [ ct ] .len = w ;
	hd [ u ] = ct ;
}

struct Node
{
	int x , dis ;
	Node ( int _x = 0 , int _dis = 0 ) : x ( _x ) , dis ( _dis ) { }
	bool operator ( ) ( Node u , Node v )
	{
		return u .dis > v .dis ;
	}
} ;

priority_queue < Node , vector < Node > , Node > q ;

void Dijkstra ( )
{
	for ( int i = 1 ; i <= n ; ++ i )
		dis [ i ] = INF ;
	for ( int i = 1 ; i <= k ; ++ i )
	{
		dis [ a [ i ] ] = 0 ;
		from [ a [ i ] ] = a [ i ] ;
		q .push ( Node ( a [ i ] , 0 ) ) ;
	}
	while ( ! q.empty ( ) )
	{
		Node tmp = q .top ( ) ; q .pop ( ) ;
		int x = tmp .x ;
		if ( dis [ x ] != tmp .dis )
			continue ;
		for ( int i = head [ x ] ; i ; i = edge [ i ] .nxt )
		{
			int y = edge [ i ] .to ;
			if ( dis [ x ] + edge [ i ] .len < dis [ y ] )
			{
				dis [ y ] = dis [ x ] + edge [ i ] .len ;
				from [ y ] = from [ x ] ;
				q .push ( Node ( y , dis [ y ] ) ) ;
			}
		}
	}
}

void Dijkstra1 ( )
{
	for ( int i = 1 ; i <= n ; ++ i )
		bis [ i ] = INF ;
	for ( int i = 1 ; i <= k ; ++ i )
	{
		bis [ a [ i ] ] = 0 ;
		to [ a [ i ] ] = a [ i ] ;
		q .push ( Node ( a [ i ] , 0 ) ) ;
	}
	while ( ! q.empty ( ) )
	{
		Node tmp = q .top ( ) ; q .pop ( ) ;
		int x = tmp .x ;
		if ( bis [ x ] != tmp .dis )
			continue ;
		for ( int i = hd [ x ] ; i ; i = ed [ i ] .nxt )
		{
			int y = ed [ i ] .to ;
			if ( bis [ x ] + ed [ i ] .len < bis [ y ] )
			{
				bis [ y ] = bis [ x ] + ed [ i ] .len ;
				to [ y ] = to [ x ] ;
				q .push ( Node ( y , bis [ y ] ) ) ;
			}
		}
	}
}

signed main ( )
{
	int T = read ( ) ;
	while ( T -- )
	{
		memset ( head , 0 , sizeof ( head ) ) ;
		memset ( hd , 0 , sizeof ( hd ) ) ;
		for ( int i = 1 ; i <= cnt ; ++ i )
			edge [ i ] .nxt = ed [ i ] .nxt = 0 ;
		cnt = ct = 0 ;
		n = read ( ) ;
		m = read ( ) ;
		k = read ( ) ;
		for ( int i = 1 ; i <= m ; ++ i )
		{
			int u = read ( ) , v = read ( ) , w = read ( ) ;
			insert ( u , v , w ) ;
			insert1 ( v , u , w ) ;
		}
		for ( int i = 1 ; i <= k ; ++ i )
			a [ i ] = read ( ) ;
		Dijkstra ( ) ;
		Dijkstra1 ( ) ;
		ans = INF ;
		for ( int i = 1 ; i <= n ; ++ i )
			if ( from [ i ] != to [ i ] )
				ans = min ( ans , dis [ i ] + bis [ i ] ) ;
		printf ( "%lld\n" , ans ) ;
	}
	return 0 ;
}

解法二:

首先,我們先討論一下只有兩個關鍵點的話,那麼顯然,就是對兩個點各跑一邊最短路,求這兩次的較小值就可以了。

根據以上想法,得到啟發,如果說有 \(k\) 個點的話,我們可以跑 k 次最短路,然後 \(k^2\) 的求出他們之間的最小值。

顯然,這樣子的話肯定過不了,複雜度 \(O(n^2\log n)\),會被卡裂開。所以考慮如何將這個東西進行簡化。

還是先討論只有兩個關鍵點的時候,之前我們的做法是跑兩次最短路,我們想一下怎麼樣能讓他只跑一次最短路,就可以求出來答案。

先思考一下SPFA演算法的流程,先是把起點加入佇列,然後每次從佇列裡拿出一個最小的點對其他的點進行更新。

而現在,我們能夠看出的是起點不止一個,如果說有兩個起點,我們是不是就可以在加起點的時候加入兩個起點,對全圖跑一次最短路。

這樣子的話我們能夠得出來的是對於全圖每個點離兩個關鍵點之一的最短路徑是多少,但是因為是求的最短路,所以兩個關鍵點的距離都不可能被更新(都是 \(0\)),我們就考慮如何讓關鍵點的距離也更新。

對於這個問題,我們就對每個點維護兩個最小值 \(dis_1~dis_2\),分別表示這兩個關鍵點到達這個點的最小值是多少,對於關鍵點來說,第一個關鍵點的 \(dis_1\)\(0\)\(dis_2\) 為無窮大,第二個關鍵點的 \(dis_2\)\(0\)\(dis_1\) 為無窮大,開始時把兩個關鍵點都加進去,求一遍最短路,然後對第一個關鍵點的 \(dis_2\) 和第二個關鍵點的 \(dis_1\)\(min\)

接下來考慮有 \(k\) 個關鍵點的情況。對於這種問題,我們同樣是維護兩個值,一個是最小值,一個是次小值,要求是這兩個值來源(也就是起點)是不同的關鍵點。然後關鍵點的最小值為 \(0\),而次小值為一個無窮大的值,其他的點最小值和次小值都為無窮大,最後求最短路。

複雜度分析:SPFA板子,複雜度 \(O(Tn^2)\)出題人竟然沒有卡spfa。 如果把這個東西的佇列改成堆,會減少很多冗餘的計算,表面上是加了一個 \(\log\),實際上跑的比用佇列的快了一倍左右。

Code

#include <iostream>
#include <cstring>
#include <queue>
using namespace std ;

#define int long long
const int N = 100005 ;
const int INF = 0x3f3f3f3f3f3f3f3f ;
int n , m , k , head[N] , a[N] , ans ;
int dis[N] , del[N] , bis[N] , bel[N] ;

int read ( )
{
	char ch = getchar ( ) ;
	int x = 0 ;
	while ( ch < '0' || ch > '9' )
		ch = getchar ( ) ;
	while ( ch >= '0' && ch <= '9' )
		x = x * 10 + ch - 48 , ch = getchar ( ) ;
	return x ;
}

struct Edge
{
	int nxt ;
	int to , len ;
} edge[N*20] ;

int cnt = 0 ;
void insert ( int u , int v , int w )
{
	edge [ ++ cnt ] .nxt = head [ u ] ;
	edge [ cnt ] .to = v ;
	edge [ cnt ] .len = w ;
	head [ u ] = cnt ;
}

struct Node
{
	int x , dis1 , bel1 , dis2 , bel2 ;
	Node ( int _x = 0 , int _dis1 = INF , int _bel1 = 0 , int _dis2 = INF , int _bel2 = 0 ) :
		x ( _x ) , dis1 ( _dis1 ) , bel1 ( _bel1 ) , dis2 ( _dis2 ) , bel2 ( _bel2 ) { }
	bool operator ( ) ( Node u , Node v )
	{
		if ( u .dis1 == v .dis1 )
			return u .dis2 > v .dis2 ;
		return u .dis1 > v .dis1 ;
	}
} ;

priority_queue < Node , vector < Node > , Node > q ;

void Dijkstra ( )
{
	for ( int i = 1 ; i <= n ; ++ i )
	{
		dis [ i ] = bis [ i ] = INF ;
		del [ i ] = i ;
		bel [ i ] = 0 ;
	}
	for ( int i = 1 ; i <= k ; ++ i )
	{
		dis [ a [ i ] ] = 0 ;
		q .push ( Node ( a [ i ] , 0 , a [ i ] , INF , 0 ) ) ;
	}
	while ( ! q .empty ( ) )
	{
		Node tmp = q .top ( ) ; q .pop ( ) ;
		int x = tmp .x ;
		if ( tmp .dis1 != dis [ x ] || tmp .dis2 != bis [ x ] )
			continue ;
		for ( int i = head [ x ] ; i ; i = edge [ i ] .nxt )
		{
			int y = edge [ i ] .to ;
			if ( bel [ x ] != del [ y ] && bel [ x ] )
			{
				if ( bis [ x ] + edge [ i ] .len < dis [ y ] )
				{
					bis [ y ] = dis [ y ] ;
					bel [ y ] = del [ y ] ;
					dis [ y ] = bis [ x ] + edge [ i ] .len ;
					del [ y ] = bel [ x ] ;
					q .push ( Node ( y , dis [ y ] , del [ y ] , bis [ y ] , bel [ y ] ) ) ;
				}
				else if ( bis [ x ] + edge [ i ] .len < bis [ y ] )
				{
					bis [ y ] = bis [ x ] + edge [ i ] .len ;
					bel [ y ] = bel [ x ] ;
					q .push ( Node ( y , dis [ y ] , del [ y ] , bis [ y ] , bel [ y ] ) ) ;
				}
			}
			else if ( bis [ x ] + edge [ i ] .len < dis [ y ] )
			{
				dis [ y ] = bis [ x ] + edge [ i ] .len ;
				del [ y ] = bel [ x ] ;
				q .push ( Node ( y , dis [ y ] , del [ y ] , bis [ y ] , bel [ y ] ) ) ;
			}
			if ( del [ x ] != del [ y ] )
			{
				if ( dis [ x ] + edge [ i ] .len < dis [ y ] )
				{
					bis [ y ] = dis [ y ] ;
					bel [ y ] = del [ y ] ;
					dis [ y ] = dis [ x ] + edge [ i ] .len ;
					del [ y ] = del [ x ] ;
					q .push ( Node ( y , dis [ y ] , del [ y ] , bis [ y ] , bel [ y ] ) ) ;
				}
				else if ( dis [ x ] + edge [ i ] .len < bis [ y ] )
				{
					bis [ y ] = dis [ x ] + edge [ i ] .len ;
					bel [ y ] = del [ x ] ;
					q .push ( Node ( y , dis [ y ] , del [ y ] , bis [ y ] , bel [ y ] ) ) ;
				}
			}
			else if ( dis [ x ] + edge [ i ] .len < dis [ y ] )
			{
				dis [ y ] = dis [ x ] + edge [ i ] .len ;
				del [ y ] = del [ x ] ;
				q .push ( Node ( y , dis [ y ] , del [ y ] , bis [ y ] , bel [ y ] ) ) ;
			}
		}
	}
}

signed main ( )
{
	int T = read ( ) ;
	while ( T -- )
	{
		memset ( head , 0 , sizeof ( head ) ) ;
		for ( int i = 1 ; i <= cnt ; ++ i )
			edge [ i ] .nxt = 0 ;
		cnt = 0 ;
		n = read ( ) ;
		m = read ( ) ;
		k = read ( ) ;
		for ( int i = 1 ; i <= m ; ++ i )
		{
			int u = read ( ) , v = read ( ) , w = read ( ) ;
			insert ( u , v , w ) ;
		}
		for ( int i = 1 ; i <= k ; ++ i )
			a [ i ] = read ( ) ;
		Dijkstra ( ) ;
		ans = INF ;
		for ( int i = 1 ; i <= k ; ++ i )
			ans = min ( ans , bis [ a [ i ] ] ) ;
		printf ( "%lld\n" , ans ) ;
	}
	return 0 ;
}

好吧,其實只看程式碼的話,應該會以為我在寫Dij。 (表面上的dij,實際上的spfa,逃