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;
}