[LOJ539] 旅遊路線
Description
T 城是一個旅遊城市,具有\(n\) 個景點和\(m\) 條道路,所有景點編號為\(1,2,...,n\) 。每條道路連線這\(n\) 個景區中的某兩個景區,道路是單向通行 的。每條道路都有一個長度。
為了方便旅遊,每個景點都有一個加油站。第\(i\) 個景點的加油站的費用為\(p_i\) ,加油量為\(c_i\) 。若汽車在第\(i\) 個景點加油,則需要花費\(p_i\) 元錢,之後車的油量將被加至 油量上限與\(c_i\) 中的較小值。不過如果加油前汽車油量已經不小於\(c_i\) ,則不能在該景點加油。
小 C 準備來到 T 城旅遊。他的汽車油量上限為\(C\) 。旅遊開始時,汽車的油量為\(0\) 。在旅遊過程中:
1、當汽車油量大於\(0\) 時,汽車可以沿從當前景區出發的任意一條道路到達 另一個景點(不能只走道路的一部分),汽車油量將減少\(1\) ;
2、當汽車在景點\(i\) 且當前油量小於\(c_i\) 時,汽車可以在當前景點加油,加油需花費\(p_i\) 元錢,這樣汽車油量將變為\(\min\{c_i,C\}\) 。
一次旅遊的總花費等於每次加油的花費之和,旅遊的總路程等於每次經過道路的長度之和。注意多次在同一景點加油,費用也要計算多次,同樣地,多次經過同一條道路,路程也要計算多次。
小 C 計劃旅遊\(T\) 次,每次旅遊前,小 C 都指定了該次旅遊的起點和目標路程。由於行程不同,每次出發前帶的錢也不同。為了省錢,小 C 需要在旅遊前先規劃好旅遊路線(包括旅遊的路徑和加油的方案),使得從起點出發,按照該旅遊路線旅遊結束後總路程不小於目標路程,且剩下的錢儘可能多。請你規劃最優旅遊路線,計算這\(T\) 次旅遊每次結束後最多可以剩下多少錢。
Solution
發現這種真正可以出到\(noip\) 裡的題的題解都是根據資料範圍和特殊資料一點一點改進演算法最後出正解的。考場上如果不能一下想到正解的話就可以根據資料範圍一點點往下做。
提前處理\(c_i=min(c_i,C)\)
資料範圍內有個很隱晦的提示是\(1\leq q_i\leq n^2\) ,也就是說每次花的錢\(\leq 10000\)
可以根據這個設計狀態\(f(i,c,q)\) 表示當前在點\(i\) ,手裡有\(q\) 元錢,車裡還有\(c\) 升油時最遠能走多遠。
那麼轉移可以枚舉出邊以及當前點加不加油
\[f(i,c,q)=\max\begin{cases}f(b_j,c-1,q)+l_j,&a_j=i,&\mathrm{if\ }c>0\\f(i,c_i,q-p_i),&&\mathrm{if\ }c<c_i\land q\ge p_i\end{cases}\]
狀態數有點大,考慮縮減狀態
發現在一個點加完油之後的油量是確定的,不用在狀態中記錄當前油量。\(f(i,q)\) 表示保證 在點\(i\) 加了油,手裡還有\(q\) 元錢最遠能走多遠,然後列舉下個加油點\(j\) ,轉移就是
\[f(i,q)=\max\left \{ f(j,q-p_i)+w(i,j,c_i) \right\}\]
其中\(w(i,j,c)\) 表示從\(i\) 到\(j\) 經過不超過\(c\) 條道路的最大路程。這個可以用\(DP\) 預處理,列舉\(i\) 的出邊\(t\)
\[w(i,j,c)=\max\left\{w(t,j,c-1)+l_{i,j}\right\}\]
邊界是\(w(i,i,0)=0,w(i,j,0)=-\infty\) (\(i\ne j\) )。
時間複雜度\(O(n^4+nmC+T\log n^2)\) ,期望得分\(75\) 。
上述演算法的複雜度瓶頸為預處理\(w(i,j,c)\) 。觀察到每次轉移\(c\) 減少\(1\) ,可以用倍增加速預處理過程。
設\(g(i,j,k)\) 表示從\(i\) 到\(j\) 經過不超過\(2^k\) 條道路的最大路程。轉移就是
\[g(i,j,k)=\max\limits_{t}\left\{g(i,t,k-1)+g(t,j,k-1)\right\}\]
那隻需要把所有\(i,j\) 都處理出\(w(i,j,c_i)\) 即可,每次提取出\(c\) 的一個二進位制位\(2^k\) ,那麼
\[w(i,j,c)=\max\limits_x\{w(i,x,c-2^k)+g(x,j,k)\}\] 。
倍增處理\(\log C\) 輪即可。
時間複雜度\(O(n^4+nm\log C+T\log n^2)\)
注意到處理\(w(i,j,c)\) 時如果還需要去求\(w(i,x,c-2^k)\) 那空間複雜度爆炸,可以優化這個過程,就像倍增\(Floyd\) 一樣,如果\(c_i\) 的第\(k\) 位為\(1\) ,那就嘗試將\(w\) 陣列和\(g(*,*,k)\) 合併。注意合併的時候要開一個臨時陣列不然會\(WA\) !
求完\(w\) 陣列之後求出\(f\) 陣列,然後每次詢問二分即可。
標準\(noip\) 題
Code
#include<set> #include<map> #include<cmath> #include<queue> #include<cctype> #include<vector> #include<cstdio> #include<cstring> #include<iostream> #include<algorithm> using std::min; using std::max; using std::swap; using std::vector; const int N=105; typedef double db; typedef long long ll; #define pb(A) push_back(A) #define pii std::pair<int,int> #define all(A) A.begin(),A.end() #define mp(A,B) std::make_pair(A,B) int n,m,C,T; int mp[N][N]; int p[N],c[N]; int f[N][N*N]; int val[N][N]; int w[N],sy[N]; int g[N][N][21]; int getint(){ int X=0,w=0;char ch=0; while(!isdigit(ch))w|=ch=='-',ch=getchar(); while( isdigit(ch))X=X*10+ch-48,ch=getchar(); if(w) return -X;return X; } signed main(){ n=getint(),m=getint(),C=getint(),T=getint(); for(int i=1;i<=n;i++){ p[i]=getint(); c[i]=getint(); c[i]=min(c[i],C); } memset(g,0xcf,sizeof g); for(int i=1;i<=n;i++) g[i][i][0]=0; for(int i=1;i<=m;i++){ int x=getint(),y=getint(),z=getint(); g[x][y][0]=max(g[x][y][0],z); } for(int p=1;(1<<p)<=C;p++){ for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ for(int k=1;k<=n;k++) g[i][j][p]=max(g[i][j][p],g[i][k][p-1]+g[k][j][p-1]); } } } for(int x=1;x<=n;x++){ memset(w,0xcf,sizeof w);w[x]=0; for(int i=0;(1<<i)<=c[x];i++){ if(c[x]>>i&1){ memcpy(sy,w,sizeof w); for(int j=1;j<=n;j++) for(int p=1;p<=n;p++) sy[j]=max(sy[j],w[p]+g[p][j][i]); memcpy(w,sy,sizeof sy); } } memcpy(val[x],w,sizeof w); } for(int i=0;i<=n*n;i++){ for(int x=1;x<=n;x++){ if(i>=p[x]){ for(int j=1;j<=n;j++) f[x][i]=max(f[x][i],f[j][i-p[x]]+val[x][j]); } } } while(T--){ int s=getint(),q=getint(),d=getint(); int l=0,r=q,ans=q+1; while(l<=r){ int mid=l+r>>1; if(f[s][mid]>=d) ans=mid,r=mid-1; else l=mid+1; } printf("%d\n",max(-1,q-ans)); } return 0; }