一 . 动态规划算法
适用于求解多阶段决策过程中的最优化问题,必须满足最优化原理、无后效性和重叠性。使用动态规划只需要多项式时间复杂度,因此它比回溯法、暴力法等要快许多。
- 划分阶段:
- 求解最短路径:
- 过程指标函数基本方程(和):
- Javascript实现算法:
/** * 示例:s = { * 4:{s:{C1:{T:3},C2:{T:4}}}, * 3:{s:{B1:{C1:1,C2:4},B2:{C1:6,C2:3},B3:{C1:3,C2:3}}}, * 2:{s:{A1:{B1:7,B2:4,B3:6},A2:{B1:3,B2:2,B3:4},A3:{B1:4,B2:1,B3:5}}}, * 1:{s:{Q:{A1:2,A2:4,A3:3}}}, * } * * DynamicProgramming(4, s) * * 11: * Q->A2->B1->C1->T; * Q->A3->B1->C1->T; * Q->A3->B2->C2->T; * * @param int k 阶段数 * @param object s 阶段状态集 * @return string */ function DynamicProgramming(k, s) { let indicators = {},//过程指标函数集声明 k0 = k,//阶段数登记 f,//当前k子过程指标函数声明 v;//阶段指标函数声明 for( ; k > 0; k--) { f = {}; for(let i in s[k].s) { f[i] = {//当前k子过程函数在状态s = i时 optValue:0,//最优值 opt:[],//最佳决策 }; for(let j in s[k].s[i]) { v = indicators[k+1] ? indicators[k+1][j].optValue : 0; v = s[k].s[i][j] + v; if (!f[i].optValue || v < f[i].optValue) { f[i].optValue = v; f[i].opt = [j]; } else if (v == f[i].optValue) { f[i].opt.push(j); } } } indicators[k] = f; } return optPath();//输出最后结果 function optPath(k = 0, s = '', preEquivalentPath = '') {//k:阶段数,s:状态,preEquivalentPath:等价路径前缀 let results = '';//结果集初始化 if (k == 0) {//结果集包含最优值 for (let i in indicators[1]) { results += indicators[1][i].optValue + ': ' + optPath(1, i); } } else if (k > k0) {//结果集包含最优路径终点 results = preEquivalentPath + s + '; '; } else {//结果集包含所有最优路径 preEquivalentPath += s + '->'; for (let i in indicators[k][s].opt) { results += optPath(k + 1, indicators[k][s].opt[i], preEquivalentPath); } } return results; } }
二. Floyd算法
Floyd算法又称为插点法,是利用动态规划思想求出它的每两点间的最短路径的算法。 Floyd算法时间复杂度为O(n^3),空间复杂度为O(n^2)。
- 算法描述:把图用邻接矩阵G表示出来,如果Vi到Vj的边存在,则G[i,j]=该边的权,否则G[i,j]=无穷大;另外,如果Vi、Vj为同一点,则权为0。定义一个矩阵Path用来记录路径信息,Path[i,j]表示从Vi到Vj需要经过的点,初始时Path[i,j] = j。对于任一点Vk,取G[i,j] = min( G[i,j], G[i,k]+G[k,j] ),如果G[i,j]的值变小,则Path[i,j] = Path[i,k]
- 状态转移方程:
G[i,j] = min{G[i,k] + G[k,j], G[i,j]}
Path[i,j] = Path[i,k] - 适用范围:
1. 适用于APSP(All Pairs Shortest Paths,多源最短路径) 2. 稠密图效果最佳 3. 边权可正可负
- Javascript实现算法:
/** * 示例:五个端点按照图的顺序构成一条路经,其5x5邻接矩阵如下: * g = [ * [0,100,1,100,100], * [100,0,100,100,4], * [1,100,0,2,100], * [100,100,2,0,3], * [100,4,100,3,0], * ] * Floyd(g) * * 0-1(1):0->1; * 0-2(3):0->1->2; * 0-3(6):0->1->2->3; * 0-4(10):0->1->2->3->4; * 1-0(1):1->0; * 1-2(2):1->2; * 1-3(5):1->2->3; * 1-4(9):1->2->3->4; * 2-0(3):2->1->0; * 2-1(2):2->1; * 2-3(3):2->3; * 2-4(7):2->3->4; * 3-0(6):3->2->1->0; * 3-1(5):3->2->1; * 3-2(3):3->2; * 3-4(4):3->4; * 4-0(10):4->3->2->1->0; * 4-1(9):4->3->2->1; * 4-2(7):4->3->2; * 4-3(4):4->3; * * @param array g nxn邻接矩阵 * @return string */ function Floyd(g) { let n = g.length,//获取端点的数目 paths = new Array(n);//最短路径初始化
for (let i = 0; i < n; i++) { paths[i] = new Array(n); for (let j = 0; j < n; j++) { paths[i][j] = j; } } for (let k = 0; k < n; k++) {//求邻接矩阵的最短路径距离及最短路径集 for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { if (g[i][k] + g[k][j] < g[i][j]) { g[i][j] = g[i][k] + g[k][j]; paths[i][j] = paths[i][k]; } } } } let optPaths = '',
post; for (let i = 0; i < n; i++) {//获取最短路径集 for (let j = 0; j < n; j++) { if (i == j) { continue; } else { optPaths += i + '-' + j + '(' + g[i][j] + '):'; post = i; while (post != j) { optPaths += post + '->'; post = paths[post][j]; } optPaths += j + '; '; } } } return optPaths; }将“求邻接矩阵的最短路径距离及最短路径集”部分代码改成:
for (let i = 0; i < n; i++) {//求邻接矩阵的最短路径距离及最短路径集 for (let j = 0; j < n; j++) { for (let k = 0; k < n; k++) { if (g[i][k] + g[k][j] < g[i][j]) { g[i][j] = g[i][k] + g[k][j]; paths[i][j] = paths[i][k]; } } } }
将出现意外结果如下:
0-1(100):0->1; 0-2(1):0->2; 0-3(3):0->2->3; 0-4(6):0->2->3->4; 1-0(100):1->0; 1-2(100):1->2; 1-3(7):1->4->3; 1-4(4):1->4; 2-0(1):2->0; 2-1(100):2->1; 2-3(2):2->3; 2-4(5):2->3->4; 3-0(3):3->2->0; 3-1(7):3->4->1; 3-2(2):3->2; 3-4(3):3->4; 4-0(6):4->3->2->0; 4-1(4):4->1; 4-2(5):4->3->2; 4-3(3):4->3;
三. Dijkstra算法
Dijkstra(迪杰斯特拉)算法是典型的单源最短路径算法,用于计算一个节点到其他所有节点的最短路径,主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止,该算法要求图中不存在负权边。
- 算法描述:
G = {V, E},V0为源点
1. 初始时S = {V0},U = V - S = {其余顶点},同时S和U也维护各点Vi到V0的距离(下简称Vi的距离,V0的距离为0)。若不存在<V0,Vi>,则距离为无穷大;若存在<V0,Vi>,则Vi的距离为该边的权值
2. 从U中选取一个距离最小的Vk,加入到S中
3. U中余下顶点中若有以Vk作中间顶点,使得从V0到Vk再到Vi的距离之和小于U所标记的Vi的距离,则以前者重新标记U中Vi的距离
4. 重复步骤2、3,直到S中包含所有顶点 - 适用范围:
1. 单源(同一个源点) 2. 不存在负权边
- Javascript实现算法:
/** * 示例:五个端点按照图的顺序构成一条路经,其5x5邻接矩阵如下: * g = [ * [0,100,1,100,100], * [100,0,100,100,4], * [1,100,0,2,100], * [100,100,2,0,3], * [100,4,100,3,0], * ] * Dijkstra(g) * * * @param array g nxn邻接矩阵 * @return string */ function Dijkstra(g, infinity = 100) { let n = g.length,//获取顶点的数目 infinityPlus1 = infinity + 1,//端点从g[0]数组中删除标识 s = Array(n);//记录各端点相对于V0的最短路径值及前驱 for (let i = 0; i < n; i++) { s[i] = [infinity, 0]; }
let mPath,
k; for (let i = 0; i < n; i++) {//求解k点最短路径,并从g[0]中删除它 mPath = minPath(); k = mPath[0]; s[k][0] = mPath[1]; g[0][k] = infinityPlus1;//标识该端点从g[0]数组中删除 for (let j = 0; j < n; j++) {//查询并处理以k为前驱的j点 if (g[0][j] != infinityPlus1 && s[k][0] + g[k][j] < g[0][j]) { g[0][j] = s[k][0] + g[k][j]; s[j][1] = k; } } } return optPaths();//返回最短路径集 function minPath() {//输出g[0]中的最小路径 let path = [infinity, infinity]; for (let i = 0; i < n; i++) { if (g[0][i] < path[1]) { path[0] = i; path[1] = g[0][i]; } } return path; } function optPaths() {//输出最短路径集 let paths = '',
paths_i,
preNode;
for (let i = 1; i < n; i++) { paths_i = ''; preNode = i; while (preNode) { paths_i = '->' + preNode + paths_i; preNode = s[preNode][1]; } paths_i = '0' + paths_i; paths += '0->' + i + '(' + s[i][0] + '): ' + paths_i + '; '; } return paths; } }
四. Bellman-Ford算法
Bellman-Ford算法能在更普遍的情况下(存在负权边)解决单源点最短路径问题。对于给定的带权(有向或无向)图 G=(V,E), 其源点为S,加权函数 w是 边集 E 的映射。对图G运行Bellman - Ford算法的结果是一个布尔值,表明图中是否存在着一个从源点S可达的负权回路。若不存在这样的回路,算法将给出从源点S到图G的任意顶点Vi的最短路径D[Vi]。
- 算法描述:
1. 将除源点S外的所有顶点的最短距离估计值 D[Vi]—>+∞, D[S]—>0 2. 反复对边集E中的每条边进行松弛操作,使得顶点集V中的每个顶点Vi的最短距离估计值逐步逼近其最短距离 3. 判断边集E中的每一条边的两个端点是否收敛。如果存在未收敛的顶点,则算法返回false,表明问题无解;否则算法返回true,并且从源点可达的顶点Vi的最短距离保存在D[Vi]中
- 适用范围:
1. 单源最短路径 2. 有向图和无向图 3. 边权可正可负 4. 差分约束系统