前言
首先要知道什么是DAG,有向无环图,可以求拓扑排序,关键路径,在工程规划上有很大的用处。如果发现某个问题给的前提是DAG,那么,根据DAG的无圈性,可以证明其具有最优子结构,就可以在(O(n+e))的复杂度内求得DAG的多元最短(最长)路。而对于所以顶点之间计算最短路我们可以用一般图的Floyd算法,其原理是利用传递闭包的原理每次(E^k=E^{k+1}),相当于路径长度+1,基于动态规划:(dp[i][j]=max(dp[i][k]+dp[k][j],dp[i][j])),复杂度(O(n^3))。而利用DAG的性质我们却可以在(O(ne))完成,原理也是基于动态规划。
DAG最短路
描述
- 首先我我们可以先给图做一次拓扑排序,这样做的原因是要让每次在更新一个点的最短路时可能递推到它的点全部已经计算完毕,拓扑排序就是在干这个事情,不难得出它没有后效性,因为每个点的的最优决策和之前哪边传递过来的没有关系。
- 保证在(O(n+e))的复杂度内的前提是用邻接表存图。
- 用pre表示到达当前点的最短路径的前驱节点
- 用dist表示其他顶点到v点的最短路径长度
- 用ts表示拓扑排序
- 首先path初始化为0,c初始化为INF,c[源点]=0
说点题外话,这里dp状态dist有两个理解方式:
- 前面的点到达当前的最短路(以i点出发的最短路)(固定终点)
- 每个节点传递下去的最短路(以i点结束的最短路)(固定起点)
很显然,状态2那个表示法是一个递归的过程,有个优化手段就是记忆化,而对于类似于某些问题(比如邻接表),容易得到<i, j>的边权而却不容易得到<j, i>的边权(相当于反着枚举)时,我们除了改变建图的数据结构(比如邻接矩阵)、反向建边等还可以采用“刷表法”:就是每走到一个状态时,把它邻接的边(下一个状态)全部传递下去,区别去这种做法的常规做法是填表法。
但是对于状态2在求某些字典序的问题,这个方法却不好求得,这时要采用状态1的设计方式
实现
void shortTest() {
Topsort(edge, ts); //计算拓扑排序存在ts中
for (int i = 0; i < ts.size(); i++) { //刷表法 每次遍历到一个节点把后面的节点刷新
int cur = ts[i]; //当前节点
for (int j = 0; j < edge[cur].size(); j++) {
edge& v = edge[cur][j]; //当前点的邻接节点
if (c[v.to] > c[cur] + v.w) { //更新后继节点
c[v.to] = c[cur] + v.w; //update
pre[v.to] = cur; //记录前驱节点
}
}
}
}
DAG最长路
描述
同DAG最短路类似只需每次更新的时候变动一下条件即可
实现
void shortTest() {
Topsort(edge, ts); //计算拓扑排序存在ts中
for (int i = 0; i < ts.size(); i++) { //刷表法 每次遍历到一个节点把后面的节点刷新
int cur = ts[i]; //当前节点
for (int j = 0; j < edge[cur].size(); j++) {
edge& v = edge[cur][j]; //当前点的邻接节点
if (c[v.to] < c[cur] + v.w) { //更新后继节点 注:这里遇到更大的更新
c[v.to] = c[cur] + v.w; //update
pre[v.to] = cur; //记录前驱节点
}
}
}
}
DAG所有顶点对之间的最短路
描述
同样是根据DAG的图上最优子结构来进行动态规划,核心思路是:利用(dfs)求出每
个邻接点的最短路,把结果保存在一个矩阵(dp[i][j])中,回溯的时候由于子结构已经
计算完毕并且最优,可以根据许多最短路算法一样进行松弛,松弛是利用三角不
等式也就是bellman方程进行更新到其余邻接点的最短路,当然如果不连通显然不能
够更新。
具体过程如下:
- 预处理所有点即dp置为INF,即初始情况下所有点都不连通,当然自己到自己也置为不连通,至于这样做的作用是为了方便判定一个点是否计算过了(我们搜索是基于点的搜索)。当然也可以换成用一个vis标记每个点是否搜索过,只不过这样空间会多出来而已,但是增加了可读性,许多记忆化搜索都用到了这个技巧。
- 每次搜索把(dp[cur][cur])标为0,相当于自己和自己之间没有距离,作用也只是同(vis[cur]=true)一致:该点被搜索过了!
- 枚举邻接点,如果邻接点搜索过了并且能够更新当前到它的直接距离w,则更新它(这个判断的作用是防止之前有当前点的之前搜索过的邻接点把这段距离提前更新了,现在要更新回来),当然,此时也可以记录路径,即用一个矩阵来表示当前节点走最短路的后继点;如果发现没搜索过就递归去处理那个点的最优子结构。
- 最后利用这个邻接点进行松弛其他点,显然能松弛的前提是这个邻接点对其他点已经松弛过了。
- 算法结束
实现
void init() {
memset(dist, inf, sizeof(dist)); //所有点初始时不连通
memset(suf, -1, sizeof(suf)); //所有点初始时没有后继点
}
void dfs(int cur) {
dist[cur][cur] = 0; //自己到自己没有cost,还有个含义表示该点被搜索过
for (int i = 0; i < edge[cur].size(); i++) {
edge& v = edge[cur][i];
if (dist[cur][v] < v.w) { //更新当前点到邻接点的距离
dist[cur][v] = v.w;
suf[cur][v] = v.to; //记录最短路路径
}
if (dist[s][v] == inf) dfs(v.to); //递归搜索子节点 隐含着如果搜索过就不在搜索的记忆化的思想
for (int j = 1; j <= n; j++) //对其他点进行更新
if (dist[v.to][j] < inf) //如果该邻接点对其他点已经更新成连通时就有更新当前点到其他点的距离的可能
if (dist[cur][j] > dist[v.to][j] + v.w) {
dist[cur][j] > dist[v.to][j] + v.w;
suf[cur][j] = v.to; //走这个邻接点的路径更短
}
}
}
void AllshortPaths() {
init();
for (int i = 1; i <= n; i++)
if (dist[i][i] == inf)
dfs(i);
}