有向无环图上的动态规划是学习动态规划的基础,很多问题都可以转化为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]类似,不过记录的是往左开的火车