【概述】
DAG 图的最长路问题是一个比较少见的问题,具体问题是:给出一个 DAG 图,寻找图中的最长路
在 AOE 网中,在找出关键路径后,对其进行 DFS 即可得到图的最长路,由于这种方法的实现过于繁琐,这里介绍几种较为简单的实现。
【最短路算法】
对于最短路算法,Floyd,Dijkstra、Bellman-Ford、SPFA 等,将其松弛操作进行修改,即可将最短路算法变为最长路算法。
以 Floyd 为例:
int G[N][N];
void Floyd(){
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(i!=k&&j!=i&&j!=k)
if(g[i][j]<g[i][k]+g[k][j])
g[i][j]=g[i][k]+g[k][j];
}
int main(){
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
scanf("%d",&g[i][j]);
Floyd();
int res=-INF;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
res=max(g[i][j],res);
printf("%d\n",res);
return 0;
}
【拓扑排序】
在拓扑排序的过程中,不断记录路径,最后对路径进行排序,输出最大的那个即为 DAG 的最长路
struct Node{
int to,dis;
Node(){}
Node(int to,int dis):to(to),dis(dis){}
};
vector<Node> G[N];
int in[N];
int dis[N];
int n,m;
void topSort() {
stack<int > S;
for(int i=1; i<=n; i++)
if(!in[i])
S.push(i);
while(!S.empty()) {
int x=S.top();
S.pop();
for(int j=0; j<G[x].size(); j++) {
int y=G[x][j].to;
dis[y]=max(dis[y],dis[x]+G[x][j].dis);
in[y]--;
if(!in[y])
S.push(y);
}
}
}
int main() {
int T;
scanf("%d",&T);
while(T--){
scanf("%d%d",&n,&m);
memset(in,0,sizeof(in));
memset(dis,0,sizeof(dis));
for(int i=0; i<=n; i++)
G[i].clear();
for(int i=1; i<=m; i++) {
int x,y,dis;
scanf("%d%d%d",&x,&y,&dis);
Node temp;
temp.to=y;
temp.dis=dis;
in[y]++;
G[x].push_back(temp);
}
topSort();
int res=-INF;
for(int i=1;i<=n;i++)
res=max(res,dis[i]);
printf("%d\n",res);
}
return 0;
}
【动态规划】
1.不固定终点起点
当给定一个 DAG 图时,要求整个图中所有路径中权值和最大的那条,即不固定终点和起点问题。
设 dp[i] 为从 i 点出发能获得的最长路径长度,G[i][j] 为从 i 点到 j 点的距离,这样所有的 dp[i] 的最大值就是整个 DAG 的最长路径长度,如果从 i 点出发,能直接到达顶点 j1、j2、...、jk,而 dp[j1]、dp[j2]、...、dp[k] 均已知,那么有:dp[i]=max{ dp[j]+G[i][j] }
根据上面的思路,由于最后的顶点没有出边,因此需要按照逆拓扑排序来求解 dp 数组,但可以利用递归来进行求解:由于从出度为 0 的顶点出发的最长路径长度为 0,因此边界就是这些点,在具体实现中不妨对整个 dp 数组初始化为 0,这样 DP 函数当前访问顶点i的出度为0时就会直接返回 dp[i]=0,而出度不为 0 的时候就会递归求解,递归过程中遇到已经计算过的顶点则直接返回对于的 dp 值,于是从逻辑上实现了逆拓扑排序的效果。
其基于邻接矩阵实现的代码如下:
int dp[N];//使用前整个数组设为0
int G[N][N];
int DP(int i) {
if(dp[i]>0)
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];
}
当需要输出这条最长路时,可以利用一个 next 数组来记录 i 顶点的后继结点,再求完最长路后,顺序打印路径即可
int DP(int i) {
if(dp[i]>0)
return dp[i];
for(int j=0; j<n; j++) { //遍历i的所有可达出边
if(G[i][j]!=INF) {
int temp=DP(j)+G[i][j];//单独计算dp
if(dp[i]<temp) { //可以获得更长的路径
dp[i]=temp;
next[i]=j; //保存i的后继顶点j
}
}
}
return dp[i];
}
void printPath(int i) {//调用前需先获得最大的dp[i],然后将i作为路径的起点传入
printf("%d",i);
while(next[i]!=-1) { //next数组初始化为-1
i=next[i];
printf("->%d",i);
}
printf("\n");
}
2.固定终点起点
给定一个 DAG 图,给出一个起点和终点,要求从起点到终点的路径中权值和最大的那条,即固定终点和起点问题。
设规定的终点为 T,那么设 dp[i] 为从 i 号点出发到达终点 T 所能获得的最大长度,G[i][j] 为从 i 点到 j 点的距离,如果从 i 点出发,能直接到达顶点 j1、j2、...、jk,而 dp[j1]、dp[j2]、...、dp[k] 均已知,那么有:dp[i]=max{ dp[j]+G[i][j] }
可以发现,这个 dp 式子与上面不固定终点起点的问题相同,但问题的区别在于边界:
第一个问题中,没有固定的终点,因此边界为所有出度为 0 的顶点,其 dp 值为 0
第二个问题中,固定了终点,因此边界应当为 dp[T]=0,需要注意的是,由于从某顶点出发可能会无法到达终点 T,因此此时 dp 数组不能再全部初始化为 0,比较合适的做法是将 dp 初始化为一个极大的负数(-INF),来表达无法到达终点,然后设置一个 vis 数组来表示顶点是否已被访问
int vis[N];
int G[N][N];
int dp[N];//使用前初始化为-INF,且终点dp[T]=0
int DP(int i) {
if(vis[i])
return dp[i];
vis[i]=true;
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];
}