我们用这个图来模拟一下迪杰斯特拉算法,其实这个算法就是贪心加上广搜,理解了感觉非常简单
贪心体现在哪呢? 广搜又体现在哪呢?
下面我们就来模拟一遍这个算
法的朴素写法,写完朴素写法之后我们试着对其优化。
此算法主要是解决,在一个图中,某一节点(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]
算法循环开始
第二步: 以1节点进行扩展 扩展之后 dis = [65525, 0, 1, 65525, 4] pre = [-1, -1, 1, -1, 1]
第二步:更新操作,以二节点进行更新 扩展之后dis = [65525, 0, 1, 2, 4] pre = [-1, -1, 1, 2, 1]
第二步:更新操作,以三节点进行更新扩展之后dis= [65525, 0, 1, 2, 3] pre = [-1, -1, 1,2, 3]
第二步:更新操作 ,以四节点进行更新扩展之后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
// 此函数作用是 寻找到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;
}
}
}
看到这里,你是否掌握了图的最短路径的求解?