DAG最长路(动态规划DP)

之前介绍了DAG有向无环图以及拓扑排序,如何求解DAG的最长路,也就是所谓的“”关键路径”,但求解关键的路径的做法对初学者来说实在不易。因此下面介绍一种简便的方法,来求解DAG最长路(最短路的思想是一致的)。

下面着重解决两个问题:

(1)求整个DAG的最长路径(即不固定起点和终点)

(2)固定终点,求DAG的最长路径

首先讨论第一个问题:给定一个有向无环图,怎样求解整个图的所有路径中权值之和最大的那条。针对这个问题令dp[i]表示从i顶点出发能获得的最长路径长度,这样所有的d[i]的最大值就是整个DAG的最长路径长度。

那怎么求解dp数组呢?注意到dp[i]表示从i顶点出发能获得的最长路径长度,如果从i号顶点出发能直接到达顶点j1,j2…jk,而dp[j1]…dp[jk]均已知。那么就有

                        dp[i]=max{dp[j]+length[i->j](i,j)∈E} 

显然,根据上面的思路需要按照逆拓扑排序来求解dp数组(因为最后的顶点没有出边),但有木有办法不求逆拓扑排序也能计算dp数组呢?当然有,那就是递归。其基于邻接矩阵实现的代码如下:

int DP(int i){
	if(dp[i]>0) return dp[i];
	for(int j=0;j<n;j++){ //遍历i的所有可达出边 
		if(map[i][j]!=Inf){
			dp[i]=max(dp[i],DP(j)+map[i][j]); 
		} 
	}
	return dp[i]; 
}

由于从出度为0的顶点出发的最长路径长度为0,因此边界就是这些点,但在具体实现中不妨对整个dp数组初始化为0。这样DP函数当前访问顶点i的出度为0时就会直接返回dp[i]=0(以此为边界),而出度不为0的时候就会递归求解,递归过程中遇到已经计算过的顶点则直接返回对于的dp值,于是从逻辑上实现了逆拓扑排序的效果。
那如果记录这条路径呢?回忆下求使用Dikskstra求解最短路径的在优化顶点v时候,使用了一个pre数组来保存v的前驱结点u。事实上,可以把这种想法应用于求解最长路径上,使用一个next数组保存i顶点的后继顶点,此时只需要顺序地打印路径即可。

int DP(int i){
	if(dp[i]>0) return dp[i];
	for(int j=0;j<n;j++){ //遍历i的所有可达出边 
		if(map[i][j]!=Inf){
			int temp=DP(j)+map[i][j];//单独计算dp 
			if(dp[i]<temp){//可以获得更长的路径 
				dp[i]=temp; 
				next[i]=j; //保存i的后继顶点j 
			}
		} 
	}
	return dp[i]; 
}
 
//调用前需先获得最大的dp[i],然后将i作为路径的起点传入 
void printPath(int i){
	cout<<i;
	while(next[i]!=-1){//next数组初始化为-1
		 i=next[i];
		 cout<<"->"<<i; 
	} 
} 

接下来再看看问题(2):固定终点,求DAG的最长路径,有了上面的经验,应当能很容易想到这个问题延伸问题的解决方案。假设规定的终点为T。那么可以令dp[i]表示从i号顶点出发到达终点T能获得的最长路径长度。,如果从i号顶点出发能直接到达顶点j1,j2…jk,而dp[j1]…dp[jk]均已知,那么就有dp[i]=max{dp[j]+lengthi->j∈E} 。可以发现怎么和问题(1)的式子是一样的。如果仅仅是这样就无法体现出dp数组的含义中添加的“到达终点T的描述”。

那么这两个问题的区别在哪呢?没错,边界。在第一个问题中没有固定的终点,因此所有出度为0的顶点dp值为0是边界;但在这个问题中固定了终点,因此边界应当为dp[T]=0。那么还能像之前那样对整个dp数组初始化为0?不行,此处会有个问题,由于从某顶点出发可能无法到达终点T(例如出度为0的顶点),因此按照之前的做法出度为0的顶点到T的最长路径长度为0,这显然是不符合逻辑的。合适的做法是初始化dp数组为一个负的大数,来保证“”无法到达终点”的含义得以表达(即-Inf,消除其他出度为0的顶点对前驱结点的最长距离的干扰);然后设置一个vis数组表示顶点是否已经被计算(问题1不用设置是因为dp的初始值为0,而问题2的dp初始值为-Inf), 代码如下:

int DP(int i){
	if(vis[i]) return dp[i]; //dp[i]已经计算得到
	vis[i]=true;
	for(int j=0;j<n;j++){ //遍历i的所有可达出边 
		if(map[i][j]!=Inf){
			dp[i]=max(dp[i],DP(j)+map[i][j]);
		} 
	}
	return dp[i]; 
}

—最后看一个经典的矩阵嵌套问题
题目描述
有n个矩形,每个矩形可以用a,b来描述,表示长和宽。矩形X(a,b)可以嵌套在矩形Y(c,d)中当且仅当a<c,b<d或者b<c,a<d(相当于旋转X90度)。例如(1,5)可以嵌套在(6,2)内,但不能嵌套在(3,4)中。你的任务是选出尽可能多的矩形排成一行,使得除最后一个外,每一个矩形都可以嵌套在下一个矩形内。
输入
第一行是一个正正数N(0<N<10),表示测试数据组数,
每组测试数据的第一行是一个正正数n,表示该组测试数据中含有矩形的个数(n<=1000)
随后的n行,每行有两个数a,b(0<a,b<100),表示矩形的长和宽
输出
每组测试数据都输出一个数,表示最多符合条件的矩形数目,每组输出占一行
样例输入
1
10
1 2
2 4
5 8
6 10
7 9
3 1
5 8
12 10
9 7
2 2
样例输出
5

分析:将每个矩阵都视为一个顶点,并将嵌套关系视为顶点和顶点之间的有向边,边权皆为1,于是就转换成了DAG最长路问题了。

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
 
const int M=1002;
int map[M][M],dp[M],n;
 
struct Rec{
	int x,y;
}rec[M]; 
 
int DP(int i){
	if(dp[i]>0) return dp[i];
	for(int j=0;j<n;j++){ //遍历i的所有可达出边 
		if(map[i][j]){
			dp[i]=max(dp[i],DP(j)+map[i][j]); 
		} 
	}
	return dp[i]; 
}
 
//检查a<c,b<d或者b<c,a<d
int check(Rec r1,Rec r2){
	if(r1.x<r2.x&&r1.y<r2.y){
		return 1;
	}	
	if(r1.x<r2.y&&r1.y<r2.x){
		return 1;
	}
	return 0; 
} 
 
int main(){
	int i,m,j;
	cin>>m;
	while(m--){
		memset(dp,0,sizeof(dp));
		memset(map,0,sizeof(map)); 
		cin>>n;
		for(i=0;i<n;i++){
			cin>>rec[i].x>>rec[i].y;
		}
		for(i=0;i<n;i++){
			for(j=0;j<n;j++){
				if(i!=j) //设置边的关系,0为无边 
					map[i][j]=check(rec[i],rec[j]);	
			}
		}
		DP(0);
		int max_=-1;
		for(i=0;i<n;i++){
			if(max_<dp[i])max_=dp[i];
		}
		cout<<max_+1<<endl; //顶点数为边数 
	}
} 

题目来源:http://codeup.cn/problem.php?cid=100000630&pid=0

作者:PJ-Javis
来源:CSDN
原文:https://blog.csdn.net/jiangpeng59/article/details/56666903

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值