
我们用这个图来模拟一下迪杰斯特拉算法,其实这个算法就是贪心加上广搜,理解了感觉非常简单
贪心体现在哪呢? 广搜又体现在哪呢?
下面我们就来模拟一遍这个算
法的朴素写法,写完朴素写法之后我们试着对其优化。
此算法主要是解决,在一个图中,某一节点(start表示)到其余的节点的最短路径和距离。
算法维护着三个数组:
dis[] : dis[i] 表示start到i这个节点的最短距离
visit[] :visit[i]= true 表示i这个节点已经被访问过之后就不需要访问了 ,这也是贪心算法体现之处
pre[]: pre[i] = j 表示到 i节点的最短路径中i的前驱节点是j
算法开始初始化:
初始化dis数组全为无穷大,表示目前到此节点不可达,为了以后更新所用,但是 dis[start] = 0,因为自己到自己永远是直接 可达的, 不需要寻找,初始化 visit数组全为false,表示都没有访问过初始化 pre数组全为负无穷,表示 前驱节点目前不存在(不一定是负无穷,不存的数皆可)
算法一共分为两步:
从dis数组中找出最小路径的节点,设置此节点为已访问
对第一步找到的节点(用p表示),根据此节点邻接节点(用t表示)对dis数组进行更新,怎么更新呢?
dis数组的含义是start节点到其他节点的最短路径,那么我们就可以这样更新:
当dis[p]+ map[p][i] < dis[i]时,dis[i]= dis[p]+ map[p][i] ,这个怎么理解呢,就是如果到t节点是从p节点再到t节点比我之前的那条路径到t节点还要短,那我么我们就要更新.更新完之后循环 第一步,直到所有节点都被访问过
好,理解完概念之后,我们用左边的图模拟一下迪杰斯特拉算法的步骤:(假如我们要寻找1到其余三个节点的最短路)
初始化 :
dis[]= [65525, 0, 65525, 65525, 65525]
visit[] =[false, false,false, false, false]
pre[] = [-1,-1,-1,-1,-1]
算法循环开始
- 第一步 : 找最小值 可以看到 node= 1,设置visit[i]= true
第二步: 以1节点进行扩展 扩展之后 dis = [65525, 0, 1, 65525, 4] pre = [-1, -1, 1, -1, 1]
- 第一步: 找最小值 可以看到 没有被访问过并且最小值为 node= 2 ,设置visit[2]= true
第二步:更新操作,以二节点进行更新 扩展之后dis = [65525, 0, 1, 2, 4] pre = [-1, -1, 1, 2, 1]
- 第一步:找最小值 可以看到 没有被访问过并且最小值为 node= 3 ,设置visit[3]= true
第二步:更新操作,以三节点进行更新扩展之后dis= [65525, 0, 1, 2, 3] pre = [-1, -1, 1,2, 3]
- 第一步:找最小值 可以看到 没有被访问过并且最小值为 node= 4 ,设置visit[4]= true
第二步:更新操作 ,以四节点进行更新扩展之后dis= [65525, 0, 1, 2, 3] pre = [-1, -1, 1,2, 3]
第四步之后算法结束
如果你理解了上面步骤,那么代码应该是水到渠成了
// times = [[2,1,1],[2,3,1],[3,4,1]], N = 4, K = 2
// time表示边关系 N表示有多少个节点 K表示从哪个节点开始寻找最短路径
public void networkDelayTime(int[][] times, int N, int K) {
HashMap<Integer, List<int[]>> map = new HashMap<>();
// 这是无向图
// 邻接表存储
for (int[] t : times) {
if (!map.containsKey(t[0])) {
map.put(t[0], new ArrayList<int[]>());
}
if (!map.containsKey(t[1])) {
map.put(t[1], new ArrayList<int[]>());
}
map.get(t[0]).add(new int[] { t[1], t[2] });
map.get(t[1]).add(new int[] { t[0], t[2] });
}
System.out.println(map);
// 存储K到每个节点的最短值
int[] dis = new int[N + 1];
Arrays.fill(dis, Integer.MAX_VALUE);
dis[K] = 0;
// pre[i] = j 表示i的前驱节点是j
int[] pre = new int[N + 1];
Arrays.fill(pre, -1);
boolean[] visit = new boolean[N + 1];
while (true) {
int min_node = -1;
int min_len = Integer.MAX_VALUE;
for (int i = 1; i < dis.length; i++) {
if (!visit[i] && dis[i] < min_len) {
min_len = dis[i];
min_node = i;
}
}
// 如果都遍历完了那就说明 图都遍历完成了
if (min_node < 0) {
break;
}
visit[min_node] = true;
if (map.containsKey(min_node)) {
for (int[] t : map.get(min_node)) {
// 按照贪心的思想 访问过的一定是最短路径了 不需要更新
// 因为从最短的边出去的节点一定是最短的,我们每一次找min_node都是找最短的边
if (!visit[t[0]] && dis[t[0]] > dis[min_node] + t[1]) {
dis[t[0]] = dis[min_node] + t[1];
pre[t[0]] = min_node;
}
}
}
}
System.out.println(Arrays.toString(dis));
System.out.println(Arrays.toString(pre));
}
到了现在,你是否体会到了广搜和贪心的思想了, 广搜就是每次搜索大一圈,直到遍历完图, 贪心就是为了更好地广搜, 使得每次只用搜索最小的那个节点
迪杰斯特拉的优先队列写法
不知道细心的你是否注意到了,我们在每次循环都需要去找一遍dir里面的最小值, 那么我们有没有什么方法直接就可以拿到呢, 不用循环去找?
答案是有的, 有一种数据结构是优先队列, 可以按照队列里面的优先级进行排序, 最小的永远在队列头, 我们就可以使用这种数据结构, 每次循环只需要取出队列头就行了, 当队列空, 自然也就遍历结束了
我们直接上代码// 使用优先队列进行优化 public void networkDelayTime_better(int[][] times, int N, int K) { HashMap<Integer, List<int[]>> map = new HashMap<>(); // 这是无向图 // 邻接表存储 for (int[] t : times) { if (!map.containsKey(t[0])) { map.put(t[0], new ArrayList<int[]>()); } if (!map.containsKey(t[1])) { map.put(t[1], new ArrayList<int[]>()); } map.get(t[0]).add(new int[] { t[1], t[2] }); map.get(t[1]).add(new int[] { t[0], t[2] }); } int[] dis = new int[N + 1]; Arrays.fill(dis, Integer.MAX_VALUE); dis[K] = 0;// 自己到自己 int[] pre = new int[N + 1]; Arrays.fill(pre, -1); boolean[] visit = new boolean[N + 1]; // 优先队列 PriorityQueue<Integer> q = new PriorityQueue<>(new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return dis[o1] - dis[o2]; } }); // 先把自己压进去 q.offer(K); int num; while (!q.isEmpty()) { num = q.poll(); if (visit[num]) { continue; } visit[num] = true; if (map.containsKey(num)) { // 如果存在这个节点 for (int[] t : map.get(num)) { // 遍历这个节点的相邻节点 if (!visit[t[0]] && dis[t[0]] > dis[num] + t[1]) { // 如果路径更少 // 则更新 dis[t[0]] = dis[num] + t[1]; pre[t[0]] = num; q.offer(t[0]); } } } } System.out.println(Arrays.toString(dis)); System.out.println(Arrays.toString(pre)); }最短路径的深度优先写法(当做理解)
我们定义一个辅助函数
// start表示从起始节点, sum表示起始节点到每个节点的路径, 用作更新用
// map表示邻接表
private void dfs(int start, int sum, HashMap<Integer, List<int[]>> map)
// 此函数作用是 寻找到start节点到没个节点的做短路径 ,存储到一个变量里
直接看代码理解
public void networkDelayTime_dfs(int[][] times, int N, int K) { HashMap<Integer, List<int[]>> map = new HashMap<>(); // 这是无向图 for (int[] t : times) { if (!map.containsKey(t[0])) { map.put(t[0], new ArrayList<int[]>()); } if (!map.containsKey(t[1])) { map.put(t[1], new ArrayList<int[]>()); } map.get(t[0]).add(new int[] { t[1], t[2] }); map.get(t[1]).add(new int[] { t[0], t[2] }); } dis = new int[N + 1]; Arrays.fill(dis, Integer.MAX_VALUE); dis[K] = 0; visit = new boolean[N + 1]; visit[K] = true; dfs(K, 0, map); System.out.println(Arrays.toString(dis)); } private void dfs(int k, int i, HashMap<Integer, List<int[]>> map) { // 剪枝操作, 当遍历到某一个节点时, 找到的路径 已经比我存储在dis数组里面的还要大, 那我们不需要遍历后面了,直接砍掉 if (dis[k] < i) { return; } // 到了这里 i肯定小于或等于dis[k] 直接更新即可 dis[k] = i; // 对于k的每一个邻接点 for (int[] t : map.get(k)) { // 如果没有访问过 if (!visit[t[0]]) { // 设置为访问状态 visit[t[0]] = true; // 后面的交给递归 dfs(t[0], i + t[1], map); // 递归结束之后 需要回溯 visit[t[0]] = false; } } }看到这里,你是否掌握了图的最短路径的求解?