題解/算法 {F - Fuel Round Trip}
@LINK: https://atcoder.jp/contests/abc320/tasks/abc320_f
;
即路程為P0 -> P1 -> ... -> P[n-1] -> Pn -> P[n-1] -> ... -> P0
長度為3 + 2*(n-1)
, 但是你無法直接對他進行DP 因為對於P0 -> ... Pi ... -> Pn -> ... Pi ... -> P0
這個Pi
他出現了2次 但是 他倆不是獨立的 是有關聯的, 即對於Pi 他有3種情況 要麼都不加油 要麼在(去程)加油 要麼在(返程)加油;
因此 雖然這個Pi在路程裡出現了2次 但是他倆不是獨立的, 因此 你需要把他倆合併成1個 從而關聯起來, 把路徑寫成如下形式:
Pi
去程: P0 -> ... -> Pn
返程: P0 <- ... <-
此時 一個Pi 他對應路徑裡的2個點 一個是去程的 一個是返程的;
此時 這裡這個DP 非常的巧妙 他不太容易想到…
先看上面那個錯誤的思路, 路程是3 + 2*(n-1)
個點 於是我們按照時間先後關係 這非常符合我們傳統的DP思路, 但是不可行的;
而這裡 我們為了讓Pi
給合併起來, DP定義為:
(i,j,k): 從`P0`出發(帶有`H`的初始油量)順時針到達`Pi`(不考慮在Pi加油)時 有`j`油量 且花費為P;
從`Pi`出發(帶有`k`的初始油量)逆時針到達`P0`(最終油量`>=0` 即可達) 且花費為S;
且讓`DP(i,j,k) = P+S`最小;
這個DP確實非常複雜…
#錯誤理解#: 你可能會認為 既然路徑是形如pre -> ... -> suf
那麼就讓DP去維護pre和suf
;
.
這裡錯誤的, 因為 你中間的...
是未知的 你一開始的pre|suf
怎麼可以說 他就是某一條路徑的前後綴呢? 你不會有這樣的預知超能力, 比如說 答案是無解的 即不存在合法路徑 那麼 按照你的定義 你的初始狀態全部為INF
可是你一開始怎麼能夠知道 答案是無解的…
仔細理解這個定義, 他並不完全是 答案路徑的前後綴! 他雖然確實維護了兩個路徑pre, suf
前綴與後綴, 但是 他更與答案路徑是毫無關係的(這點是樸素DP的思維慣性非常不同), 他是這樣的 非常複雜… 先是前綴P0->...->Pi
且到達Pi
點的油量是j
然後是後綴Pi(以k的油量出發)->...->P0
且前綴裡進行過加油的點 不能再加油, 可以發現: [前後綴 他倆有關聯(即加油站最多加油1次)] [但是 他倆也有獨立性 因為前綴結束時的油量j
和後綴出發時的油量k
是毫無關係的], 這確實是很複雜… 既有關聯 又有獨立性, 可以想象是 你從P0 -> ... -> Pi
後 忘掉你此時的油量j
將油量改成k
然後再立刻往回走到P0
的最小花費;
#前言#: 每個點有2個狀態{Fir: 剛到達該點(馬上要考慮加油問題), Sec: 馬上要離開該點(已經考慮完加油問題 你無需考慮加油問題)}
.
條件1: j
(可以認為是前綴) 定義是: 從起點(滿油)正向出發 到達Pi
點的Fir
狀態時 還有j
的油量 且花費為P
;
.
條件2: k
(後綴) 定義: 從Pi
點的Sec
狀態(即不考慮其加油) 以k
的初始油量 反向出發 到達P0
點 (剩餘油量隨意 只要能到P0
即可) 且花費為S
;
.
條件3: {1,2,...,i-1}
這些加油站 最多加油1次 (要麼不加油 要麼在前綴裡 要麼在後綴裡); (前後綴 這兩次行程 他倆能進行加油的點 是相同的 都是{1,2,...,i-1}
(根據定義 Pi
是不會進行加油的));
.
條件4: DP(i,j,k)
表示 是要讓P+S
(即總費用) 最小;
初始狀態: DP( 0, H, 0/1/.../H) = 0
;
DP轉移:
從i -> i+1
枚舉對Pi
加油站的3種情況 {不加油 前綴加油 後綴加油}
d: (Pi,P[i+1])的距離
j: 在Pi的`Fir`狀態的油量
k: 在Pi的`Sec`狀態的油量
如果前綴要加油 即油量會變成`min( j+加油, H)` 然後他需要`>= d` 這樣才能保證可以到達下個點
如果前綴不加油 則如果`j >= d` 則可以到達下個點;
(後綴是最難的... `P[i+1] -> Pi -> ... P0`
如果後綴不加油 則`P[i+1]`的`Sec`狀態的油量 必須是`k+d`(其要滿足`<=H`);
如果後綴加油 令`p: Pi的Fir狀態的油量` 此時有`max( p+加油, H)=k`, 當`p>=0 && p+d<=H`時 更新DP;
於是你得到下面的代碼
int N, H; cin>> N>> H;
Pos[ 0] = 0;
for( int i = 1; i <= N; ++i) cin>> Pos[ i];
for( int i = 1; i < N; ++i) cin>> Cost[ i]>> Add[ i];
memset( DP, 0x3F, sizeof( DP));
for( int i = 0; i <= H; ++i){
DP[ 0][ H][ i] = 0;
}
for( int i = 0; i < N; ++i){
for( int j = 0; j <= H; ++j){
for( int k = 0; k <= H; ++k){
auto & curDP = DP[i][j][k];
if( curDP == INT32_0x3F){ continue;}
auto dist = Pos[ i + 1] - Pos[ i];
if( i == 0){
if( j >= dist && k+dist <= H){
DP[ i+1][ j-dist][ k+dist] = 0;
}
continue;
}
{ // no use
if( j >= dist && k+dist <= H){
MinSelf_( DP[ i+1][ j-dist][ k+dist], curDP);
}
}
{ // use in OutBound
auto new_j = min( j + Add[ i], H);
if( new_j >= dist && k+dist <= H){
MinSelf_( DP[ i+1][ new_j-dist][ k+dist], curDP + Cost[ i]);
}
}
{ // use in Return
auto new_k = k - Add[ i];
if( j >= dist && new_k >= 0 && new_k+dist <= H){
MinSelf_( DP[ i+1][ j-dist][ new_k+dist], curDP + Cost[ i]);
}
}
}
}
}
int ANS = INT32_0x3F;
for( int j = 0; j <= H; ++j){
MinSelf_( ANS, DP[ N][ j][ j]);
}
if( ANS == INT32_0x3F){ ANS = -1;}
cout<< ANS<< ED_;
看似很正確, 卻疏忽了一點…
上面在分析後綴加油時
的那個p
(也就是代碼裡的new_k
), 你認為 他就是k - 加油
;
.
首先看下前綴的加油, 因為他是正向的, 因此 加油後 他的加油就變成了min( j+加油, H)
;
.
於是你照貓畫虎 後綴的加油 就是j - 加油
, 這是錯誤的… 因為 後綴他是反向的, 即new_k -> 加油 -> k
, 如果k < H
此時的new_k
確實只有一種可能 即k - 加油
這是沒問題的; 可以當k=H
時 他的new_k
是有多種可能的! 比如加油=4, k=H=5
此時new_k = {1,2,3,4,5}
都是可以的, 因此 這裡不像(前綴加油那樣) 這裡是有很多種可能性的 因此需要進行for
循環 一個個特判;
{ // use in Return
if( k == H){
for( int new_k = H-1; new_k >= 0 && new_k + Add[i] >= H; --new_k){
if( j >= dist && new_k+dist <= H){
MinSelf_( DP[ i+1][ j-dist][ new_k+dist], curDP + Cost[ i]);
}
}
}
else{
auto new_k = k - Add[ i];
if( j >= dist && new_k >= 0 && new_k+dist <= H){
MinSelf_( DP[ i+1][ j-dist][ new_k+dist], curDP + Cost[ i]);
}
}
}
注意 看似是4層循環 但因此最後那個是k==H
的特判 即可以認為是N* H * (H + H)
;