1. 程式人生 > 實用技巧 >P1119 災後重建

P1119 災後重建


題解

我覺得這個題出的很好,讓我對 Floyd 演算法有了一個更深的理解。

淺談 Floyd

Floyd 是一個求多源最短路徑的演算法,演算法的內容很簡單。
這個演算法的主要思路,就是通過其他的點進行中轉來求的兩點之間的最短路。因為我們知道,兩點之間有多條路,如果換一條路可以縮短距離的話,就更新最短距離。而它最本質的思想,就是用其他的點進行中轉,從而達到求出最短路的目的。
\(f[i][j][k]\) 表示從 \(i\)\(j\) 只經過編號為 \(1\)~\(k\) 的節點的最短路。
轉移的話需要考慮兩種情況:最短路經過 \(k\) 和最短路不經過 \(k\),那麼就可以寫出轉移方程:
\(f[i][j][k]=\min(f[i][j][k-1],f[i][k][k-1]+f[k][j][k-1])\)


但是三維的狀態我們無法接受,需要考慮優化。
由於 \(k\) 是由 \(k-1\) 轉移來的,所以我們可以在外層列舉 \(k\),這樣就可以省掉第三維的狀態。
核心 \(Code:\)

for(int k=1;k<=n;k++)         //在最外層列舉中轉點k 
    for(int i=1;i<=n;i++)
	for(int j=1;j<=n;j++)
	    if(i!=k&&i!=j)
	        f[i][j]=min(f[i][j],f[i][k]+f[k][j]);

雖然 Floyd 演算法的程式碼很簡單,但是它的本質思想還是很重要的,而此題恰恰巧妙地考查了這點。

簡化題面

再回來看這個題,這個題就是讓我們求多次詢問的最短路,每次詢問都會有一些點無法經過。

思路

如果我們對於每次詢問都跑一次 Floyd 演算法的話時間複雜度肯定是爆炸的,這就提醒我們可以離線操作。
注意到對於每次詢問給出的時間 \(T\)我們需要在 \(t[i]<=T\) 的所有點中跑最短路。換句話說,我們需要求 \(f[i][j][T]\) 表示從 \(i\)\(j\) 只經過 \(t<=T\) 的點的最短路。
發現這和 Floyd 演算法的本質思想一致,那麼我們就可以順水推舟地往下做了:
我們按照每個點的時間 \(t\) 來從小到大列舉 \(k\),這樣每次內層迴圈結束後我們就會更新所有 \(t<=t[k]\)

的點之間的最短路。
然後我們每處理完一箇中轉點 \(k\) 之後就看看能否回答一些詢問,能回答就輸出。由於題目中保證 \(T\) 是遞增的,所以我們只要讀入+儲存就好了,不必再按照時間排序了。

細節

此題我們用鄰接矩陣來存圖,一定要處理好初始化的問題。

\(Code\)

#include<algorithm>
#include<iostream>
#include<cstdio>
using namespace std;
const int N=205;
const int INF=1e9;
int read()                        //讀入優化      
{
	int a=0,x=1;
	char ch=getchar();
	while(ch<'0'||ch>'9')
	{
		if(ch=='-') x=-x;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9')
	{
		a=(a<<1)+(a<<3)+(ch^48);
		ch=getchar();
	}
	return a*x;
}
int n,m,top=1;
int t[N],id[N],f[N][N];
struct node                       //記錄每個詢問的資訊 
{
	int u,v,t;
}a[1000000];
bool cmp(int x,int y)             //按照t從小到大排序 
{ 
	return t[x]<t[y];
}
int main()
{
	n=read();m=read();
	for(int i=0;i<n;i++)          //注意是從0開始編號 
	{
		t[i]=read();              //t[i]表示第i個點修成的時間 
		id[i]=i;                  //id[i]表示第i個點的編號為i 
	}
	for(int i=0;i<n;i++) 
		for(int j=0;j<n;j++)
		    if(i!=j) f[i][j]=INF; //邊初始化INF 
	for(int i=1;i<=m;i++)         
	{
		int u=read();
		int v=read();
		int w=read();
		f[u][v]=f[v][u]=w;        //注意雙向建邊 
	}
	int q=read(); 
	for(int i=1;i<=q;i++)         //我們將q次詢問存起來離線處理 
	{
		a[i].u=read();
		a[i].v=read();
		a[i].t=read();
	}
	sort(id,id+n,cmp);            //將每個點按照t從小到大排序,排完序後id[i]表示t從小到大排第i的數的編號 
	for(int k=0;k<n;k++)          //最外層迴圈列舉k,求t<=t[id[k]]的所有點間的最短路 
	{
		while(a[top].t<t[id[k]])  //如果能回答一些詢問(詢問時間a[i].t內的最短路已經求過了) 
		{
			int u=a[top].u;
			int v=a[top].v;
			if(f[u][v]>=INF||t[u]>a[top].t||t[v]>a[top].t) printf("-1\n");   //注意無解情況 
			else printf("%d\n",f[u][v]);
			top++;                //下一個問題 
		}
		for(int i=0;i<n;i++)      //鬆弛操作,Floyd演算法核心 
			for(int j=0;j<n;j++)
	    			f[id[i]][id[j]]=min(f[id[i]][id[j]],f[id[i]][id[k]]+f[id[k]][id[j]]);
	}
    while(top<=q)                 //所有點都建好了,但是詢問還沒問完,接著把剩下的輸出,且此時不用考慮時間的影響 
    {
    	int u=a[top].u;
		int v=a[top].v;
		if(f[u][v]>=INF) printf("-1\n"); //無解的情況 
		else printf("%d\n",f[u][v]);
		top++;
	}
	return 0;
}