【第六章】最短路径

第一节:只有五行的算法–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
周六:早上十点多瞅了瞅,似乎看进去点儿,嗯还是我性子太急了,慢下来~

首先我要清楚的是:firstnext 这两个数组存的是边的编号
在这里插入图片描述

/* 存储邻接表 */
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++;
}

总结

这章可终于结束了,托了好久,当然消化的也不太好,有时间要回过头来复盘好多遍。
思想:松弛,能理解这个就差不多了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值