动态规划-DAG最长路
DAG就是有向无环图,并且在之前已经讨论了如何求解DAG中的最长路,也就是所谓的”关键路径“。但是求解关键路径的做法对初学者来说确实有些复杂,而DAG上的最长路或者最短路问题又是特别重要的一类问题,很多问题都可以转换成求解DAG上的最长或最短路径问题,因此有必要介绍一下更简便的方法,也就是使用本节介绍的方法。由于DAG最长路和最短路的思想是一致的,因此下面以最长路为例。
本节着重解决两个问题:
- 求整个DAG中的最长路径(即不固定起点跟终点)
- 固定终点,求DAG的最长路径。
先讨论第一个问题:**给定一个有向无环图,怎样求解整个图的所有路径中权值之和最大的那条。**如图11-6所示,B->D->F->I就是该图的最长路径,长度为9。
针对这个问题,令dp[i]表示从i号顶点出发能获得的最长路径长度,这样所有dp[i]的最大值就是整个DAG的最长路径长度。
那么怎么求解dp数组呢?注意到dp[i]表示从i号顶点出发能获得的最长路径长度,如果从i号顶点出发能直接到达顶点j1,j2,…jk,而dp[j1],dp[j2],…,dp[jk]均已知,那么就有dp[i] = max{dp[j] + length[i->j]}
显然,根据上面的思路,需要按照逆拓扑排序来求解dp数组(想一想,为什么?)。但是有没有不求出逆拓扑序列也能计算dp数组的方法呢?当然有,那就是递归。请看下面的代码,其中图使用邻接矩阵的方式存储。
int DP(int i){
if(dp[i] > 0) return dp[i]; // dp[i]已计算得到
for(int j = 0 ; j < n ; j++){ // 遍历i的所有出边
if(G[i][j] != INF){
dp[i] = max(dp[i],DP(j)+G[i][j]);
}
}
return dp[i]; // 返回计算完毕的dp[i]
}
由于从出度为0的顶点出发的最长路径长度为0,因此边界为这些顶点的dp值为0。但具体实现中不妨对整个dp数组初始化为0,这样dp函数当前访问的顶点i的出度为0时就会返回dp[i] = 0 (以此作为dp的边界),而出度不是0的顶点则会递归求解,递归过程中遇到已经计算过的顶点则直接返回对应的dp值,于是从程序逻辑上按照了逆拓扑序列的顺序进行。希望读者能认真体会上面代码的思想。
那么,如何知道最长路径具体是哪条呢?
回忆在Dijkstra算法中是如何求解最短路径的。-开了一个int型数组pre,来记录每个顶点的前驱,每当发现更短的路径时对pre进行修改。事实上,可以把这种想法应用于求解最长路径上-开一个int型,choice数组记录最长路径上顶点的后继顶点,这样就可以像Dijkstra算法中那样来求解最长路径了,只不过由于choice数组存放的是后继结点,因此使用迭代即可(当然使用递归也是可以的),如下面的代码所示。如果最终可能有多条最长路径,将choice数组改为vector类型的数组即可(也就是Dijkstra算法中有多条最短路径时的做法),代码留给读者实现。读者可以顺便思考一下如何求解最长路径的条数。
int DP(int i){
if(dp[i] > 0) return dp[i]; // dp[i]已经得到
for(int j = 0 ; j < n ; j++){
if(G[i][j] != INF){
int temp = DP(j) + G[i][j]; // 单独计算,防止if中调用DP函数两次
if(temp > dp[i]){
dp[i] = temp; // 覆盖dp[i]
choice[i] = j; // 从i号顶点的后继顶点是j
}
}
}
return dp[i]; // 返回计算完毕的dp[i]
}
// 调用printPath前需要先得到最大的dp[i],然后将i作为路径起点传入
void printPath(int i){
printf("%d",i);
while(choice[i] != -1){// choice数组初始化为-1
i = choice[i];
printf("->%d",i);
}
}
在上面讨论的基础上,接下来讨论本节开头的第二个问题:固定终点,求DAG得最长路径长度。例如在图11-6中,如果固定H为路径的终点,那么最长路径就会变成B->D->F->H。
int DP(int i){
if(vis[i]) return dp[i]; // dp[i]已计算得到
vis[i] = true;
for(int j = 0 ; j < n ; j++){
if(G[i][j] != INF){
dp[i] = max(dp[i],DP(j)+G[i][j]);
}
}
return dp[i]; // 返回计算完毕的dp[i]
}