有向無環圖上的動態規劃是學習動態規劃的基礎,很多問題都可以轉化爲DAG上的最長路,最短路或路徑計數問題
9.2.1DAG模型
嵌套矩形問題
分析:
矩形之間的可嵌套關係是一個典型的二元關係,二元關係可以用圖來建模,如果矩形X可以嵌套在矩形Y裏,就從X到Y練一條有向邊,這個有向圖是無環的,因爲一個矩形無法套在自己內部,所以他就是一個DAG,所以要求的便是DAG上的最長路徑
銀幣問題
分析:
將每種面值看做一個點,表示“還需要湊足的面值”,則初始狀態爲S,目標狀態爲0,若當前在狀態i,每使用一個銀幣j,狀態便轉移到i-Vj,本題的起點必須爲S,終點必須爲0
9.2.2最長路及其字典序
首先思考嵌套矩形,設d(i)表示從結點i出發的最長路長度,第一步只能走到他的相鄰點,因此
d(i)=max{d(j)+1}
其中j是i的相鄰邊。最終答案是所有d(i)中的最大值。首先把圖先建立出來,假設用鄰接矩陣保存矩陣在G中
記憶化搜索程序
int dp(int i){
int& ans=d[i];
if(ans>0) return ans; //d數組以全被初始化爲0
ans=1;
for(int j=1lj<=n;j++)
if(G[i][j])
ans=max(ans,dp(j)+i);
return ans;
}
原題還有一個要求:如有多個最優解,矩形編號的字典序應最小
將所有d值計算出來後,選擇最大的d[i]對應的i,如果有對個i,選擇最小的,程序如下
void print_ans(int i){
printf("%d ",i);
for(int j=1;j<=n;j++)
if(G[i][j]&&d[i]==d[j]+1){
print_ans(j);
break;
}
}
9.2.3固定終點的最長路和最短路
接下來考慮“銀幣問題”。最長路和最短路的求法是類似的,下面只考慮最長路。由於終點固定,d(i)的確切含義變爲“從結點i出發到0結點的最長路徑”
int dp(int S){
int& ans=d[S];
if(ans>=0)
return ans;
ans=0;
for(int i=1;i<=n;i++)
if(S>=V[i])
ans=max(ans,dp(S-V[i])+1);
return ans;
}
此程序有個致命的錯誤,就是結點S不一定真的能到達結點 0,所以需用特殊的d[S]表示“無法到達”
int dp(int S){
int& ans=d[S];
if(ans!=-1)
return ans;
ans=-(1<<30);
for(int i=1;i<=n;i++)
if(S>=V[i])
ans=max(ans,dp(S-V[i])+1);
return ans;
}
在記憶化搜索中,如果用特殊值表示“還沒算過”,則必須將其和其他特殊值區分開
上述錯誤都是很常見的。另一個解法是用一個數組VIS[i]表示狀態i時候訪問過
int dp(int S){
if(vis[S])
return d[S];
vis[S]=1;
int& ans=d[S];
ans=-(1<<30);
for(int i=1;i<=n;i++)
if(S>=V[i])
ans=max(ans,dp(S-V[i])+1);
return ans;
}
本題要求最小,最大兩個值,記憶化搜索必須寫兩個。在這種情況下,用遞推更加方便
minv[0]=maxv[0]=0;
for(int i=1;i<=S;i++){
minv[i]=INF;
maxv[i]=-INF;
}
for(int i=1;i<=S;i++)
for(int j=1;j<=n;j++)
if(i>=V[j]){
minv[i]=min(minv[i],minv[i-V[j]+1);
maxv[i]=max(maxv[i],maxv[i-V[j]+1)'
}
printf("%d %d\n",minv[S],maxv[S]);
//輸出字典序最小的方案
void print_ans(int* d,int S){
for(int i=1;i<=n;i++)
if(S>=V[i]&&d[S]==d[S-V[i]]+1){
printf("%d ",i);
print_ans(d,S-V[i]);
break;
}
}
很多用戶喜歡另外一種打印路徑的方法:遞推時直接用min_coin[S]記錄滿足min[S]->min[S-V[i]]+1的最小i
for(int i=1;i<=S;i++)
for(int j=1;j<=n;j++)
if(i>=V[j]){
if(min[i]>min[i-V[j]]+i){
min[i]=min[i-V[j]]+1;
min_coin[i]=j;
}
if(max[i]<max[i-V[j]]+1){
max[i]=max[i-V[j]]+1;
max_coin[i]=j;
}
}
9.2.4小結與應用程序
例題9-1城市裏的間諜
分析:時間是單向流逝的,是一個天然的“序”。影響到決策的只有當前時間和所處的車站,所以可以用d(i,j)表示時刻i,你在車站j,最少還需要等待多長時間
邊界條件是d(T,n)=0,其他d(T,i)爲正無窮
決策1:等一分鐘
決策2:搭乘往右開的車(如果有)
決策3:搭乘往左邊開的車(如果有)
主過程的代碼如下
for(int i=1;i<=n-1;i++)
dp[T][i]=INF;
dp[T][n]=0;
for(int i=T-1;i>=0;i--)
for(int j=1;j>=n;j++){
dp[i][j]=dp[i+1][j]+1;//等待一個單位
if(j<n&&has_train[i][j][0]&&i+t[j]<=T)
dp[i][j]=min(dp[i][j],dp[i+t[i]][j+1]); //右
if(j>1&&has_train[i][j][1]&&i+t[j-1]<=T)
dp[i][j]=min(dp[i][j],dp[i+t[j-1]][j-1]);//左
}
cout<<"Case Number "<<++kase<< ":";
if(dp[0][1]>=INF)
cout<<"impossible\n";
else
cout<<dp[0][1]<<"\n";
上面代碼中有一個has_train數組,其中has_train[t][i][0]表示時刻t,在車站i是否有往右開的火車,has_train[t][i][1]類似,不過記錄的是往左開的火車