DAG
DAG:就是有向无环图。
DAG中的最长路:就是关键路径
本节着重解决两个问题:
1.求整个DAG中的最长路径(即不固定起点或终点)
2.固定终点,求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]|(i,j) ∈ E},如下图所示:
使用递归,邻接矩阵的方式来存储图:
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算法中使用pre数组来保存每个顶点的前驱结点。事实上,可以把这种想法用到这里——开一个int型choice数组记录最长路径上顶点的后继结点。
如果最终可能有多条最长路径,将choice数组改为vector类型的数组即可。
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){
int temp = DP(j) + G[i][j]; //单独计算,防止if中调用
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);
}
}
对一般的动态规划问题而言,如果需要得到具体的最优方案,可以采用类似的方法,即记录每次决策所选择的策略,然后在dp数组计算完毕后根据具体情况进行递归或者迭代来获取方案。
更进一步,模仿字符串来定义路径序列的字典序:如果有两条路径a1→a2→…→am与b1→b2→…→bn,且a1=b1、a2=b2、…、ak=bk、ak+1<bk+1,那么称路径序列a1→a2→…→am的字典序小于路径b1→b2→…→bn。
如果DAG中有多条最长路径,如何选择字典序最小的那条?
只需要遍历i的邻接点的顺序从小到大即可
至此,都是令dp[i]表示从i号顶点出发能获得的最长路径长度。
如果令dp[i]表示以i号结点结尾能获得的最长路径长度,又会有什么结果呢?
可以想象,只要把求解公式变为dp[i] = max{dp[j] + length[j→i]|(j,i)∈E}(相应的求解顺序变成了拓扑序),就可以同样得到最长路径长度,也可以设置choice数组求出具体方案,但却不能直接得到字典序最小的方案,这是为什么呢?
举个简单的例子,如图11-8所示,如果令dp[i]表示从i号顶点出发能获得的最长路径长度,且dp[2]和dp[3]已经计算得到,那么计算dp[1]的时候只需要从V2和V3中选择字典序较小的V2即可;而如果令dp[i]表示以i号顶点结尾能获得的最长路径长度,且dp[4]和dp[5]已经计算得到,那么计算dp[6]时如果选择了字典序较小的V4,则会导致错误的选择结果:理论上应当是V1→V2→V5的字典序最小,可是却选择了V1→V3→V4。
显然,由于字典序的大小总是先根据序列中较前的部分来判断,因此序列中越靠前的顶点,其dp值应当越后计算(对一般的序列型动态规划问题也是如此)。
固定终点,求DAG的最长路径长度
固定终点,求DAG的最长路径长度。例如在图11-6中,如果固定H为路径的终点,那么最长路径就会变成B→D→F→H。
假设规定的终点为T,那么可以令dp[i]表示从i号顶点出发到达终点T能获得的最长路径长度。
同样的,如果从i号顶点出发能直接到达顶点j1、j2、…、jk,而dp[j1]、dp[j2]、…、dp[jk]均已知,那么就有dp[i] = max{dp[j]+length[i→j]|(i,j) ∈ E}
可以发现,这个式子和第一个式子一样——如果这样,显然无法体现dp数组的含义中增加的“到达终点T”的描述。那么这两个问题的区别——边界。在第一个问题中没有固定终点,因此所有出度为0的顶点的dp值为0是边界;但是这个问题中固定了终点,因此边界应当为dp[T]=0。则不能对整个dp数组都赋值为0,就会出现问题,由于从某些顶点出发可能无法到达终点T,因此如果按之前的做法会得到错误的结果。合适做法:初始化dp数组为一个负的大数,来保证“无法到达终点”的含义得以表达;然后设置一个vis数组表示顶点是否已经被计算
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]
}
如果令dp[i]表示以i号顶点结尾能获得的最长路径长度,应当如何处理?
事实上这样设置dp[i]会变得更容易解决问题,并且dp[T]就是结果,只不过仍然不方便处理字典序最小的情况。
矩阵嵌套问题
给出n个矩阵的长和宽,定义矩阵的嵌套关系为:如果有两个矩形A和B,其中矩形A的长和宽分别为a、b,矩形B的长和宽分别为c、d,且满足a<c、b<d,或a<d、b<c,则称矩形A可以嵌套于矩形B内。现在要求一个矩形序列,使得这个序列中任意两个相邻的矩形都满足前面的矩形可以嵌套于后一个矩形内,且序列的长度最长。如果有多个这样的最长序列,选择矩形编号序列的字典序最小的那个。
这个例子就是典型的DAG最长路问题——将每个矩形都看成一个顶点,并将嵌套关系视为顶点之间的有向边,边权均为1,于是就可以转换为DAG最长路问题。