第一节:只有五行的算法–Floyd
// 最短路径
// 动态规划
let road = [
[1, 2, 2],
[1, 3, 6],
[1, 4, 4],
[2, 3, 3],
[3, 1, 7],
[3, 4, 1],
[4, 1, 5],
[4, 3, 12],
];
let e = [];
let n = 4; // 城市的个数
for (let i = 0; i <= n; i++) {
let eChild = new Array(n + 1);
e[i] = eChild;
}
for (let i = 0; i <= n; i++) {
for (let j = 0; j <= n; j++) {
e[i][j] = i === j ? 0 : 999;
}
}
// 初始化图
road.forEach((i) => {
let start = i[0];
let end = i[1];
let dis = i[2];
e[start][end] = dis;
});
console.table(JSON.parse(JSON.stringify(e)));
// 核心算法
for (let k = 1; k <= n; k++) {
for (let i = 1; i <= n; i++) {
for (let j = 1; j <= n; j++) {
e[i][j] = Math.min(e[i][k] + e[k][j], e[i][j]);
}
}
}
console.table(e);
第二节:Dijkstra算法—通过边实现松弛
这个算法跟第一节的算法比较,多了不少东西,目的是降低时间复杂度。
看一看多了些什么:book
数组,dis
数组,用来维护顶点1
到各个顶点
的最小距离。
let road = [
[1, 2, 1],
[1, 3, 12],
[2, 3, 9],
[2, 4, 3],
[3, 5, 5],
[4, 3, 4],
[4, 5, 13],
[4, 6, 15],
[5, 6, 4],
];
let e = [];
let n = 6; // 城市的个数
let inf = 999;
for (let i = 0; i <= n; i++) {
let eChild = new Array(n + 1);
e[i] = eChild;
}
for (let i = 0; i <= n; i++) {
for (let j = 0; j <= n; j++) {
e[i][j] = i === j ? 0 : 999;
}
}
// 初始化图
road.forEach((i) => {
let start = i[0];
let end = i[1];
let dis = i[2];
e[start][end] = dis;
});
console.table(JSON.parse(JSON.stringify(e)));
// 初始化dis数组:估计值数组
let dis = new Array(n + 1);
for (let i = 1; i <= n; i++) {
dis[i] = e[1][i]
}
let book = new Array(n + 1).fill(0);
book[1] = 1;
let min, u;
// 核心语句
// 为什么要循环 n - 1
// 我觉得因为总共有六个点,其中顶点1到顶点1这个不用处理,表现在 book[1] = 1
// 所以外循环只需要处理其他五个点即可,也就是 n-1
for (let i = 1; i <= n - 1; i++) {
min = inf;
// 首先找到距离1号点最近的,没有被访问过的顶点
for (let j = 1; j <= n; j++) {
if (book[j] === 0 && dis[j] < min) {
min = dis[j];
u = j;
}
}
book[u] = 1;
// 找到了最近的 u 顶点,并标记这个顶点已经被访问了。
// 此时 dis[u] 从估计值变成确定值。
// 根据这个点进行松弛,
for (let v = 1; v <= n; v++) {
if (e[u][v] < inf) {
if (dis[v] > dis[u] + e[u][v]) {
dis[v] = dis[u] + e[u][v]; // 更新最小值
}
}
}
}
console.log('dis:>>', dis);
2021-01-15
卧槽这个邻接表卡了我一会儿,我实在是看不懂这鸟东西了,等我先跳过这一部分,有时间回头再看
2021-01-16
周六:早上十点多瞅了瞅,似乎看进去点儿,嗯还是我性子太急了,慢下来~
首先我要清楚的是:first
和 next
这两个数组存的是边的编号
/* 存储邻接表 */
let n = 4,
m = 5,
i;
let u = [0, 1, 4, 1, 2, 1];
let v = [0, 4, 3, 2, 4, 3];
let w = [0, 9, 8, 5, 6, 7];
let first = new Array(6).fill(-1);
let next = new Array(6);
for (let i = 1; i <= m; i++) {
next[i] = first[u[i]];
first[u[i]] = i;
}
console.log("first:>>", first);
console.log("next:>>", next);
/* 读取邻接表 */
for (let i = 1; i <= n; i++) {
let k = first[i];
while (k !== -1) {
console.log(u[k],v[k],w[k]);
k = next[k];
}
}
真的,有点懂了的感觉真是太爽了,感觉像是多年的老便秘一下子通畅了似的。
第三节:Bellman-Ford — 解决负权边
一个很神奇的算法,核心代码就几行
思想也是松弛每个边,需要 n-1
轮松弛
为什么是
n-1
?
因为最短路径中不可能包含回路
如果是正权回路,那么越包含回路,这个最短距离的值就越大了;
如果是负权回路,那么就不能有最短路了,最短距离的值越加越小。
排除掉没有回路,那么这就转换成:在一个含有n个顶点的图中,任意两点之间的最短路径最多包含n-1条边的问题了。
let inf = 9999;
let n = 5,
m = 5;
let u = [0, 2, 1, 1, 4, 3],
v = [0, 3, 2, 5, 5, 4],
w = [0, 2, -3, 5, 2, 3];
let dis = new Array(n + 1).fill(inf);
dis[1] = 0;
for (let k = 1; k <= n - 1; k++) {
for (let i = 1; i <= m; i++) {
// v[i] 代表终点
// u[i] 代表边的起点
dis[v[i]] = Math.min(dis[v[i]], dis[u[i]] + w[i]);
}
console.log(`第${k}个dis:>>`, dis);
}
之后讲了如何判断一个图是否含有负权回路:思想就是在n-1
论之后,最短路径已经确定了,如果在接下来还能继续松弛,那么就判定有负权回路。
接着对此算法做了优化:因为可能提前就确定了最短路,比方说,再上图所示,第三轮的dis
数组和第四轮的dis
数组其实都是一样的,所以可以提前结束松弛
let inf = 9999;
let u = [0, 2, 1, 1, 4, 3],
v = [0, 3, 2, 5, 5, 4],
w = [0, 2, -3, 5, 2, 3];
// 含有负权边的样例
// let u = [0, 2, 1, 1, 4, 3, 3],
// v = [0, 3, 2, 5, 5, 4, 1],
// w = [0, 2, -3, 5, 2, 3, -4];
let n = u.length,
m = u.length;
let dis = new Array(n + 1).fill(inf);
dis[1] = 0;
let check;
for (let k = 1; k <= n - 1; k++) {
check = 0;
for (let i = 1; i <= m; i++) {
// v[i] 代表终点
// u[i] 代表边的起点
if (dis[v[i]] > dis[u[i]] + w[i]) {
dis[v[i]] = dis[u[i]] + w[i];
check = 1;
}
}
if (check === 0) {
console.log("提前结束松弛");
break;
} // 说明dis数组不变了。可以提前结束掉松弛。
console.log(`第${k}轮松弛后的dis数组:>>`, dis);
}
let flag = 0;
for (let i = 1; i <= m; i++) {
if (dis[v[i]] > dis[u[i]] + w[i]) {
flag = 1;
}
}
if (flag === 1) {
// 说明还能松弛
console.log("此图含有负权回路");
} else {
console.log("结束", dis);
}
第一个正常样例
加了一个负权回路,顶点3
->顶点1
,权值为-4
第四节:Bellman-Ford 的队列优化
我这书上,给的答案是下面这图,答案印错了吧…
// let a = [1,3,4];
// console.log(a.indexOf(3));
let u = [0, 1, 1, 2, 2, 3, 4, 5],
v = [0, 2, 5, 3, 5, 4, 5, 3],
w = [2, 10, 3, 7, 4, 5, 6];
let n = 5,// n 表示顶点个数
m = 7;// m表示有多少条边
let inf = 999;
let first = new Array(n + 1).fill(-1),
next = new Array(m + 1);
let dis = new Array(n + 1).fill(inf);
let book = new Array(m + 1).fill(0);
let que = new Array(100).fill(0);
let head = 1,
tail = 1;
// dis[0] = 0;
dis[1] = 0;
// 读入每一条边,建立邻接表
for (let i = 1; i <= m; i++) {
next[i] = first[u[i]];
first[u[i]] = i;
}
console.log('first:>>', first);
console.log('next:>>', next);
// 1 顶点入队
que[tail] = 1;
tail++;
book[1] = 1;
while (head < tail) {
k = first[que[head]];
while (k != -1) {
if (dis[v[k]] > dis[u[k]] + w[k]) {
// 松弛成功
dis[v[k]] = dis[u[k]] + w[k];
// 记录
if (book[v[k]] == 0) {
// 入队
que[tail] = v[k];
tail++;
book[v[k]] = 1; // 标记
}
}
k = next[k];
}
book[que[head]] = 0;// 取消头部的标记
head++;
}
总结
这章可终于结束了,托了好久,当然消化的也不太好,有时间要回过头来复盘好多遍。
思想:松弛,能理解这个就差不多了。