《算法笔记》读书记录DAY_57

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记录路径递归求解可得。

猜你喜欢

转载自blog.csdn.net/jgsecurity/article/details/121309482