CHAPTER_11 提高篇(5)——动态规划
11.6 DAG最长路径
在10.6.1节已经介绍过,DAG就是有向无环图,并且在10.7.3节中已经讨论如何求解DAG中的最长路径,也就是“关键路径”的求法。这里介绍利用动态规划来求DAG最大路径的做法。
本节着重解决两个问题:
(1)求解整个DAG中的最长路径(即不固定起点和终点)。
(2)固定终点,求DAG的最长路径。
先讨论第一个问题:给定一个有向无环图,怎样求解整个图中的所有路径权值之和最大的那一条。
针对这个问题,令dp[i]表示从 i 号顶点出发能获得的最长路径长度,这样dp数组中的最大值就是DAG的最大路径长度。
那如何求解dp数组呢?注意到dp[i]表示从 i 号顶点出发能获得的最长路径长度,如果从 i 号顶点出发能直接到达顶点j1、j2、...、jk。,而d[j1]、d[j2]、...、d[jk]均已知。那么就有dp[i]=max(dp[j]+length(dp[i-j]) )。如下图:
与图论中的关键路径求法的思想类似,动态规划的算法也需要使用逆拓扑序列来求解dp数组。但是有没有不使用拓扑排序的方法呢?当然有,那就是递归。请看下面的代码,图使用邻接矩阵存储:
int DP(int i) {
if(dp[i]>0) //dp[i]已经计算得到
return 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];
}
由于从出度为0的顶点出发的最长路径长度为0,因此边界为出度为0的顶点的dp值为0。但具体实现中不妨对整个dp数组初始化为0,这样dp函数当前访问的顶点 i 的出度为0时就会返回dp[i]=0,出度不是0的顶点会进入递归求解,对于已经求过的dp会直接返回。
那么,我们既已求得最长路径长度,如何得知最长路径具体是哪一条呢?
回忆Dijkstra算法中我们使用了pre数组来记录最短路径中每个顶点的前驱,每当发现更短路径时对pre做修改。同样地,我们可以开一个数组choice记录最长路径上顶点的后继顶点,由于存放的是后继顶点,我们要使用迭代或者递归求解。如果有多条最长路径,我们要使用vector数组来记录每个顶点的多个后继顶点。代码如下:
int dp[maxn];
vector<int> choice[maxn];
vector<int> path; //记录每条最大路径
//计算dp数组,并生成最大路径choice数组
int DP(int i) {
if(dp[i]>0)
return dp[i];
for(int j=0;j<n;j++) {
if(G[i][j]!=INF) {
int temp=DP(j)+G[i][j];
if(temp>dp[i]) {
dp[i]=temp;
choice[i].clear();
choice[i].push(j);
}
else if(temp==dp[i]) {
choice[i].push(j);
}
}
}
return dp[i];
}
//调用printPath前需要先得到最大的dp[i],然后将i作为起点传入
void printPath(int i) {
if(dp[i]==0) { //如果到达了边界顶点(出度为0的顶点)
path.push_back(i); //将顶点i加入路径最后面
for(int j=0;j<path.size();j++) { //输出整条最大路径
路径
cout<<path[j]<<' ';
}
path.pop_back(); //处理完后将刚加入的顶点弹出
return;
}
path.push_back(i);
for(int j=0;j<choice[i].size();j++) { //遍历i的所有后继
printPath(choice[i][j]); //向后继递归
}
path.pop_back(i); //处理完后将刚加入的顶点弹出
}
下面讨论第二个问题:固定终点,求DAG中的最大路径长度。
类似地,令dp[i]表示从 i 号顶点出发到达终点 t 的最大路径长度。同样地,如果从 i 号顶点出发能直接到达顶点j1、j2、...、jk。,而d[j1]、d[j2]、...、d[jk]均已知。那么就有dp[i]=max(dp[j]+length(dp[i-j]) )。
但是,这个dp和第一个问题的dp是有区别的,区别体现在哪里呢?答案就是边界。第一个问题没有固定终点,因此所有出度为0的点都是边界。而此处只有终点 t 为边界,边界应当为dp[t]=0。因此不能再像之前那样将所有dp初始化为0。我们要将dp初始化为一个负的大数,来保证“无法到达终点”这种情况。然后设置一个bool数组vis表示顶点是否被计算。代码如下:
int DP(int i) {
if(vis[i]) //dp[i]已经计算得到
return 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数组中最大的值就是最大路径和。如果要求到终点 t 的最大路径,方法和第一个问题一样,设置一个int型数组choice(或者vector数组)来记录后继,然后使用path记录路径递归求解可得。