题目描述
有n个车站,编号1到n。从1号车站开往n号车站的列车有M1趟,发车时间为。从n号车站开往1号车站的列车有M2趟,发车时间类似。列车驶过每两个车站间的用时为。现在有一个间谍,0时刻在车站1,他要在T时刻到达车站n。求间谍在车站逗留的最短时间。
题解
一道动态规划的好题。自己初学动态规划,结合该题记录一下自己的感悟。
动态规划中有几个概念:
- 阶段:把所给求解问题的过程恰当地分成若干个相互联系的阶段。阶段间的联系一般不是简单的线性关系,并且解决问题的阶段可能在代码中体现的十分抽象。
- 状态:状态表示每个阶段开始面临的自然状况或客观条件,它是某个阶段的出发位置。明确状态是解决动态规划问题的一个重点。
- 决策:一个阶段的状态给定以后,从该状态演变到下一阶段某个状态的一种选择(行动)称为决策。通常,决策有很强的实际意义。在明确了状态后,分析出问题的决策是写出状态转移方程的关键。
- 最优化原理(最优子结构性质):一个最优化策略具有这样的性质,不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。最优化原理是确定决策的理论和要求,它是整个过程的最优策略(注意与贪心策略区分)。通常为选取几个策略中的最大值或最小值。同时,从子问题子结构的角度考虑,可以帮助我们找出问题的状态。
- 无后效性:将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。无后效性是贪心问题与动态规划的本质区别。
回到本题。
通过简单模拟该题的过程可以发现几个很重要的状态:在某个时间、在某个车站、是否在车上。但考虑到最终只是要求在时间T到达车站n,且上下车的时间不计。所以是否在车上这个状态可以忽略。
那么本题的状态只有两个:时间 i 与车站 j ,则设表示在时间 i 和 车站 j 时还需等待的最短时间。
考虑这两状态,那么决策有三个:
决策1:等待一分钟,
决策2:乘坐向左开的车(如果有),
决策3:乘坐向右开的车(如果有),
那么状态转移方程就是在这三个决策中选择一个最小的。
有了状态和状态转移方程后便可代码实现。代码实现有两种方案可选,具体如下:
1. 递推
如果使用递推,我们就必须自己划分阶段,确定计算顺序。观察决策可以发现,状态(i,j)都是由状态(l,k)转移而来,其中 i < l。这就要求必自底向上计算。核心代码如下:
memset(dp, 0, sizeof(dp));
for(int i = 1; i < n; 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[j]][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]);
}
2.记忆化搜索
记忆化搜索隐含了计算顺序。核心代码如下:
int solve(int i, int j){
if(vis[i][j]) return dp[i][j];
vis[i][j] = true;
if(i==T){
if(j == n) return dp[i][j] = 0;
else return dp[i][j] = INF;
}
int a,b = INF, c = INF;
a = solve(i+1, j)+1;
if(j<n && has_train[i][j][0] && i+t[j] <= T)
b = solve(i+t[j], j+1);
if(j>1 && has_train[i][j][1] && i+t[j-1] <= T)
c = solve(i+t[j-1], j-1);
return dp[i][j] = min(a,min(b,c));
}
AC代码
1.递推
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 300;
const int INF = 0x3f3f3f3f;
int t[maxn],d[maxn], e[maxn];
int dp[maxn][maxn];
bool has_train[maxn][maxn][2];
int main(){
int n,T,m1,m2, kase = 0;
while(scanf("%d", &n) != EOF && n){
memset(has_train, false, sizeof(has_train));
scanf("%d", &T);
for(int i = 1; i < n; i++){
scanf("%d", t+i);
}
scanf("%d", &m1);
for(int i = 0; i < m1; i++){
scanf("%d", d+i);
for(int j = 1,tt = d[i]; j <= n; j++){
has_train[tt][j][0] = true;
tt += t[j];
}
}
scanf("%d", &m2);
for(int i = 0; i < m2; i++){
scanf("%d", e+i);
for(int j = n, tt = e[i]; j>0; j--){
has_train[tt][j][1] = true;
tt += t[j-1];
}
}
memset(dp, 0, sizeof(dp));
for(int i = 1; i < n; 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[j]][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]);
}
printf("Case Number %d: ", ++kase);
if(dp[0][1] >= INF) printf("impossible\n");
else printf("%d\n", dp[0][1]);
}
}
2.记忆化搜索
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 300;
const int INF = 0x3f3f3f3f;
int t[maxn],d[maxn], e[maxn];
int dp[maxn][maxn];
bool has_train[maxn][maxn][2];
bool vis[maxn][maxn];
int n,T,m1,m2, kase = 0;
int solve(int i, int j){
if(vis[i][j]) return dp[i][j];
vis[i][j] = true;
if(i==T){
if(j == n) return dp[i][j] = 0;
else return dp[i][j] = INF;
}
int a,b = INF, c = INF;
a = solve(i+1, j)+1;
if(j<n && has_train[i][j][0] && i+t[j] <= T)
b = solve(i+t[j], j+1);
if(j>1 && has_train[i][j][1] && i+t[j-1] <= T)
c = solve(i+t[j-1], j-1);
return dp[i][j] = min(a,min(b,c));
}
int main(){
while(scanf("%d", &n) != EOF && n){
memset(has_train, false, sizeof(has_train));
scanf("%d", &T);
for(int i = 1; i < n; i++){
scanf("%d", t+i);
}
scanf("%d", &m1);
for(int i = 0; i < m1; i++){
scanf("%d", d+i);
for(int j = 1,tt = d[i]; j <= n; j++){
has_train[tt][j][0] = true;
tt += t[j];
}
}
scanf("%d", &m2);
for(int i = 0; i < m2; i++){
scanf("%d", e+i);
for(int j = n, tt = e[i]; j>0; j--){
has_train[tt][j][1] = true;
tt += t[j-1];
}
}
memset(dp, 0, sizeof(dp));
memset(vis, false, sizeof(vis));
solve(0,1);
printf("Case Number %d: ", ++kase);
if(dp[0][1] >= INF) printf("impossible\n");
else printf("%d\n", dp[0][1]);
}
}