Skip to main content

Weighted Shortest Paths: Dijkstra, Bellman-Ford and Floyd-Warshall

Editorial

A plain queue or a heap

BFS finds the fewest steps when every step costs the same. Give the steps weights and the fewest steps can be the dearest way. The first fix anyone writes keeps BFS's queue and lets a cell back in whenever a cheaper way to it turns up: right in the end, but in no particular order. Dijkstra's algorithm takes, every time, the cell nearest the start that isn't settled yet, from a heap. Both, on a grid of 300 × 300 cells where every step between two cells weighs from 1 to 100:

import heapq
import time
from collections import deque

N = 300                                        # a grid of 300 x 300 cells; every step between two has its own weight
x = 7
right, down = [], []                           # the weight of the step from a cell to the cell right of it, and below
for _ in range(N * N):
    x = x * 48271 % 2147483647
    right.append(1 + x % 100)
    x = x * 48271 % 2147483647
    down.append(1 + x % 100)


def steps(v):                                  # (neighbour, weight) for the four moves
    r, c = divmod(v, N)
    if r < N - 1:
        yield v + N, down[v]
    if r > 0:
        yield v - N, down[v - N]
    if c < N - 1:
        yield v + 1, right[v]
    if c > 0:
        yield v - 1, right[v - 1]


def by_queue():                                # BFS with weights: a plain queue, a cell in again whenever it improves
    dist = [float("inf")] * (N * N)
    dist[0] = 0
    queue = deque([0])
    taken = 0
    while queue:
        u = queue.popleft()
        taken += 1
        for v, w in steps(u):
            if dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                queue.append(v)
    return dist[-1], taken


def by_heap():                                 # Dijkstra: always the nearest cell not settled yet
    dist = [float("inf")] * (N * N)
    dist[0] = 0
    heap = [(0, 0)]
    settled = stale = 0
    while heap:
        d, u = heapq.heappop(heap)
        if d > dist[u]:                        # an older, longer entry for a cell settled already
            stale += 1
            continue
        settled += 1
        for v, w in steps(u):
            if d + w < dist[v]:
                dist[v] = d + w
                heapq.heappush(heap, (dist[v], v))
    return dist[-1], settled, stale


print(f"{N} x {N} cells, {2 * N * (N - 1)} steps, each weighing 1 to 100")
start = time.perf_counter()
cost, taken = by_queue()
ms = round((time.perf_counter() - start) * 1000)
print(f"a plain queue: corner to corner costs {cost}, {taken} cells taken off, {ms} ms")
start = time.perf_counter()
cost, settled, stale = by_heap()
ms = round((time.perf_counter() - start) * 1000)
print(f"a heap: corner to corner costs {cost}, {settled} settled, {stale} stale entries skipped, {ms} ms")

# 300 x 300 cells, 179400 steps, each weighing 1 to 100
# a plain queue: corner to corner costs 14340, 1595745 cells taken off, 1416 ms
# a heap: corner to corner costs 14340, 90000 settled, 28518 stale entries skipped, 165 ms
import java.util.*;

public class Main {
    static final int N = 300;                       // a grid of 300 x 300 cells; every step has its own weight
    static int[] right = new int[N * N], down = new int[N * N];   // the step to the cell right, and the one below

    static int[][] steps(int v) {                   // {neighbour, weight} for the four moves
        int r = v / N, c = v % N, k = 0;
        int[][] out = new int[4][];
        if (r < N - 1) out[k++] = new int[] {v + N, down[v]};
        if (r > 0) out[k++] = new int[] {v - N, down[v - N]};
        if (c < N - 1) out[k++] = new int[] {v + 1, right[v]};
        if (c > 0) out[k++] = new int[] {v - 1, right[v - 1]};
        return Arrays.copyOf(out, k);
    }

    static long[] byQueue() {                       // BFS with weights: a plain queue, a cell in again when it improves
        int[] dist = new int[N * N];
        Arrays.fill(dist, Integer.MAX_VALUE);
        dist[0] = 0;
        ArrayDeque<Integer> queue = new ArrayDeque<>();
        queue.add(0);
        long taken = 0;
        while (!queue.isEmpty()) {
            int u = queue.poll();
            taken++;
            for (int[] s : steps(u)) {
                if (dist[u] + s[1] < dist[s[0]]) {
                    dist[s[0]] = dist[u] + s[1];
                    queue.add(s[0]);
                }
            }
        }
        return new long[] {dist[N * N - 1], taken};
    }

    static long[] byHeap() {                        // Dijkstra: always the nearest cell not settled yet
        int[] dist = new int[N * N];
        Arrays.fill(dist, Integer.MAX_VALUE);
        dist[0] = 0;
        PriorityQueue<int[]> heap = new PriorityQueue<>((a, b) -> a[0] != b[0] ? Integer.compare(a[0], b[0])
                : Integer.compare(a[1], b[1]));
        heap.add(new int[] {0, 0});
        long settled = 0, stale = 0;
        while (!heap.isEmpty()) {
            int[] top = heap.poll();
            int d = top[0], u = top[1];
            if (d > dist[u]) {                      // an older, longer entry for a cell settled already
                stale++;
                continue;
            }
            settled++;
            for (int[] s : steps(u)) {
                if (d + s[1] < dist[s[0]]) {
                    dist[s[0]] = d + s[1];
                    heap.add(new int[] {dist[s[0]], s[0]});
                }
            }
        }
        return new long[] {dist[N * N - 1], settled, stale};
    }

    public static void main(String[] args) {
        long x = 7;
        for (int i = 0; i < N * N; i++) {
            x = x * 48271 % 2147483647;
            right[i] = (int) (1 + x % 100);
            x = x * 48271 % 2147483647;
            down[i] = (int) (1 + x % 100);
        }
        System.out.println(N + " x " + N + " cells, " + 2 * N * (N - 1) + " steps, each weighing 1 to 100");
        long start = System.nanoTime();
        long[] q = byQueue();
        System.out.println("a plain queue: corner to corner costs " + q[0] + ", " + q[1] + " cells taken off, "
                + (System.nanoTime() - start) / 1000000 + " ms");
        start = System.nanoTime();
        long[] h = byHeap();
        System.out.println("a heap: corner to corner costs " + h[0] + ", " + h[1] + " settled, " + h[2]
                + " stale entries skipped, " + (System.nanoTime() - start) / 1000000 + " ms");
    }
}

// 300 x 300 cells, 179400 steps, each weighing 1 to 100
// a plain queue: corner to corner costs 14340, 1595745 cells taken off, 218 ms
// a heap: corner to corner costs 14340, 90000 settled, 28518 stale entries skipped, 74 ms
#include <bits/stdc++.h>
using namespace std;

const int N = 300;                                   // a grid of 300 x 300 cells; every step has its own weight
vector<int> rightW(N * N), downW(N * N);             // the step to the cell right of a cell, and to the one below

vector<pair<int, int>> steps(int v) {                // {neighbour, weight} for the four moves
    int r = v / N, c = v % N;
    vector<pair<int, int>> out;
    if (r < N - 1) out.push_back({v + N, downW[v]});
    if (r > 0) out.push_back({v - N, downW[v - N]});
    if (c < N - 1) out.push_back({v + 1, rightW[v]});
    if (c > 0) out.push_back({v - 1, rightW[v - 1]});
    return out;
}

pair<int, long long> byQueue() {                     // BFS with weights: a plain queue, a cell in again when it improves
    vector<int> dist(N * N, INT_MAX);
    dist[0] = 0;
    queue<int> q;
    q.push(0);
    long long taken = 0;
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        taken++;
        for (auto [v, w] : steps(u)) {
            if (dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                q.push(v);
            }
        }
    }
    return {dist[N * N - 1], taken};
}

tuple<int, long long, long long> byHeap() {          // Dijkstra: always the nearest cell not settled yet
    vector<int> dist(N * N, INT_MAX);
    dist[0] = 0;
    priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> heap;
    heap.push({0, 0});
    long long settled = 0, stale = 0;
    while (!heap.empty()) {
        auto [d, u] = heap.top();
        heap.pop();
        if (d > dist[u]) {                           // an older, longer entry for a cell settled already
            stale++;
            continue;
        }
        settled++;
        for (auto [v, w] : steps(u)) {
            if (d + w < dist[v]) {
                dist[v] = d + w;
                heap.push({dist[v], v});
            }
        }
    }
    return {dist[N * N - 1], settled, stale};
}

int main() {
    long long x = 7;
    for (int i = 0; i < N * N; i++) {
        x = x * 48271 % 2147483647;
        rightW[i] = 1 + x % 100;
        x = x * 48271 % 2147483647;
        downW[i] = 1 + x % 100;
    }
    cout << N << " x " << N << " cells, " << 2 * N * (N - 1) << " steps, each weighing 1 to 100\n";
    auto start = chrono::steady_clock::now();
    auto [cost, taken] = byQueue();
    auto ms = chrono::duration_cast<chrono::milliseconds>(chrono::steady_clock::now() - start).count();
    cout << "a plain queue: corner to corner costs " << cost << ", " << taken << " cells taken off, " << ms << " ms\n";
    start = chrono::steady_clock::now();
    auto [cost2, settled, stale] = byHeap();
    ms = chrono::duration_cast<chrono::milliseconds>(chrono::steady_clock::now() - start).count();
    cout << "a heap: corner to corner costs " << cost2 << ", " << settled << " settled, " << stale
         << " stale entries skipped, " << ms << " ms\n";
}

// 300 x 300 cells, 179400 steps, each weighing 1 to 100
// a plain queue: corner to corner costs 14340, 1595745 cells taken off, 132 ms
// a heap: corner to corner costs 14340, 90000 settled, 28518 stale entries skipped, 19 ms
class Heap {                                   // a min-heap of arrays, compared item by item
    constructor() { this.a = []; }
    get size() { return this.a.length; }
    less(i, j) {
        const x = this.a[i], y = this.a[j];
        for (let k = 0; k < x.length; k++) if (x[k] !== y[k]) return x[k] < y[k];
        return false;
    }
    push(item) {
        const a = this.a;
        a.push(item);
        for (let i = a.length - 1; i > 0;) {
            const p = (i - 1) >> 1;
            if (!this.less(i, p)) break;
            [a[i], a[p]] = [a[p], a[i]];
            i = p;
        }
    }
    pop() {
        const a = this.a, top = a[0], last = a.pop();
        if (a.length) {
            a[0] = last;
            for (let i = 0; ;) {
                const l = 2 * i + 1, r = l + 1;
                let m = i;
                if (l < a.length && this.less(l, m)) m = l;
                if (r < a.length && this.less(r, m)) m = r;
                if (m === i) break;
                [a[i], a[m]] = [a[m], a[i]];
                i = m;
            }
        }
        return top;
    }
}

const N = 300;                                 // a grid of 300 x 300 cells; every step between two has its own weight
const right = new Array(N * N), down = new Array(N * N);   // the step to the cell right of a cell, and to the one below
let x = 7;
for (let i = 0; i < N * N; i++) {
    x = x * 48271 % 2147483647;
    right[i] = 1 + x % 100;
    x = x * 48271 % 2147483647;
    down[i] = 1 + x % 100;
}

function steps(v) {                            // [neighbour, weight] for the four moves
    const r = Math.floor(v / N), c = v % N, out = [];
    if (r < N - 1) out.push([v + N, down[v]]);
    if (r > 0) out.push([v - N, down[v - N]]);
    if (c < N - 1) out.push([v + 1, right[v]]);
    if (c > 0) out.push([v - 1, right[v - 1]]);
    return out;
}

function byQueue() {                           // BFS with weights: a plain queue, a cell in again whenever it improves
    const dist = new Array(N * N).fill(Infinity);
    dist[0] = 0;
    const queue = [0];
    let taken = 0;
    for (let head = 0; head < queue.length; head++) {
        const u = queue[head];
        taken++;
        for (const [v, w] of steps(u)) {
            if (dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                queue.push(v);
            }
        }
    }
    return [dist[N * N - 1], taken];
}

function byHeap() {                            // Dijkstra: always the nearest cell not settled yet
    const dist = new Array(N * N).fill(Infinity);
    dist[0] = 0;
    const heap = new Heap();
    heap.push([0, 0]);
    let settled = 0, stale = 0;
    while (heap.size) {
        const [d, u] = heap.pop();
        if (d > dist[u]) {                     // an older, longer entry for a cell settled already
            stale++;
            continue;
        }
        settled++;
        for (const [v, w] of steps(u)) {
            if (d + w < dist[v]) {
                dist[v] = d + w;
                heap.push([dist[v], v]);
            }
        }
    }
    return [dist[N * N - 1], settled, stale];
}

console.log(`${N} x ${N} cells, ${2 * N * (N - 1)} steps, each weighing 1 to 100`);
let start = performance.now();
const [cost, taken] = byQueue();
console.log(`a plain queue: corner to corner costs ${cost}, ${taken} cells taken off, ${Math.round(performance.now() - start)} ms`);
start = performance.now();
const [cost2, settled, stale] = byHeap();
console.log(`a heap: corner to corner costs ${cost2}, ${settled} settled, ${stale} stale entries skipped, ${
    Math.round(performance.now() - start)} ms`);

// 300 x 300 cells, 179400 steps, each weighing 1 to 100
// a plain queue: corner to corner costs 14340, 1595745 cells taken off, 324 ms
// a heap: corner to corner costs 14340, 90000 settled, 28518 stale entries skipped, 80 ms
class Heap {                                   // a min-heap of arrays, compared item by item
    private a: number[][] = [];
    get size(): number { return this.a.length; }
    private less(i: number, j: number): boolean {
        const x = this.a[i], y = this.a[j];
        for (let k = 0; k < x.length; k++) if (x[k] !== y[k]) return x[k] < y[k];
        return false;
    }
    push(item: number[]): void {
        const a = this.a;
        a.push(item);
        for (let i = a.length - 1; i > 0;) {
            const p = (i - 1) >> 1;
            if (!this.less(i, p)) break;
            [a[i], a[p]] = [a[p], a[i]];
            i = p;
        }
    }
    pop(): number[] {
        const a = this.a, top = a[0], last = a.pop()!;
        if (a.length) {
            a[0] = last;
            for (let i = 0; ;) {
                const l = 2 * i + 1, r = l + 1;
                let m = i;
                if (l < a.length && this.less(l, m)) m = l;
                if (r < a.length && this.less(r, m)) m = r;
                if (m === i) break;
                [a[i], a[m]] = [a[m], a[i]];
                i = m;
            }
        }
        return top;
    }
}

const N = 300;                                 // a grid of 300 x 300 cells; every step between two has its own weight
const right: number[] = new Array(N * N), down: number[] = new Array(N * N);   // the step right, and the step down
let x = 7;
for (let i = 0; i < N * N; i++) {
    x = x * 48271 % 2147483647;
    right[i] = 1 + x % 100;
    x = x * 48271 % 2147483647;
    down[i] = 1 + x % 100;
}

function steps(v: number): [number, number][] {   // [neighbour, weight] for the four moves
    const r = Math.floor(v / N), c = v % N, out: [number, number][] = [];
    if (r < N - 1) out.push([v + N, down[v]]);
    if (r > 0) out.push([v - N, down[v - N]]);
    if (c < N - 1) out.push([v + 1, right[v]]);
    if (c > 0) out.push([v - 1, right[v - 1]]);
    return out;
}

function byQueue(): [number, number] {         // BFS with weights: a plain queue, a cell in again whenever it improves
    const dist: number[] = new Array(N * N).fill(Infinity);
    dist[0] = 0;
    const queue = [0];
    let taken = 0;
    for (let head = 0; head < queue.length; head++) {
        const u = queue[head];
        taken++;
        for (const [v, w] of steps(u)) {
            if (dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                queue.push(v);
            }
        }
    }
    return [dist[N * N - 1], taken];
}

function byHeap(): [number, number, number] {  // Dijkstra: always the nearest cell not settled yet
    const dist: number[] = new Array(N * N).fill(Infinity);
    dist[0] = 0;
    const heap = new Heap();
    heap.push([0, 0]);
    let settled = 0, stale = 0;
    while (heap.size) {
        const [d, u] = heap.pop();
        if (d > dist[u]) {                     // an older, longer entry for a cell settled already
            stale++;
            continue;
        }
        settled++;
        for (const [v, w] of steps(u)) {
            if (d + w < dist[v]) {
                dist[v] = d + w;
                heap.push([dist[v], v]);
            }
        }
    }
    return [dist[N * N - 1], settled, stale];
}

console.log(`${N} x ${N} cells, ${2 * N * (N - 1)} steps, each weighing 1 to 100`);
let start = performance.now();
const [cost, taken] = byQueue();
console.log(`a plain queue: corner to corner costs ${cost}, ${taken} cells taken off, ${Math.round(performance.now() - start)} ms`);
start = performance.now();
const [cost2, settled, stale] = byHeap();
console.log(`a heap: corner to corner costs ${cost2}, ${settled} settled, ${stale} stale entries skipped, ${
    Math.round(performance.now() - start)} ms`);

// 300 x 300 cells, 179400 steps, each weighing 1 to 100
// a plain queue: corner to corner costs 14340, 1595745 cells taken off, 316 ms
// a heap: corner to corner costs 14340, 90000 settled, 28518 stale entries skipped, 82 ms
using System;
using System.Collections.Generic;
using System.Diagnostics;

class Program {
    const int N = 300;                                // a grid of 300 x 300 cells; every step has its own weight
    static int[] right = new int[N * N], down = new int[N * N];   // the step to the cell right, and the one below

    static List<(int v, int w)> Steps(int v) {        // (neighbour, weight) for the four moves
        int r = v / N, c = v % N;
        var output = new List<(int, int)>(4);
        if (r < N - 1) output.Add((v + N, down[v]));
        if (r > 0) output.Add((v - N, down[v - N]));
        if (c < N - 1) output.Add((v + 1, right[v]));
        if (c > 0) output.Add((v - 1, right[v - 1]));
        return output;
    }

    static (int, long) ByQueue() {                    // BFS with weights: a plain queue, a cell in again when it improves
        var dist = new int[N * N];
        Array.Fill(dist, int.MaxValue);
        dist[0] = 0;
        var queue = new Queue<int>();
        queue.Enqueue(0);
        long taken = 0;
        while (queue.Count > 0) {
            int u = queue.Dequeue();
            taken++;
            foreach (var (v, w) in Steps(u)) {
                if (dist[u] + w < dist[v]) {
                    dist[v] = dist[u] + w;
                    queue.Enqueue(v);
                }
            }
        }
        return (dist[N * N - 1], taken);
    }

    static (int, long, long) ByHeap() {               // Dijkstra: always the nearest cell not settled yet
        var dist = new int[N * N];
        Array.Fill(dist, int.MaxValue);
        dist[0] = 0;
        var heap = new PriorityQueue<int, (int, int)>();   // ordered by (distance, cell)
        heap.Enqueue(0, (0, 0));
        long settled = 0, stale = 0;
        while (heap.TryDequeue(out int u, out var key)) {
            int d = key.Item1;
            if (d > dist[u]) {                        // an older, longer entry for a cell settled already
                stale++;
                continue;
            }
            settled++;
            foreach (var (v, w) in Steps(u)) {
                if (d + w < dist[v]) {
                    dist[v] = d + w;
                    heap.Enqueue(v, (dist[v], v));
                }
            }
        }
        return (dist[N * N - 1], settled, stale);
    }

    static void Main() {
        long x = 7;
        for (int i = 0; i < N * N; i++) {
            x = x * 48271 % 2147483647;
            right[i] = (int) (1 + x % 100);
            x = x * 48271 % 2147483647;
            down[i] = (int) (1 + x % 100);
        }
        Console.WriteLine($"{N} x {N} cells, {2 * N * (N - 1)} steps, each weighing 1 to 100");
        var watch = Stopwatch.StartNew();
        var (cost, taken) = ByQueue();
        Console.WriteLine($"a plain queue: corner to corner costs {cost}, {taken} cells taken off, {watch.ElapsedMilliseconds} ms");
        watch.Restart();
        var (cost2, settled, stale) = ByHeap();
        Console.WriteLine($"a heap: corner to corner costs {cost2}, {settled} settled, {stale} stale entries skipped, {watch.ElapsedMilliseconds} ms");
    }
}

// 300 x 300 cells, 179400 steps, each weighing 1 to 100
// a plain queue: corner to corner costs 14340, 1595745 cells taken off, 105 ms
// a heap: corner to corner costs 14340, 90000 settled, 28518 stale entries skipped, 32 ms
package main

import (
	"container/heap"
	"fmt"
	"math"
	"time"
)

type item struct{ d, v int }
type minHeap []item // the smallest distance on top, the smaller vertex first on a tie

func (h minHeap) Len() int            { return len(h) }
func (h minHeap) Less(i, j int) bool  { return h[i].d < h[j].d || (h[i].d == h[j].d && h[i].v < h[j].v) }
func (h minHeap) Swap(i, j int)       { h[i], h[j] = h[j], h[i] }
func (h *minHeap) Push(x interface{}) { *h = append(*h, x.(item)) }
func (h *minHeap) Pop() interface{} {
	old := *h
	x := old[len(old)-1]
	*h = old[:len(old)-1]
	return x
}

const N = 300 // a grid of 300 x 300 cells; every step between two has its own weight

var right, down [N * N]int // the weight of the step to the cell right of a cell, and to the one below

func steps(v int) [][2]int { // {neighbour, weight} for the four moves
	r, c := v/N, v%N
	out := make([][2]int, 0, 4)
	if r < N-1 {
		out = append(out, [2]int{v + N, down[v]})
	}
	if r > 0 {
		out = append(out, [2]int{v - N, down[v-N]})
	}
	if c < N-1 {
		out = append(out, [2]int{v + 1, right[v]})
	}
	if c > 0 {
		out = append(out, [2]int{v - 1, right[v-1]})
	}
	return out
}

func byQueue() (int, int) { // BFS with weights: a plain queue, a cell in again whenever it improves
	dist := make([]int, N*N)
	for i := range dist {
		dist[i] = math.MaxInt64
	}
	dist[0] = 0
	queue := []int{0}
	taken := 0
	for head := 0; head < len(queue); head++ {
		u := queue[head]
		taken++
		for _, s := range steps(u) {
			if dist[u]+s[1] < dist[s[0]] {
				dist[s[0]] = dist[u] + s[1]
				queue = append(queue, s[0])
			}
		}
	}
	return dist[N*N-1], taken
}

func byHeap() (int, int, int) { // Dijkstra: always the nearest cell not settled yet
	dist := make([]int, N*N)
	for i := range dist {
		dist[i] = math.MaxInt64
	}
	dist[0] = 0
	h := &minHeap{{0, 0}}
	settled, stale := 0, 0
	for h.Len() > 0 {
		top := heap.Pop(h).(item)
		if top.d > dist[top.v] { // an older, longer entry for a cell settled already
			stale++
			continue
		}
		settled++
		for _, s := range steps(top.v) {
			if top.d+s[1] < dist[s[0]] {
				dist[s[0]] = top.d + s[1]
				heap.Push(h, item{dist[s[0]], s[0]})
			}
		}
	}
	return dist[N*N-1], settled, stale
}

func main() {
	x := 7
	for i := 0; i < N*N; i++ {
		x = x * 48271 % 2147483647
		right[i] = 1 + x%100
		x = x * 48271 % 2147483647
		down[i] = 1 + x%100
	}
	fmt.Printf("%d x %d cells, %d steps, each weighing 1 to 100\n", N, N, 2*N*(N-1))
	start := time.Now()
	cost, taken := byQueue()
	fmt.Printf("a plain queue: corner to corner costs %d, %d cells taken off, %d ms\n", cost, taken, time.Since(start).Milliseconds())
	start = time.Now()
	cost, settled, stale := byHeap()
	fmt.Printf("a heap: corner to corner costs %d, %d settled, %d stale entries skipped, %d ms\n", cost, settled, stale,
		time.Since(start).Milliseconds())
}

// 300 x 300 cells, 179400 steps, each weighing 1 to 100
// a plain queue: corner to corner costs 14340, 1595745 cells taken off, 211 ms
// a heap: corner to corner costs 14340, 90000 settled, 28518 stale entries skipped, 36 ms

Both reach the far corner for 14,340, in every language. The plain queue took cells off 1,595,745 times, about 18 times each: a cell reached early by a dear way had to be taken again for every cheaper way that turned up later, and everything after it with it. The heap settled each of the 90,000 cells once and skipped 28,518 entries left over from ways that were beaten later. In Python that was 1,416 ms against 165 ms, and the heap was faster in every language.

The lesson's network of six nodes, 1 to 6, with edges 1 to 2 weighing 4, 1 to 3 weighing 1, 3 to 2 weighing 2, 2 to 4 weighing 1, 3 to 4 weighing 5, 4 to 5 weighing 3, 5 to 6 weighing 1 and 3 to 6 weighing 9, after Dijkstra from 1 has settled two nodes: 1 at 0 and 3 at 1, in green, their edges lit; 2 is at 3, 4 at 6, 6 at 10 and 5 not reached yet. Beside it the heap, smallest first: 3 for node 2, ringed as the next to settle, the nearest; 4 for node 2, dashed, stale, to be skipped; 6 for node 4; 10 for node 6. Under them two cards for the grid of 300 by 300: a plain queue takes cells off 1,595,745 times; a heap settles 90,000 and skips 28,518 stale entries. Focal line: nothing waiting is nearer, so the nearest is final. Title: settle the nearest.

Dijkstra: settle the nearest

Why taking the nearest works: every other vertex waiting is at least as far, and no weight is negative, so no way through them can come back cheaper. The nearest is settled, final. Then relax its edges: for each neighbour, if going through it is cheaper, lower the neighbour's distance and push it on the heap again. The old entry stays in the heap; when it comes off later, longer than the distance now known, skip it. Network Delay Time asks when the last node hears a signal sent from k: the largest of these distances.

import heapq


def network_delay(times, n, k):                # Network Delay Time: times[i] = [from, to, minutes], nodes 1 to n
    adj = [[] for _ in range(n + 1)]
    for u, v, w in times:
        adj[u].append((v, w))
    dist, parent = [float("inf")] * (n + 1), [0] * (n + 1)
    dist[k] = 0
    heap, settled, stale = [(0, k)], [], 0
    while heap:
        d, u = heapq.heappop(heap)
        if d > dist[u]:                        # a longer entry left behind when u improved: skip it
            stale += 1
            continue
        settled.append(u)                      # the nearest not settled yet: nothing found later can beat d
        for v, w in adj[u]:
            if d + w < dist[v]:
                dist[v], parent[v] = d + w, u
                heapq.heappush(heap, (dist[v], v))
    return dist, parent, settled, stale


times = [[1, 2, 4], [1, 3, 1], [3, 2, 2], [2, 4, 1], [3, 4, 5], [4, 5, 3], [5, 6, 1], [3, 6, 9]]
dist, parent, settled, stale = network_delay(times, 6, 1)
print("times from 1:", " ".join(map(str, dist[1:])))
print("settled:", " ".join(map(str, settled)) + f", with {stale} stale entries skipped")
last = max(range(1, 7), key=lambda v: dist[v])
route = [last]
while route[-1] != 1:                          # read the route back from the last node to hear
    route.append(parent[route[-1]])
print(f"all 6 have it after {dist[last]}; the last, {last}, by", " -> ".join(map(str, route[::-1])))

# times from 1: 0 3 1 4 7 8
# settled: 1 3 2 4 5 6, with 3 stale entries skipped
# all 6 have it after 8; the last, 6, by 1 -> 3 -> 2 -> 4 -> 5 -> 6
import java.util.*;

public class Main {
    public static void main(String[] args) {
        int[][] times = {{1, 2, 4}, {1, 3, 1}, {3, 2, 2}, {2, 4, 1}, {3, 4, 5}, {4, 5, 3}, {5, 6, 1}, {3, 6, 9}};
        int n = 6, k = 1;                            // Network Delay Time: {from, to, minutes}, nodes 1 to n
        List<List<int[]>> adj = new ArrayList<>();
        for (int i = 0; i <= n; i++) adj.add(new ArrayList<>());
        for (int[] t : times) adj.get(t[0]).add(new int[] {t[1], t[2]});
        int[] dist = new int[n + 1], parent = new int[n + 1];
        Arrays.fill(dist, Integer.MAX_VALUE);
        dist[k] = 0;
        PriorityQueue<int[]> heap = new PriorityQueue<>((a, b) -> a[0] != b[0] ? Integer.compare(a[0], b[0])
                : Integer.compare(a[1], b[1]));
        heap.add(new int[] {0, k});
        List<Integer> settled = new ArrayList<>();
        int stale = 0;
        while (!heap.isEmpty()) {
            int[] top = heap.poll();
            int d = top[0], u = top[1];
            if (d > dist[u]) {                       // a longer entry left behind when u improved: skip it
                stale++;
                continue;
            }
            settled.add(u);                          // the nearest not settled yet: nothing found later can beat d
            for (int[] e : adj.get(u)) {
                if (d + e[1] < dist[e[0]]) {
                    dist[e[0]] = d + e[1];
                    parent[e[0]] = u;
                    heap.add(new int[] {dist[e[0]], e[0]});
                }
            }
        }
        StringJoiner ds = new StringJoiner(" "), ss = new StringJoiner(" ");
        for (int v = 1; v <= n; v++) ds.add(String.valueOf(dist[v]));
        for (int v : settled) ss.add(String.valueOf(v));
        System.out.println("times from 1: " + ds);
        System.out.println("settled: " + ss + ", with " + stale + " stale entries skipped");
        int last = 1;
        for (int v = 1; v <= n; v++) if (dist[v] > dist[last]) last = v;
        LinkedList<Integer> route = new LinkedList<>();
        for (int v = last; v != k; v = parent[v]) route.addFirst(v);   // read the route back from the last to hear
        route.addFirst(k);
        StringJoiner rs = new StringJoiner(" -> ");
        for (int v : route) rs.add(String.valueOf(v));
        System.out.println("all 6 have it after " + dist[last] + "; the last, " + last + ", by " + rs);
    }
}

// times from 1: 0 3 1 4 7 8
// settled: 1 3 2 4 5 6, with 3 stale entries skipped
// all 6 have it after 8; the last, 6, by 1 -> 3 -> 2 -> 4 -> 5 -> 6
#include <bits/stdc++.h>
using namespace std;

int main() {
    vector<array<int, 3>> times = {{1, 2, 4}, {1, 3, 1}, {3, 2, 2}, {2, 4, 1}, {3, 4, 5}, {4, 5, 3}, {5, 6, 1}, {3, 6, 9}};
    int n = 6, k = 1;                                // Network Delay Time: {from, to, minutes}, nodes 1 to n
    vector<vector<pair<int, int>>> adj(n + 1);
    for (auto& t : times) adj[t[0]].push_back({t[1], t[2]});
    vector<int> dist(n + 1, INT_MAX), parent(n + 1, 0), settled;
    dist[k] = 0;
    priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> heap;
    heap.push({0, k});
    int stale = 0;
    while (!heap.empty()) {
        auto [d, u] = heap.top();
        heap.pop();
        if (d > dist[u]) {                           // a longer entry left behind when u improved: skip it
            stale++;
            continue;
        }
        settled.push_back(u);                        // the nearest not settled yet: nothing found later can beat d
        for (auto [v, w] : adj[u]) {
            if (d + w < dist[v]) {
                dist[v] = d + w;
                parent[v] = u;
                heap.push({dist[v], v});
            }
        }
    }
    cout << "times from 1:";
    for (int v = 1; v <= n; v++) cout << " " << dist[v];
    cout << "\nsettled:";
    for (size_t i = 0; i < settled.size(); i++) cout << (i ? " " : " ") << settled[i];
    cout << ", with " << stale << " stale entries skipped\n";
    int last = 1;
    for (int v = 1; v <= n; v++) if (dist[v] > dist[last]) last = v;
    vector<int> route;
    for (int v = last; v != k; v = parent[v]) route.push_back(v);   // read the route back from the last to hear
    route.push_back(k);
    reverse(route.begin(), route.end());
    cout << "all 6 have it after " << dist[last] << "; the last, " << last << ", by";
    for (size_t i = 0; i < route.size(); i++) cout << (i ? " -> " : " ") << route[i];
    cout << "\n";
}

// times from 1: 0 3 1 4 7 8
// settled: 1 3 2 4 5 6, with 3 stale entries skipped
// all 6 have it after 8; the last, 6, by 1 -> 3 -> 2 -> 4 -> 5 -> 6
class Heap {                                   // a min-heap of arrays, compared item by item
    constructor() { this.a = []; }
    get size() { return this.a.length; }
    less(i, j) {
        const x = this.a[i], y = this.a[j];
        for (let k = 0; k < x.length; k++) if (x[k] !== y[k]) return x[k] < y[k];
        return false;
    }
    push(item) {
        const a = this.a;
        a.push(item);
        for (let i = a.length - 1; i > 0;) {
            const p = (i - 1) >> 1;
            if (!this.less(i, p)) break;
            [a[i], a[p]] = [a[p], a[i]];
            i = p;
        }
    }
    pop() {
        const a = this.a, top = a[0], last = a.pop();
        if (a.length) {
            a[0] = last;
            for (let i = 0; ;) {
                const l = 2 * i + 1, r = l + 1;
                let m = i;
                if (l < a.length && this.less(l, m)) m = l;
                if (r < a.length && this.less(r, m)) m = r;
                if (m === i) break;
                [a[i], a[m]] = [a[m], a[i]];
                i = m;
            }
        }
        return top;
    }
}

const times = [[1, 2, 4], [1, 3, 1], [3, 2, 2], [2, 4, 1], [3, 4, 5], [4, 5, 3], [5, 6, 1], [3, 6, 9]];
const n = 6, k = 1;                            // Network Delay Time: [from, to, minutes], nodes 1 to n
const adj = Array.from({ length: n + 1 }, () => []);
for (const [u, v, w] of times) adj[u].push([v, w]);
const dist = new Array(n + 1).fill(Infinity), parent = new Array(n + 1).fill(0);
dist[k] = 0;
const heap = new Heap();
heap.push([0, k]);
const settled = [];
let stale = 0;
while (heap.size) {
    const [d, u] = heap.pop();
    if (d > dist[u]) {                         // a longer entry left behind when u improved: skip it
        stale++;
        continue;
    }
    settled.push(u);                           // the nearest not settled yet: nothing found later can beat d
    for (const [v, w] of adj[u]) {
        if (d + w < dist[v]) {
            dist[v] = d + w;
            parent[v] = u;
            heap.push([dist[v], v]);
        }
    }
}
console.log(`times from 1: ${dist.slice(1).join(" ")}`);
console.log(`settled: ${settled.join(" ")}, with ${stale} stale entries skipped`);
let last = 1;
for (let v = 1; v <= n; v++) if (dist[v] > dist[last]) last = v;
const route = [last];
while (route[route.length - 1] !== k) route.push(parent[route[route.length - 1]]);   // read the route back
console.log(`all 6 have it after ${dist[last]}; the last, ${last}, by ${route.reverse().join(" -> ")}`);

// times from 1: 0 3 1 4 7 8
// settled: 1 3 2 4 5 6, with 3 stale entries skipped
// all 6 have it after 8; the last, 6, by 1 -> 3 -> 2 -> 4 -> 5 -> 6
class Heap {                                   // a min-heap of arrays, compared item by item
    private a: number[][] = [];
    get size(): number { return this.a.length; }
    private less(i: number, j: number): boolean {
        const x = this.a[i], y = this.a[j];
        for (let k = 0; k < x.length; k++) if (x[k] !== y[k]) return x[k] < y[k];
        return false;
    }
    push(item: number[]): void {
        const a = this.a;
        a.push(item);
        for (let i = a.length - 1; i > 0;) {
            const p = (i - 1) >> 1;
            if (!this.less(i, p)) break;
            [a[i], a[p]] = [a[p], a[i]];
            i = p;
        }
    }
    pop(): number[] {
        const a = this.a, top = a[0], last = a.pop()!;
        if (a.length) {
            a[0] = last;
            for (let i = 0; ;) {
                const l = 2 * i + 1, r = l + 1;
                let m = i;
                if (l < a.length && this.less(l, m)) m = l;
                if (r < a.length && this.less(r, m)) m = r;
                if (m === i) break;
                [a[i], a[m]] = [a[m], a[i]];
                i = m;
            }
        }
        return top;
    }
}

const times: [number, number, number][] = [[1, 2, 4], [1, 3, 1], [3, 2, 2], [2, 4, 1], [3, 4, 5], [4, 5, 3], [5, 6, 1],
                                           [3, 6, 9]];
const n = 6, k = 1;                            // Network Delay Time: [from, to, minutes], nodes 1 to n
const adj: [number, number][][] = Array.from({ length: n + 1 }, () => []);
for (const [u, v, w] of times) adj[u].push([v, w]);
const dist: number[] = new Array(n + 1).fill(Infinity), parent: number[] = new Array(n + 1).fill(0);
dist[k] = 0;
const heap = new Heap();
heap.push([0, k]);
const settled: number[] = [];
let stale = 0;
while (heap.size) {
    const [d, u] = heap.pop();
    if (d > dist[u]) {                         // a longer entry left behind when u improved: skip it
        stale++;
        continue;
    }
    settled.push(u);                           // the nearest not settled yet: nothing found later can beat d
    for (const [v, w] of adj[u]) {
        if (d + w < dist[v]) {
            dist[v] = d + w;
            parent[v] = u;
            heap.push([dist[v], v]);
        }
    }
}
console.log(`times from 1: ${dist.slice(1).join(" ")}`);
console.log(`settled: ${settled.join(" ")}, with ${stale} stale entries skipped`);
let last = 1;
for (let v = 1; v <= n; v++) if (dist[v] > dist[last]) last = v;
const route = [last];
while (route[route.length - 1] !== k) route.push(parent[route[route.length - 1]]);   // read the route back
console.log(`all 6 have it after ${dist[last]}; the last, ${last}, by ${route.reverse().join(" -> ")}`);

// times from 1: 0 3 1 4 7 8
// settled: 1 3 2 4 5 6, with 3 stale entries skipped
// all 6 have it after 8; the last, 6, by 1 -> 3 -> 2 -> 4 -> 5 -> 6
using System;
using System.Collections.Generic;
using System.Linq;

class Program {
    static void Main() {
        int[][] times = { new[] {1, 2, 4}, new[] {1, 3, 1}, new[] {3, 2, 2}, new[] {2, 4, 1}, new[] {3, 4, 5},
                          new[] {4, 5, 3}, new[] {5, 6, 1}, new[] {3, 6, 9} };
        int n = 6, k = 1;                             // Network Delay Time: {from, to, minutes}, nodes 1 to n
        var adj = new List<(int v, int w)>[n + 1];
        for (int i = 0; i <= n; i++) adj[i] = new List<(int, int)>();
        foreach (var t in times) adj[t[0]].Add((t[1], t[2]));
        var dist = new int[n + 1];
        var parent = new int[n + 1];
        Array.Fill(dist, int.MaxValue);
        dist[k] = 0;
        var heap = new PriorityQueue<int, (int, int)>();   // ordered by (distance, node)
        heap.Enqueue(k, (0, k));
        var settled = new List<int>();
        int stale = 0;
        while (heap.TryDequeue(out int u, out var key)) {
            int d = key.Item1;
            if (d > dist[u]) {                        // a longer entry left behind when u improved: skip it
                stale++;
                continue;
            }
            settled.Add(u);                           // the nearest not settled yet: nothing found later can beat d
            foreach (var (v, w) in adj[u]) {
                if (d + w < dist[v]) {
                    dist[v] = d + w;
                    parent[v] = u;
                    heap.Enqueue(v, (dist[v], v));
                }
            }
        }
        Console.WriteLine($"times from 1: {string.Join(" ", dist.Skip(1))}");
        Console.WriteLine($"settled: {string.Join(" ", settled)}, with {stale} stale entries skipped");
        int last = 1;
        for (int v = 1; v <= n; v++) if (dist[v] > dist[last]) last = v;
        var route = new List<int>();
        for (int v = last; v != k; v = parent[v]) route.Insert(0, v);   // read the route back from the last to hear
        route.Insert(0, k);
        Console.WriteLine($"all 6 have it after {dist[last]}; the last, {last}, by {string.Join(" -> ", route)}");
    }
}

// times from 1: 0 3 1 4 7 8
// settled: 1 3 2 4 5 6, with 3 stale entries skipped
// all 6 have it after 8; the last, 6, by 1 -> 3 -> 2 -> 4 -> 5 -> 6
package main

import (
	"container/heap"
	"fmt"
	"math"
	"strconv"
	"strings"
)

type item struct{ d, v int }
type minHeap []item // the smallest distance on top, the smaller vertex first on a tie

func (h minHeap) Len() int            { return len(h) }
func (h minHeap) Less(i, j int) bool  { return h[i].d < h[j].d || (h[i].d == h[j].d && h[i].v < h[j].v) }
func (h minHeap) Swap(i, j int)       { h[i], h[j] = h[j], h[i] }
func (h *minHeap) Push(x interface{}) { *h = append(*h, x.(item)) }
func (h *minHeap) Pop() interface{} {
	old := *h
	x := old[len(old)-1]
	*h = old[:len(old)-1]
	return x
}

func joined(xs []int, sep string) string {
	parts := make([]string, len(xs))
	for i, x := range xs {
		parts[i] = strconv.Itoa(x)
	}
	return strings.Join(parts, sep)
}

func main() {
	times := [][3]int{{1, 2, 4}, {1, 3, 1}, {3, 2, 2}, {2, 4, 1}, {3, 4, 5}, {4, 5, 3}, {5, 6, 1}, {3, 6, 9}}
	n, k := 6, 1 // Network Delay Time: {from, to, minutes}, nodes 1 to n
	adj := make([][][2]int, n+1)
	for _, t := range times {
		adj[t[0]] = append(adj[t[0]], [2]int{t[1], t[2]})
	}
	dist, parent := make([]int, n+1), make([]int, n+1)
	for i := range dist {
		dist[i] = math.MaxInt64
	}
	dist[k] = 0
	h := &minHeap{{0, k}}
	settled, stale := []int{}, 0
	for h.Len() > 0 {
		top := heap.Pop(h).(item)
		if top.d > dist[top.v] { // a longer entry left behind when v improved: skip it
			stale++
			continue
		}
		settled = append(settled, top.v) // the nearest not settled yet: nothing found later can beat d
		for _, e := range adj[top.v] {
			if top.d+e[1] < dist[e[0]] {
				dist[e[0]], parent[e[0]] = top.d+e[1], top.v
				heap.Push(h, item{dist[e[0]], e[0]})
			}
		}
	}
	fmt.Println("times from 1:", joined(dist[1:], " "))
	fmt.Printf("settled: %s, with %d stale entries skipped\n", joined(settled, " "), stale)
	last := 1
	for v := 1; v <= n; v++ {
		if dist[v] > dist[last] {
			last = v
		}
	}
	route := []int{}
	for v := last; v != k; v = parent[v] { // read the route back from the last to hear
		route = append([]int{v}, route...)
	}
	route = append([]int{k}, route...)
	fmt.Printf("all 6 have it after %d; the last, %d, by %s\n", dist[last], last, joined(route, " -> "))
}

// times from 1: 0 3 1 4 7 8
// settled: 1 3 2 4 5 6, with 3 stale entries skipped
// all 6 have it after 8; the last, 6, by 1 -> 3 -> 2 -> 4 -> 5 -> 6

After 1 itself, node 3 comes off at 1 and lowers 2 from 4 to 3, leaving 2's old 4 in the heap. 4's first entry, 6 through 3, is beaten by 4 through 2, and 6's 10 by 8 through 5: with 2's old 4, those are the 3 stale entries. The last to hear is 6, after 8, along 1 → 3 → 2 → 4 → 5 → 6, read back from the parents. With a binary heap that's O((V + E) log V). Dijkstra's Algorithm asks for exactly these distances, and Dijkstra: Queue vs Priority Queue for the difference the grid above showed.

Settle the Nearest. Run it yourself: settle the nearest node each time and watch its edges relax; then take the heap's entries off one by one, settling each or skipping it as stale; then read 6's route back to 1 from the parents.

Settle the Nearest: settle the nearest node yourself, settle or skip each entry the heap gives back, and read the route back from the parents

A negative edge

The proof leaned on one fact: no weight is negative. Break it, and a vertex settled early can be undercut later, through a vertex that was still waiting:

import heapq

edges = [(0, 1, 2), (0, 2, 5), (2, 1, -4), (1, 3, 1)]   # the edge 2 -> 1 weighs -4
n = 4
adj = [[] for _ in range(n)]
for u, v, w in edges:
    adj[u].append((v, w))

dist, done = [float("inf")] * n, [False] * n   # Dijkstra: a vertex is final once it comes off the heap
dist[0] = 0
heap = [(0, 0)]
while heap:
    d, u = heapq.heappop(heap)
    if done[u]:
        continue
    done[u] = True
    for v, w in adj[u]:
        if not done[v] and d + w < dist[v]:
            dist[v] = d + w
            heapq.heappush(heap, (dist[v], v))
print("Dijkstra:", " ".join(map(str, dist)))

best = [float("inf")] * n                      # Bellman-Ford: every edge, n - 1 times over
best[0] = 0
for _ in range(n - 1):
    for u, v, w in edges:
        if best[u] + w < best[v]:
            best[v] = best[u] + w
print("Bellman-Ford:", " ".join(map(str, best)))

# Dijkstra: 0 2 5 3
# Bellman-Ford: 0 1 5 2
import java.util.*;

public class Main {
    public static void main(String[] args) {
        int[][] edges = {{0, 1, 2}, {0, 2, 5}, {2, 1, -4}, {1, 3, 1}};   // the edge 2 -> 1 weighs -4
        int n = 4;
        List<List<int[]>> adj = new ArrayList<>();
        for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
        for (int[] e : edges) adj.get(e[0]).add(new int[] {e[1], e[2]});

        int[] dist = new int[n];                     // Dijkstra: a vertex is final once it comes off the heap
        boolean[] done = new boolean[n];
        Arrays.fill(dist, Integer.MAX_VALUE);
        dist[0] = 0;
        PriorityQueue<int[]> heap = new PriorityQueue<>((a, b) -> a[0] != b[0] ? Integer.compare(a[0], b[0])
                : Integer.compare(a[1], b[1]));
        heap.add(new int[] {0, 0});
        while (!heap.isEmpty()) {
            int[] top = heap.poll();
            int d = top[0], u = top[1];
            if (done[u]) continue;
            done[u] = true;
            for (int[] e : adj.get(u)) {
                if (!done[e[0]] && d + e[1] < dist[e[0]]) {
                    dist[e[0]] = d + e[1];
                    heap.add(new int[] {dist[e[0]], e[0]});
                }
            }
        }
        StringJoiner a = new StringJoiner(" ");
        for (int d : dist) a.add(String.valueOf(d));
        System.out.println("Dijkstra: " + a);

        int[] best = new int[n];                     // Bellman-Ford: every edge, n - 1 times over
        Arrays.fill(best, Integer.MAX_VALUE);
        best[0] = 0;
        for (int round = 0; round < n - 1; round++) {
            for (int[] e : edges) {
                if (best[e[0]] != Integer.MAX_VALUE && best[e[0]] + e[2] < best[e[1]]) best[e[1]] = best[e[0]] + e[2];
            }
        }
        StringJoiner b = new StringJoiner(" ");
        for (int d : best) b.add(String.valueOf(d));
        System.out.println("Bellman-Ford: " + b);
    }
}

// Dijkstra: 0 2 5 3
// Bellman-Ford: 0 1 5 2
#include <bits/stdc++.h>
using namespace std;

int main() {
    vector<array<int, 3>> edges = {{0, 1, 2}, {0, 2, 5}, {2, 1, -4}, {1, 3, 1}};   // the edge 2 -> 1 weighs -4
    int n = 4;
    vector<vector<pair<int, int>>> adj(n);
    for (auto& e : edges) adj[e[0]].push_back({e[1], e[2]});

    vector<int> dist(n, INT_MAX);                    // Dijkstra: a vertex is final once it comes off the heap
    vector<char> done(n, 0);
    dist[0] = 0;
    priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> heap;
    heap.push({0, 0});
    while (!heap.empty()) {
        auto [d, u] = heap.top();
        heap.pop();
        if (done[u]) continue;
        done[u] = 1;
        for (auto [v, w] : adj[u]) {
            if (!done[v] && d + w < dist[v]) {
                dist[v] = d + w;
                heap.push({dist[v], v});
            }
        }
    }
    cout << "Dijkstra:";
    for (int d : dist) cout << " " << d;
    cout << "\n";

    vector<int> best(n, INT_MAX);                    // Bellman-Ford: every edge, n - 1 times over
    best[0] = 0;
    for (int round = 0; round < n - 1; round++)
        for (auto& e : edges)
            if (best[e[0]] != INT_MAX && best[e[0]] + e[2] < best[e[1]]) best[e[1]] = best[e[0]] + e[2];
    cout << "Bellman-Ford:";
    for (int d : best) cout << " " << d;
    cout << "\n";
}

// Dijkstra: 0 2 5 3
// Bellman-Ford: 0 1 5 2
class Heap {                                   // a min-heap of arrays, compared item by item
    constructor() { this.a = []; }
    get size() { return this.a.length; }
    less(i, j) {
        const x = this.a[i], y = this.a[j];
        for (let k = 0; k < x.length; k++) if (x[k] !== y[k]) return x[k] < y[k];
        return false;
    }
    push(item) {
        const a = this.a;
        a.push(item);
        for (let i = a.length - 1; i > 0;) {
            const p = (i - 1) >> 1;
            if (!this.less(i, p)) break;
            [a[i], a[p]] = [a[p], a[i]];
            i = p;
        }
    }
    pop() {
        const a = this.a, top = a[0], last = a.pop();
        if (a.length) {
            a[0] = last;
            for (let i = 0; ;) {
                const l = 2 * i + 1, r = l + 1;
                let m = i;
                if (l < a.length && this.less(l, m)) m = l;
                if (r < a.length && this.less(r, m)) m = r;
                if (m === i) break;
                [a[i], a[m]] = [a[m], a[i]];
                i = m;
            }
        }
        return top;
    }
}

const edges = [[0, 1, 2], [0, 2, 5], [2, 1, -4], [1, 3, 1]];   // the edge 2 -> 1 weighs -4
const n = 4;
const adj = Array.from({ length: n }, () => []);
for (const [u, v, w] of edges) adj[u].push([v, w]);

const dist = new Array(n).fill(Infinity), done = new Array(n).fill(false);   // Dijkstra: final once off the heap
dist[0] = 0;
const heap = new Heap();
heap.push([0, 0]);
while (heap.size) {
    const [d, u] = heap.pop();
    if (done[u]) continue;
    done[u] = true;
    for (const [v, w] of adj[u]) {
        if (!done[v] && d + w < dist[v]) {
            dist[v] = d + w;
            heap.push([dist[v], v]);
        }
    }
}
console.log(`Dijkstra: ${dist.join(" ")}`);

const best = new Array(n).fill(Infinity);      // Bellman-Ford: every edge, n - 1 times over
best[0] = 0;
for (let round = 0; round < n - 1; round++)
    for (const [u, v, w] of edges) if (best[u] + w < best[v]) best[v] = best[u] + w;
console.log(`Bellman-Ford: ${best.join(" ")}`);

// Dijkstra: 0 2 5 3
// Bellman-Ford: 0 1 5 2
class Heap {                                   // a min-heap of arrays, compared item by item
    private a: number[][] = [];
    get size(): number { return this.a.length; }
    private less(i: number, j: number): boolean {
        const x = this.a[i], y = this.a[j];
        for (let k = 0; k < x.length; k++) if (x[k] !== y[k]) return x[k] < y[k];
        return false;
    }
    push(item: number[]): void {
        const a = this.a;
        a.push(item);
        for (let i = a.length - 1; i > 0;) {
            const p = (i - 1) >> 1;
            if (!this.less(i, p)) break;
            [a[i], a[p]] = [a[p], a[i]];
            i = p;
        }
    }
    pop(): number[] {
        const a = this.a, top = a[0], last = a.pop()!;
        if (a.length) {
            a[0] = last;
            for (let i = 0; ;) {
                const l = 2 * i + 1, r = l + 1;
                let m = i;
                if (l < a.length && this.less(l, m)) m = l;
                if (r < a.length && this.less(r, m)) m = r;
                if (m === i) break;
                [a[i], a[m]] = [a[m], a[i]];
                i = m;
            }
        }
        return top;
    }
}

const edges: [number, number, number][] = [[0, 1, 2], [0, 2, 5], [2, 1, -4], [1, 3, 1]];   // 2 -> 1 weighs -4
const n = 4;
const adj: [number, number][][] = Array.from({ length: n }, () => []);
for (const [u, v, w] of edges) adj[u].push([v, w]);

const dist: number[] = new Array(n).fill(Infinity);   // Dijkstra: a vertex is final once it comes off the heap
const done: boolean[] = new Array(n).fill(false);
dist[0] = 0;
const heap = new Heap();
heap.push([0, 0]);
while (heap.size) {
    const [d, u] = heap.pop();
    if (done[u]) continue;
    done[u] = true;
    for (const [v, w] of adj[u]) {
        if (!done[v] && d + w < dist[v]) {
            dist[v] = d + w;
            heap.push([dist[v], v]);
        }
    }
}
console.log(`Dijkstra: ${dist.join(" ")}`);

const best: number[] = new Array(n).fill(Infinity);   // Bellman-Ford: every edge, n - 1 times over
best[0] = 0;
for (let round = 0; round < n - 1; round++)
    for (const [u, v, w] of edges) if (best[u] + w < best[v]) best[v] = best[u] + w;
console.log(`Bellman-Ford: ${best.join(" ")}`);

// Dijkstra: 0 2 5 3
// Bellman-Ford: 0 1 5 2
using System;
using System.Collections.Generic;

class Program {
    static void Main() {
        int[][] edges = { new[] {0, 1, 2}, new[] {0, 2, 5}, new[] {2, 1, -4}, new[] {1, 3, 1} };   // 2 -> 1 weighs -4
        int n = 4;
        var adj = new List<(int v, int w)>[n];
        for (int i = 0; i < n; i++) adj[i] = new List<(int, int)>();
        foreach (var e in edges) adj[e[0]].Add((e[1], e[2]));

        var dist = new int[n];                        // Dijkstra: a vertex is final once it comes off the heap
        var done = new bool[n];
        Array.Fill(dist, int.MaxValue);
        dist[0] = 0;
        var heap = new PriorityQueue<int, (int, int)>();
        heap.Enqueue(0, (0, 0));
        while (heap.TryDequeue(out int u, out var key)) {
            int d = key.Item1;
            if (done[u]) continue;
            done[u] = true;
            foreach (var (v, w) in adj[u]) {
                if (!done[v] && d + w < dist[v]) {
                    dist[v] = d + w;
                    heap.Enqueue(v, (dist[v], v));
                }
            }
        }
        Console.WriteLine($"Dijkstra: {string.Join(" ", dist)}");

        var best = new int[n];                        // Bellman-Ford: every edge, n - 1 times over
        Array.Fill(best, int.MaxValue);
        best[0] = 0;
        for (int round = 0; round < n - 1; round++)
            foreach (var e in edges)
                if (best[e[0]] != int.MaxValue && best[e[0]] + e[2] < best[e[1]]) best[e[1]] = best[e[0]] + e[2];
        Console.WriteLine($"Bellman-Ford: {string.Join(" ", best)}");
    }
}

// Dijkstra: 0 2 5 3
// Bellman-Ford: 0 1 5 2
package main

import (
	"container/heap"
	"fmt"
	"math"
	"strconv"
	"strings"
)

type item struct{ d, v int }
type minHeap []item // the smallest distance on top, the smaller vertex first on a tie

func (h minHeap) Len() int            { return len(h) }
func (h minHeap) Less(i, j int) bool  { return h[i].d < h[j].d || (h[i].d == h[j].d && h[i].v < h[j].v) }
func (h minHeap) Swap(i, j int)       { h[i], h[j] = h[j], h[i] }
func (h *minHeap) Push(x interface{}) { *h = append(*h, x.(item)) }
func (h *minHeap) Pop() interface{} {
	old := *h
	x := old[len(old)-1]
	*h = old[:len(old)-1]
	return x
}

func joined(xs []int) string {
	parts := make([]string, len(xs))
	for i, x := range xs {
		parts[i] = strconv.Itoa(x)
	}
	return strings.Join(parts, " ")
}

func main() {
	edges := [][3]int{{0, 1, 2}, {0, 2, 5}, {2, 1, -4}, {1, 3, 1}} // the edge 2 -> 1 weighs -4
	n := 4
	adj := make([][][2]int, n)
	for _, e := range edges {
		adj[e[0]] = append(adj[e[0]], [2]int{e[1], e[2]})
	}

	dist, done := make([]int, n), make([]bool, n) // Dijkstra: a vertex is final once it comes off the heap
	for i := range dist {
		dist[i] = math.MaxInt64
	}
	dist[0] = 0
	h := &minHeap{{0, 0}}
	for h.Len() > 0 {
		top := heap.Pop(h).(item)
		if done[top.v] {
			continue
		}
		done[top.v] = true
		for _, e := range adj[top.v] {
			if !done[e[0]] && top.d+e[1] < dist[e[0]] {
				dist[e[0]] = top.d + e[1]
				heap.Push(h, item{dist[e[0]], e[0]})
			}
		}
	}
	fmt.Println("Dijkstra:", joined(dist))

	best := make([]int, n) // Bellman-Ford: every edge, n - 1 times over
	for i := range best {
		best[i] = math.MaxInt64
	}
	best[0] = 0
	for round := 0; round < n-1; round++ {
		for _, e := range edges {
			if best[e[0]] != math.MaxInt64 && best[e[0]]+e[2] < best[e[1]] {
				best[e[1]] = best[e[0]] + e[2]
			}
		}
	}
	fmt.Println("Bellman-Ford:", joined(best))
}

// Dijkstra: 0 2 5 3
// Bellman-Ford: 0 1 5 2

Dijkstra settled 1 at 2, straight from 0, and then 3 at 3 through it. Only after that did 2 come off, at 5, with its edge of −4 back to 1. 1 was final already, so the cheaper way, 5 − 4 = 1, was never used, and neither was the 2 it would have given 3. Bellman-Ford trusts no order: it relaxes every edge, round after round, and gets 0 1 5 2. When weights can be negative it's the one to reach for, at a price.

The lesson's graph of four vertices: 0 to 1 weighing 2, 0 to 2 weighing 5, 2 to 1 weighing −4, drawn dashed in red, and 1 to 3 weighing 1. Dijkstra, as 2 comes off: 0 at 0, 1 at 2 and 3 at 3 settled in green, 2 at 5 ringed in orange. Beside it the order they were settled, nearest first: 0 at 0, the start; 1 at 2, final; 3 at 3, final; 2 at 5, its −4 too late. Under them two cards: Dijkstra gives 0 2 5 3, since 1 was final before 2 came off; Bellman-Ford gives 0 1 5 2, since 0 → 2 → 1 costs 5 − 4 = 1. Focal line: a negative edge can undercut a vertex already settled. Title: a negative edge.

The largest step, not the sum

Path With Minimum Effort measures a route by its largest climb or drop between neighbouring cells, not by their sum. Dijkstra still works with max where the sum was: extending a route never makes its largest step smaller, so the nearest-first promise still holds:

import heapq


def least_effort(heights):                     # Path With Minimum Effort: a route costs its largest climb or drop
    R, C = len(heights), len(heights[0])
    best = [[float("inf")] * C for _ in range(R)]
    parent = [[None] * C for _ in range(R)]
    best[0][0] = 0
    heap = [(0, 0, 0)]
    while heap:
        e, r, c = heapq.heappop(heap)
        if e > best[r][c]:
            continue
        if (r, c) == (R - 1, C - 1):
            break
        for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
            nr, nc = r + dr, c + dc
            if 0 <= nr < R and 0 <= nc < C:
                ne = max(e, abs(heights[nr][nc] - heights[r][c]))   # max in place of +: never smaller than e
                if ne < best[nr][nc]:
                    best[nr][nc], parent[nr][nc] = ne, (r, c)
                    heapq.heappush(heap, (ne, nr, nc))
    route, cell = [], (R - 1, C - 1)
    while cell:
        route.append(heights[cell[0]][cell[1]])
        cell = parent[cell[0]][cell[1]]
    return best[R - 1][C - 1], route[::-1]


effort, route = least_effort([[4, 8, 6], [7, 1, 9], [5, 3, 2]])
print("the least effort, corner to corner:", effort, "| by the heights", " ".join(map(str, route)))

# the least effort, corner to corner: 3 | by the heights 4 7 5 3 2
import java.util.*;

public class Main {
    public static void main(String[] args) {
        int[][] heights = {{4, 8, 6}, {7, 1, 9}, {5, 3, 2}};   // Path With Minimum Effort: a route costs its largest step
        int R = heights.length, C = heights[0].length;
        int[][] best = new int[R][C], parent = new int[R][C];
        for (int[] row : best) Arrays.fill(row, Integer.MAX_VALUE);
        best[0][0] = 0;
        parent[0][0] = -1;
        PriorityQueue<int[]> heap = new PriorityQueue<>((a, b) -> a[0] != b[0] ? Integer.compare(a[0], b[0])
                : Integer.compare(a[1], b[1]));
        heap.add(new int[] {0, 0});                  // {effort, cell r * C + c}
        int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
        while (!heap.isEmpty()) {
            int[] top = heap.poll();
            int e = top[0], r = top[1] / C, c = top[1] % C;
            if (e > best[r][c]) continue;
            if (r == R - 1 && c == C - 1) break;
            for (int[] d : dirs) {
                int nr = r + d[0], nc = c + d[1];
                if (nr < 0 || nr >= R || nc < 0 || nc >= C) continue;
                int ne = Math.max(e, Math.abs(heights[nr][nc] - heights[r][c]));   // max in place of +
                if (ne < best[nr][nc]) {
                    best[nr][nc] = ne;
                    parent[nr][nc] = r * C + c;
                    heap.add(new int[] {ne, nr * C + nc});
                }
            }
        }
        LinkedList<Integer> route = new LinkedList<>();
        for (int cell = (R - 1) * C + C - 1; cell != -1; cell = parent[cell / C][cell % C])
            route.addFirst(heights[cell / C][cell % C]);
        StringJoiner s = new StringJoiner(" ");
        for (int h : route) s.add(String.valueOf(h));
        System.out.println("the least effort, corner to corner: " + best[R - 1][C - 1] + " | by the heights " + s);
    }
}

// the least effort, corner to corner: 3 | by the heights 4 7 5 3 2
#include <bits/stdc++.h>
using namespace std;

int main() {
    vector<vector<int>> heights = {{4, 8, 6}, {7, 1, 9}, {5, 3, 2}};   // a route costs its largest climb or drop
    int R = heights.size(), C = heights[0].size();
    vector<vector<int>> best(R, vector<int>(C, INT_MAX)), parent(R, vector<int>(C, -1));
    best[0][0] = 0;
    priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> heap;   // {effort, cell r * C + c}
    heap.push({0, 0});
    const int dirs[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
    while (!heap.empty()) {
        auto [e, cell] = heap.top();
        heap.pop();
        int r = cell / C, c = cell % C;
        if (e > best[r][c]) continue;
        if (r == R - 1 && c == C - 1) break;
        for (auto& d : dirs) {
            int nr = r + d[0], nc = c + d[1];
            if (nr < 0 || nr >= R || nc < 0 || nc >= C) continue;
            int ne = max(e, abs(heights[nr][nc] - heights[r][c]));   // max in place of +: never smaller than e
            if (ne < best[nr][nc]) {
                best[nr][nc] = ne;
                parent[nr][nc] = cell;
                heap.push({ne, nr * C + nc});
            }
        }
    }
    vector<int> route;
    for (int cell = (R - 1) * C + C - 1; cell != -1; cell = parent[cell / C][cell % C])
        route.push_back(heights[cell / C][cell % C]);
    reverse(route.begin(), route.end());
    cout << "the least effort, corner to corner: " << best[R - 1][C - 1] << " | by the heights";
    for (int h : route) cout << " " << h;
    cout << "\n";
}

// the least effort, corner to corner: 3 | by the heights 4 7 5 3 2
class Heap {                                   // a min-heap of arrays, compared item by item
    constructor() { this.a = []; }
    get size() { return this.a.length; }
    less(i, j) {
        const x = this.a[i], y = this.a[j];
        for (let k = 0; k < x.length; k++) if (x[k] !== y[k]) return x[k] < y[k];
        return false;
    }
    push(item) {
        const a = this.a;
        a.push(item);
        for (let i = a.length - 1; i > 0;) {
            const p = (i - 1) >> 1;
            if (!this.less(i, p)) break;
            [a[i], a[p]] = [a[p], a[i]];
            i = p;
        }
    }
    pop() {
        const a = this.a, top = a[0], last = a.pop();
        if (a.length) {
            a[0] = last;
            for (let i = 0; ;) {
                const l = 2 * i + 1, r = l + 1;
                let m = i;
                if (l < a.length && this.less(l, m)) m = l;
                if (r < a.length && this.less(r, m)) m = r;
                if (m === i) break;
                [a[i], a[m]] = [a[m], a[i]];
                i = m;
            }
        }
        return top;
    }
}

const heights = [[4, 8, 6], [7, 1, 9], [5, 3, 2]];   // Path With Minimum Effort: a route costs its largest step
const R = heights.length, C = heights[0].length;
const best = Array.from({ length: R }, () => new Array(C).fill(Infinity));
const parent = Array.from({ length: R }, () => new Array(C).fill(-1));
best[0][0] = 0;
const heap = new Heap();
heap.push([0, 0]);                             // [effort, cell r * C + c]
while (heap.size) {
    const [e, cell] = heap.pop();
    const r = Math.floor(cell / C), c = cell % C;
    if (e > best[r][c]) continue;
    if (r === R - 1 && c === C - 1) break;
    for (const [dr, dc] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
        const nr = r + dr, nc = c + dc;
        if (nr < 0 || nr >= R || nc < 0 || nc >= C) continue;
        const ne = Math.max(e, Math.abs(heights[nr][nc] - heights[r][c]));   // max in place of +
        if (ne < best[nr][nc]) {
            best[nr][nc] = ne;
            parent[nr][nc] = cell;
            heap.push([ne, nr * C + nc]);
        }
    }
}
const route = [];
for (let cell = (R - 1) * C + C - 1; cell !== -1; cell = parent[Math.floor(cell / C)][cell % C])
    route.unshift(heights[Math.floor(cell / C)][cell % C]);
console.log(`the least effort, corner to corner: ${best[R - 1][C - 1]} | by the heights ${route.join(" ")}`);

// the least effort, corner to corner: 3 | by the heights 4 7 5 3 2
class Heap {                                   // a min-heap of arrays, compared item by item
    private a: number[][] = [];
    get size(): number { return this.a.length; }
    private less(i: number, j: number): boolean {
        const x = this.a[i], y = this.a[j];
        for (let k = 0; k < x.length; k++) if (x[k] !== y[k]) return x[k] < y[k];
        return false;
    }
    push(item: number[]): void {
        const a = this.a;
        a.push(item);
        for (let i = a.length - 1; i > 0;) {
            const p = (i - 1) >> 1;
            if (!this.less(i, p)) break;
            [a[i], a[p]] = [a[p], a[i]];
            i = p;
        }
    }
    pop(): number[] {
        const a = this.a, top = a[0], last = a.pop()!;
        if (a.length) {
            a[0] = last;
            for (let i = 0; ;) {
                const l = 2 * i + 1, r = l + 1;
                let m = i;
                if (l < a.length && this.less(l, m)) m = l;
                if (r < a.length && this.less(r, m)) m = r;
                if (m === i) break;
                [a[i], a[m]] = [a[m], a[i]];
                i = m;
            }
        }
        return top;
    }
}

const heights: number[][] = [[4, 8, 6], [7, 1, 9], [5, 3, 2]];   // a route costs its largest climb or drop
const R = heights.length, C = heights[0].length;
const best: number[][] = Array.from({ length: R }, () => new Array(C).fill(Infinity));
const parent: number[][] = Array.from({ length: R }, () => new Array(C).fill(-1));
best[0][0] = 0;
const heap = new Heap();
heap.push([0, 0]);                             // [effort, cell r * C + c]
while (heap.size) {
    const [e, cell] = heap.pop();
    const r = Math.floor(cell / C), c = cell % C;
    if (e > best[r][c]) continue;
    if (r === R - 1 && c === C - 1) break;
    for (const [dr, dc] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
        const nr = r + dr, nc = c + dc;
        if (nr < 0 || nr >= R || nc < 0 || nc >= C) continue;
        const ne = Math.max(e, Math.abs(heights[nr][nc] - heights[r][c]));   // max in place of +
        if (ne < best[nr][nc]) {
            best[nr][nc] = ne;
            parent[nr][nc] = cell;
            heap.push([ne, nr * C + nc]);
        }
    }
}
const route: number[] = [];
for (let cell = (R - 1) * C + C - 1; cell !== -1; cell = parent[Math.floor(cell / C)][cell % C])
    route.unshift(heights[Math.floor(cell / C)][cell % C]);
console.log(`the least effort, corner to corner: ${best[R - 1][C - 1]} | by the heights ${route.join(" ")}`);

// the least effort, corner to corner: 3 | by the heights 4 7 5 3 2
using System;
using System.Collections.Generic;

class Program {
    static void Main() {
        int[,] heights = { {4, 8, 6}, {7, 1, 9}, {5, 3, 2} };   // Path With Minimum Effort: a route costs its largest step
        int R = heights.GetLength(0), C = heights.GetLength(1);
        var best = new int[R, C];
        var parent = new int[R, C];
        for (int r = 0; r < R; r++) for (int c = 0; c < C; c++) best[r, c] = int.MaxValue;
        best[0, 0] = 0;
        parent[0, 0] = -1;
        var heap = new PriorityQueue<int, (int, int)>();   // a cell r * C + c, ordered by (effort, cell)
        heap.Enqueue(0, (0, 0));
        int[][] dirs = { new[] {1, 0}, new[] {-1, 0}, new[] {0, 1}, new[] {0, -1} };
        while (heap.TryDequeue(out int cell, out var key)) {
            int e = key.Item1, r = cell / C, c = cell % C;
            if (e > best[r, c]) continue;
            if (r == R - 1 && c == C - 1) break;
            foreach (var d in dirs) {
                int nr = r + d[0], nc = c + d[1];
                if (nr < 0 || nr >= R || nc < 0 || nc >= C) continue;
                int ne = Math.Max(e, Math.Abs(heights[nr, nc] - heights[r, c]));   // max in place of +
                if (ne < best[nr, nc]) {
                    best[nr, nc] = ne;
                    parent[nr, nc] = cell;
                    heap.Enqueue(nr * C + nc, (ne, nr * C + nc));
                }
            }
        }
        var route = new List<int>();
        for (int cell = (R - 1) * C + C - 1; cell != -1; cell = parent[cell / C, cell % C])
            route.Insert(0, heights[cell / C, cell % C]);
        Console.WriteLine($"the least effort, corner to corner: {best[R - 1, C - 1]} | by the heights {string.Join(" ", route)}");
    }
}

// the least effort, corner to corner: 3 | by the heights 4 7 5 3 2
package main

import (
	"container/heap"
	"fmt"
	"math"
	"strconv"
	"strings"
)

type item struct{ d, v int }
type minHeap []item // the smallest distance on top, the smaller vertex first on a tie

func (h minHeap) Len() int            { return len(h) }
func (h minHeap) Less(i, j int) bool  { return h[i].d < h[j].d || (h[i].d == h[j].d && h[i].v < h[j].v) }
func (h minHeap) Swap(i, j int)       { h[i], h[j] = h[j], h[i] }
func (h *minHeap) Push(x interface{}) { *h = append(*h, x.(item)) }
func (h *minHeap) Pop() interface{} {
	old := *h
	x := old[len(old)-1]
	*h = old[:len(old)-1]
	return x
}

func abs(x int) int {
	if x < 0 {
		return -x
	}
	return x
}

func main() {
	heights := [][]int{{4, 8, 6}, {7, 1, 9}, {5, 3, 2}} // Path With Minimum Effort: a route costs its largest step
	R, C := len(heights), len(heights[0])
	best, parent := make([][]int, R), make([][]int, R)
	for r := range best {
		best[r], parent[r] = make([]int, C), make([]int, C)
		for c := range best[r] {
			best[r][c] = math.MaxInt64
		}
	}
	best[0][0], parent[0][0] = 0, -1
	h := &minHeap{{0, 0}} // {effort, cell r * C + c}
	dirs := [4][2]int{{1, 0}, {-1, 0}, {0, 1}, {0, -1}}
	for h.Len() > 0 {
		top := heap.Pop(h).(item)
		r, c := top.v/C, top.v%C
		if top.d > best[r][c] {
			continue
		}
		if r == R-1 && c == C-1 {
			break
		}
		for _, d := range dirs {
			nr, nc := r+d[0], c+d[1]
			if nr < 0 || nr >= R || nc < 0 || nc >= C {
				continue
			}
			ne := top.d // max in place of +: never smaller than the effort so far
			if step := abs(heights[nr][nc] - heights[r][c]); step > ne {
				ne = step
			}
			if ne < best[nr][nc] {
				best[nr][nc], parent[nr][nc] = ne, top.v
				heap.Push(h, item{ne, nr*C + nc})
			}
		}
	}
	route := []string{}
	for cell := (R-1)*C + C - 1; cell != -1; cell = parent[cell/C][cell%C] {
		route = append([]string{strconv.Itoa(heights[cell/C][cell%C])}, route...)
	}
	fmt.Printf("the least effort, corner to corner: %d | by the heights %s\n", best[R-1][C-1], strings.Join(route, " "))
}

// the least effort, corner to corner: 3 | by the heights 4 7 5 3 2

The route 4, 7, 5, 3, 2 climbs or drops by 3, 2, 2 and 1. Every route leaves 4 by a step of at least 3, to 7 or to 8, so 3 is the least effort there is. The same largest-step idea comes back in Swim in Rising Water, in Union-Find and Minimum Spanning Trees.

Counting the shortest paths

Number of Ways to Arrive at Destination counts the shortest routes. Keep a count beside each distance: a strictly shorter way replaces the count with the count of the vertex it came through, and an equally short way adds that count. Since vertices are settled nearest first, a vertex's count is complete before anything relaxes through it:

import heapq

MOD = 10 ** 9 + 7


def count_ways(n, roads):                      # Number of Ways to Arrive at Destination: two-way roads [u, v, time]
    adj = [[] for _ in range(n)]
    for u, v, t in roads:
        adj[u].append((v, t))
        adj[v].append((u, t))
    dist, ways = [float("inf")] * n, [0] * n
    dist[0], ways[0] = 0, 1
    heap = [(0, 0)]
    while heap:
        d, u = heapq.heappop(heap)
        if d > dist[u]:
            continue
        for v, t in adj[u]:
            if d + t < dist[v]:                # a shorter way: the old ways no longer count
                dist[v], ways[v] = d + t, ways[u]
                heapq.heappush(heap, (dist[v], v))
            elif d + t == dist[v]:             # as short: u's ways are more ways to v
                ways[v] = (ways[v] + ways[u]) % MOD
    return dist[n - 1], ways[n - 1]


best, ways = count_ways(5, [[0, 1, 2], [0, 2, 3], [1, 3, 2], [2, 3, 1], [3, 4, 2], [1, 4, 5], [0, 4, 6]])
print(f"the shortest time to 4: {best}, reached {ways} ways")

# the shortest time to 4: 6, reached 3 ways
import java.util.*;

public class Main {
    public static void main(String[] args) {
        int n = 5;                                   // Number of Ways to Arrive at Destination: two-way roads
        int[][] roads = {{0, 1, 2}, {0, 2, 3}, {1, 3, 2}, {2, 3, 1}, {3, 4, 2}, {1, 4, 5}, {0, 4, 6}};
        final long MOD = 1_000_000_007L;
        List<List<int[]>> adj = new ArrayList<>();
        for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
        for (int[] r : roads) {
            adj.get(r[0]).add(new int[] {r[1], r[2]});
            adj.get(r[1]).add(new int[] {r[0], r[2]});
        }
        long[] dist = new long[n], ways = new long[n];
        Arrays.fill(dist, Long.MAX_VALUE);
        dist[0] = 0;
        ways[0] = 1;
        PriorityQueue<long[]> heap = new PriorityQueue<>((a, b) -> a[0] != b[0] ? Long.compare(a[0], b[0])
                : Long.compare(a[1], b[1]));
        heap.add(new long[] {0, 0});
        while (!heap.isEmpty()) {
            long[] top = heap.poll();
            long d = top[0];
            int u = (int) top[1];
            if (d > dist[u]) continue;
            for (int[] e : adj.get(u)) {
                int v = e[0];
                if (d + e[1] < dist[v]) {            // a shorter way: the old ways no longer count
                    dist[v] = d + e[1];
                    ways[v] = ways[u];
                    heap.add(new long[] {dist[v], v});
                } else if (d + e[1] == dist[v]) {    // as short: u's ways are more ways to v
                    ways[v] = (ways[v] + ways[u]) % MOD;
                }
            }
        }
        System.out.println("the shortest time to 4: " + dist[n - 1] + ", reached " + ways[n - 1] + " ways");
    }
}

// the shortest time to 4: 6, reached 3 ways
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n = 5;                                       // Number of Ways to Arrive at Destination: two-way roads
    vector<array<int, 3>> roads = {{0, 1, 2}, {0, 2, 3}, {1, 3, 2}, {2, 3, 1}, {3, 4, 2}, {1, 4, 5}, {0, 4, 6}};
    const long long MOD = 1000000007LL;
    vector<vector<pair<int, int>>> adj(n);
    for (auto& r : roads) {
        adj[r[0]].push_back({r[1], r[2]});
        adj[r[1]].push_back({r[0], r[2]});
    }
    vector<long long> dist(n, LLONG_MAX), ways(n, 0);
    dist[0] = 0;
    ways[0] = 1;
    priority_queue<pair<long long, int>, vector<pair<long long, int>>, greater<>> heap;
    heap.push({0, 0});
    while (!heap.empty()) {
        auto [d, u] = heap.top();
        heap.pop();
        if (d > dist[u]) continue;
        for (auto [v, t] : adj[u]) {
            if (d + t < dist[v]) {                   // a shorter way: the old ways no longer count
                dist[v] = d + t;
                ways[v] = ways[u];
                heap.push({dist[v], v});
            } else if (d + t == dist[v]) {           // as short: u's ways are more ways to v
                ways[v] = (ways[v] + ways[u]) % MOD;
            }
        }
    }
    cout << "the shortest time to 4: " << dist[n - 1] << ", reached " << ways[n - 1] << " ways\n";
}

// the shortest time to 4: 6, reached 3 ways
class Heap {                                   // a min-heap of arrays, compared item by item
    constructor() { this.a = []; }
    get size() { return this.a.length; }
    less(i, j) {
        const x = this.a[i], y = this.a[j];
        for (let k = 0; k < x.length; k++) if (x[k] !== y[k]) return x[k] < y[k];
        return false;
    }
    push(item) {
        const a = this.a;
        a.push(item);
        for (let i = a.length - 1; i > 0;) {
            const p = (i - 1) >> 1;
            if (!this.less(i, p)) break;
            [a[i], a[p]] = [a[p], a[i]];
            i = p;
        }
    }
    pop() {
        const a = this.a, top = a[0], last = a.pop();
        if (a.length) {
            a[0] = last;
            for (let i = 0; ;) {
                const l = 2 * i + 1, r = l + 1;
                let m = i;
                if (l < a.length && this.less(l, m)) m = l;
                if (r < a.length && this.less(r, m)) m = r;
                if (m === i) break;
                [a[i], a[m]] = [a[m], a[i]];
                i = m;
            }
        }
        return top;
    }
}

const n = 5;                                   // Number of Ways to Arrive at Destination: two-way roads [u, v, time]
const roads = [[0, 1, 2], [0, 2, 3], [1, 3, 2], [2, 3, 1], [3, 4, 2], [1, 4, 5], [0, 4, 6]];
const MOD = 1000000007;
const adj = Array.from({ length: n }, () => []);
for (const [u, v, t] of roads) {
    adj[u].push([v, t]);
    adj[v].push([u, t]);
}
const dist = new Array(n).fill(Infinity), ways = new Array(n).fill(0);
dist[0] = 0;
ways[0] = 1;
const heap = new Heap();
heap.push([0, 0]);
while (heap.size) {
    const [d, u] = heap.pop();
    if (d > dist[u]) continue;
    for (const [v, t] of adj[u]) {
        if (d + t < dist[v]) {                 // a shorter way: the old ways no longer count
            dist[v] = d + t;
            ways[v] = ways[u];
            heap.push([dist[v], v]);
        } else if (d + t === dist[v]) {        // as short: u's ways are more ways to v
            ways[v] = (ways[v] + ways[u]) % MOD;
        }
    }
}
console.log(`the shortest time to 4: ${dist[n - 1]}, reached ${ways[n - 1]} ways`);

// the shortest time to 4: 6, reached 3 ways
class Heap {                                   // a min-heap of arrays, compared item by item
    private a: number[][] = [];
    get size(): number { return this.a.length; }
    private less(i: number, j: number): boolean {
        const x = this.a[i], y = this.a[j];
        for (let k = 0; k < x.length; k++) if (x[k] !== y[k]) return x[k] < y[k];
        return false;
    }
    push(item: number[]): void {
        const a = this.a;
        a.push(item);
        for (let i = a.length - 1; i > 0;) {
            const p = (i - 1) >> 1;
            if (!this.less(i, p)) break;
            [a[i], a[p]] = [a[p], a[i]];
            i = p;
        }
    }
    pop(): number[] {
        const a = this.a, top = a[0], last = a.pop()!;
        if (a.length) {
            a[0] = last;
            for (let i = 0; ;) {
                const l = 2 * i + 1, r = l + 1;
                let m = i;
                if (l < a.length && this.less(l, m)) m = l;
                if (r < a.length && this.less(r, m)) m = r;
                if (m === i) break;
                [a[i], a[m]] = [a[m], a[i]];
                i = m;
            }
        }
        return top;
    }
}

const n = 5;                                   // Number of Ways to Arrive at Destination: two-way roads [u, v, time]
const roads: [number, number, number][] = [[0, 1, 2], [0, 2, 3], [1, 3, 2], [2, 3, 1], [3, 4, 2], [1, 4, 5], [0, 4, 6]];
const MOD = 1000000007;
const adj: [number, number][][] = Array.from({ length: n }, () => []);
for (const [u, v, t] of roads) {
    adj[u].push([v, t]);
    adj[v].push([u, t]);
}
const dist: number[] = new Array(n).fill(Infinity), ways: number[] = new Array(n).fill(0);
dist[0] = 0;
ways[0] = 1;
const heap = new Heap();
heap.push([0, 0]);
while (heap.size) {
    const [d, u] = heap.pop();
    if (d > dist[u]) continue;
    for (const [v, t] of adj[u]) {
        if (d + t < dist[v]) {                 // a shorter way: the old ways no longer count
            dist[v] = d + t;
            ways[v] = ways[u];
            heap.push([dist[v], v]);
        } else if (d + t === dist[v]) {        // as short: u's ways are more ways to v
            ways[v] = (ways[v] + ways[u]) % MOD;
        }
    }
}
console.log(`the shortest time to 4: ${dist[n - 1]}, reached ${ways[n - 1]} ways`);

// the shortest time to 4: 6, reached 3 ways
using System;
using System.Collections.Generic;

class Program {
    static void Main() {
        int n = 5;                                    // Number of Ways to Arrive at Destination: two-way roads
        int[][] roads = { new[] {0, 1, 2}, new[] {0, 2, 3}, new[] {1, 3, 2}, new[] {2, 3, 1}, new[] {3, 4, 2},
                          new[] {1, 4, 5}, new[] {0, 4, 6} };
        const long MOD = 1_000_000_007L;
        var adj = new List<(int v, int t)>[n];
        for (int i = 0; i < n; i++) adj[i] = new List<(int, int)>();
        foreach (var r in roads) {
            adj[r[0]].Add((r[1], r[2]));
            adj[r[1]].Add((r[0], r[2]));
        }
        var dist = new long[n];
        var ways = new long[n];
        Array.Fill(dist, long.MaxValue);
        dist[0] = 0;
        ways[0] = 1;
        var heap = new PriorityQueue<int, (long, int)>();
        heap.Enqueue(0, (0, 0));
        while (heap.TryDequeue(out int u, out var key)) {
            long d = key.Item1;
            if (d > dist[u]) continue;
            foreach (var (v, t) in adj[u]) {
                if (d + t < dist[v]) {                // a shorter way: the old ways no longer count
                    dist[v] = d + t;
                    ways[v] = ways[u];
                    heap.Enqueue(v, (dist[v], v));
                } else if (d + t == dist[v]) {        // as short: u's ways are more ways to v
                    ways[v] = (ways[v] + ways[u]) % MOD;
                }
            }
        }
        Console.WriteLine($"the shortest time to 4: {dist[n - 1]}, reached {ways[n - 1]} ways");
    }
}

// the shortest time to 4: 6, reached 3 ways
package main

import (
	"container/heap"
	"fmt"
	"math"
)

type item struct{ d, v int }
type minHeap []item // the smallest distance on top, the smaller vertex first on a tie

func (h minHeap) Len() int            { return len(h) }
func (h minHeap) Less(i, j int) bool  { return h[i].d < h[j].d || (h[i].d == h[j].d && h[i].v < h[j].v) }
func (h minHeap) Swap(i, j int)       { h[i], h[j] = h[j], h[i] }
func (h *minHeap) Push(x interface{}) { *h = append(*h, x.(item)) }
func (h *minHeap) Pop() interface{} {
	old := *h
	x := old[len(old)-1]
	*h = old[:len(old)-1]
	return x
}

func main() {
	n := 5 // Number of Ways to Arrive at Destination: two-way roads {u, v, time}
	roads := [][3]int{{0, 1, 2}, {0, 2, 3}, {1, 3, 2}, {2, 3, 1}, {3, 4, 2}, {1, 4, 5}, {0, 4, 6}}
	const MOD = 1000000007
	adj := make([][][2]int, n)
	for _, r := range roads {
		adj[r[0]] = append(adj[r[0]], [2]int{r[1], r[2]})
		adj[r[1]] = append(adj[r[1]], [2]int{r[0], r[2]})
	}
	dist, ways := make([]int, n), make([]int, n)
	for i := range dist {
		dist[i] = math.MaxInt64
	}
	dist[0], ways[0] = 0, 1
	h := &minHeap{{0, 0}}
	for h.Len() > 0 {
		top := heap.Pop(h).(item)
		if top.d > dist[top.v] {
			continue
		}
		for _, e := range adj[top.v] {
			v, t := e[0], e[1]
			if top.d+t < dist[v] { // a shorter way: the old ways no longer count
				dist[v], ways[v] = top.d+t, ways[top.v]
				heap.Push(h, item{dist[v], v})
			} else if top.d+t == dist[v] { // as short: u's ways are more ways to v
				ways[v] = (ways[v] + ways[top.v]) % MOD
			}
		}
	}
	fmt.Printf("the shortest time to 4: %d, reached %d ways\n", dist[n-1], ways[n-1])
}

// the shortest time to 4: 6, reached 3 ways

Three routes take 6: 0 → 1 → 3 → 4, 0 → 2 → 3 → 4, and the road 0 → 4 itself. The counts are kept modulo 10⁹ + 7, as the problem asks, because on a big graph they grow past any integer.

Bellman-Ford: every edge, round after round

A shortest path with no cycle has at most n − 1 edges, and each round of relaxing every edge makes sure of one more edge of it. So n − 1 rounds get every distance right, whatever the weights, unless a cycle of negative total weight can be reached. Then going round it once more always lowers something, so one more round still finds a change:

def bellman_ford(n, edges, src):              # n - 1 rounds of every edge, then one more to look for a negative cycle
    dist = [float("inf")] * n
    dist[src] = 0
    for _ in range(n - 1):
        for u, v, w in edges:
            if dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
    cycle = any(dist[u] + w < dist[v] for u, v, w in edges)   # still lowering after n - 1 rounds
    return dist, cycle


edges = [(0, 1, 6), (0, 2, 7), (1, 2, 8), (1, 3, 5), (2, 3, -3), (3, 1, -2), (2, 4, 9), (4, 3, 7)]
dist, cycle = bellman_ford(5, edges, 0)
print("from 0:", " ".join(map(str, dist)), "| a negative cycle:", "yes" if cycle else "no")
dist, cycle = bellman_ford(5, edges + [(3, 2, -1)], 0)
print("with 3 -> 2 of -1 added, a negative cycle:", "yes" if cycle else "no")

# from 0: 0 2 7 4 16 | a negative cycle: no
# with 3 -> 2 of -1 added, a negative cycle: yes
import java.util.*;

public class Main {
    static long[] dist;

    static boolean bellmanFord(int n, int[][] edges, int src) {   // n - 1 rounds of every edge, then one more
        dist = new long[n];
        Arrays.fill(dist, Long.MAX_VALUE);
        dist[src] = 0;
        for (int round = 0; round < n - 1; round++)
            for (int[] e : edges)
                if (dist[e[0]] != Long.MAX_VALUE && dist[e[0]] + e[2] < dist[e[1]]) dist[e[1]] = dist[e[0]] + e[2];
        for (int[] e : edges)                        // still lowering after n - 1 rounds: a negative cycle
            if (dist[e[0]] != Long.MAX_VALUE && dist[e[0]] + e[2] < dist[e[1]]) return true;
        return false;
    }

    public static void main(String[] args) {
        int[][] edges = {{0, 1, 6}, {0, 2, 7}, {1, 2, 8}, {1, 3, 5}, {2, 3, -3}, {3, 1, -2}, {2, 4, 9}, {4, 3, 7}};
        boolean cycle = bellmanFord(5, edges, 0);
        StringJoiner s = new StringJoiner(" ");
        for (long d : dist) s.add(String.valueOf(d));
        System.out.println("from 0: " + s + " | a negative cycle: " + (cycle ? "yes" : "no"));
        int[][] more = Arrays.copyOf(edges, edges.length + 1);
        more[edges.length] = new int[] {3, 2, -1};
        System.out.println("with 3 -> 2 of -1 added, a negative cycle: " + (bellmanFord(5, more, 0) ? "yes" : "no"));
    }
}

// from 0: 0 2 7 4 16 | a negative cycle: no
// with 3 -> 2 of -1 added, a negative cycle: yes
#include <bits/stdc++.h>
using namespace std;

vector<long long> dist;

bool bellmanFord(int n, const vector<array<int, 3>>& edges, int src) {   // n - 1 rounds of every edge, then one more
    dist.assign(n, LLONG_MAX);
    dist[src] = 0;
    for (int round = 0; round < n - 1; round++)
        for (auto& e : edges)
            if (dist[e[0]] != LLONG_MAX && dist[e[0]] + e[2] < dist[e[1]]) dist[e[1]] = dist[e[0]] + e[2];
    for (auto& e : edges)                            // still lowering after n - 1 rounds: a negative cycle
        if (dist[e[0]] != LLONG_MAX && dist[e[0]] + e[2] < dist[e[1]]) return true;
    return false;
}

int main() {
    vector<array<int, 3>> edges = {{0, 1, 6}, {0, 2, 7}, {1, 2, 8}, {1, 3, 5}, {2, 3, -3}, {3, 1, -2}, {2, 4, 9}, {4, 3, 7}};
    bool cycle = bellmanFord(5, edges, 0);
    cout << "from 0:";
    for (long long d : dist) cout << " " << d;
    cout << " | a negative cycle: " << (cycle ? "yes" : "no") << "\n";
    auto more = edges;
    more.push_back({3, 2, -1});
    cout << "with 3 -> 2 of -1 added, a negative cycle: " << (bellmanFord(5, more, 0) ? "yes" : "no") << "\n";
}

// from 0: 0 2 7 4 16 | a negative cycle: no
// with 3 -> 2 of -1 added, a negative cycle: yes
function bellmanFord(n, edges, src) {          // n - 1 rounds of every edge, then one more to look for a negative cycle
    const dist = new Array(n).fill(Infinity);
    dist[src] = 0;
    for (let round = 0; round < n - 1; round++)
        for (const [u, v, w] of edges) if (dist[u] + w < dist[v]) dist[v] = dist[u] + w;
    const cycle = edges.some(([u, v, w]) => dist[u] + w < dist[v]);   // still lowering after n - 1 rounds
    return { dist, cycle };
}

const edges = [[0, 1, 6], [0, 2, 7], [1, 2, 8], [1, 3, 5], [2, 3, -3], [3, 1, -2], [2, 4, 9], [4, 3, 7]];
const first = bellmanFord(5, edges, 0);
console.log(`from 0: ${first.dist.join(" ")} | a negative cycle: ${first.cycle ? "yes" : "no"}`);
const second = bellmanFord(5, [...edges, [3, 2, -1]], 0);
console.log(`with 3 -> 2 of -1 added, a negative cycle: ${second.cycle ? "yes" : "no"}`);

// from 0: 0 2 7 4 16 | a negative cycle: no
// with 3 -> 2 of -1 added, a negative cycle: yes
function bellmanFord(n: number, edges: [number, number, number][], src: number): { dist: number[]; cycle: boolean } {
    const dist: number[] = new Array(n).fill(Infinity);   // n - 1 rounds of every edge, then one more
    dist[src] = 0;
    for (let round = 0; round < n - 1; round++)
        for (const [u, v, w] of edges) if (dist[u] + w < dist[v]) dist[v] = dist[u] + w;
    const cycle = edges.some(([u, v, w]) => dist[u] + w < dist[v]);   // still lowering after n - 1 rounds
    return { dist, cycle };
}

const edges: [number, number, number][] = [[0, 1, 6], [0, 2, 7], [1, 2, 8], [1, 3, 5], [2, 3, -3], [3, 1, -2],
                                           [2, 4, 9], [4, 3, 7]];
const first = bellmanFord(5, edges, 0);
console.log(`from 0: ${first.dist.join(" ")} | a negative cycle: ${first.cycle ? "yes" : "no"}`);
const second = bellmanFord(5, [...edges, [3, 2, -1]], 0);
console.log(`with 3 -> 2 of -1 added, a negative cycle: ${second.cycle ? "yes" : "no"}`);

// from 0: 0 2 7 4 16 | a negative cycle: no
// with 3 -> 2 of -1 added, a negative cycle: yes
using System;
using System.Linq;

class Program {
    static long[] dist;

    static bool BellmanFord(int n, int[][] edges, int src) {   // n - 1 rounds of every edge, then one more
        dist = new long[n];
        Array.Fill(dist, long.MaxValue);
        dist[src] = 0;
        for (int round = 0; round < n - 1; round++)
            foreach (var e in edges)
                if (dist[e[0]] != long.MaxValue && dist[e[0]] + e[2] < dist[e[1]]) dist[e[1]] = dist[e[0]] + e[2];
        return edges.Any(e => dist[e[0]] != long.MaxValue && dist[e[0]] + e[2] < dist[e[1]]);   // still lowering
    }

    static void Main() {
        int[][] edges = { new[] {0, 1, 6}, new[] {0, 2, 7}, new[] {1, 2, 8}, new[] {1, 3, 5}, new[] {2, 3, -3},
                          new[] {3, 1, -2}, new[] {2, 4, 9}, new[] {4, 3, 7} };
        bool cycle = BellmanFord(5, edges, 0);
        Console.WriteLine($"from 0: {string.Join(" ", dist)} | a negative cycle: {(cycle ? "yes" : "no")}");
        var more = edges.Append(new[] {3, 2, -1}).ToArray();
        Console.WriteLine($"with 3 -> 2 of -1 added, a negative cycle: {(BellmanFord(5, more, 0) ? "yes" : "no")}");
    }
}

// from 0: 0 2 7 4 16 | a negative cycle: no
// with 3 -> 2 of -1 added, a negative cycle: yes
package main

import (
	"fmt"
	"math"
	"strconv"
	"strings"
)

func bellmanFord(n int, edges [][3]int, src int) ([]int, bool) { // n - 1 rounds of every edge, then one more
	dist := make([]int, n)
	for i := range dist {
		dist[i] = math.MaxInt64
	}
	dist[src] = 0
	for round := 0; round < n-1; round++ {
		for _, e := range edges {
			if dist[e[0]] != math.MaxInt64 && dist[e[0]]+e[2] < dist[e[1]] {
				dist[e[1]] = dist[e[0]] + e[2]
			}
		}
	}
	for _, e := range edges { // still lowering after n - 1 rounds: a negative cycle
		if dist[e[0]] != math.MaxInt64 && dist[e[0]]+e[2] < dist[e[1]] {
			return dist, true
		}
	}
	return dist, false
}

func word(yes bool) string {
	if yes {
		return "yes"
	}
	return "no"
}

func main() {
	edges := [][3]int{{0, 1, 6}, {0, 2, 7}, {1, 2, 8}, {1, 3, 5}, {2, 3, -3}, {3, 1, -2}, {2, 4, 9}, {4, 3, 7}}
	dist, cycle := bellmanFord(5, edges, 0)
	parts := []string{}
	for _, d := range dist {
		parts = append(parts, strconv.Itoa(d))
	}
	fmt.Printf("from 0: %s | a negative cycle: %s\n", strings.Join(parts, " "), word(cycle))
	more := append(append([][3]int{}, edges...), [3]int{3, 2, -1})
	_, cycle = bellmanFord(5, more, 0)
	fmt.Println("with 3 -> 2 of -1 added, a negative cycle:", word(cycle))
}

// from 0: 0 2 7 4 16 | a negative cycle: no
// with 3 -> 2 of -1 added, a negative cycle: yes

With the negative edges 2 → 3 and 3 → 1, the distances settle at 0 2 7 4 16, and a further round changes nothing. Add 3 → 2 of −1, and 2 → 3 → 2 sums to −4: a further round still lowers a distance, so there's no shortest path at all. Bellman-Ford Algorithm is this, in O(V × E).

At most k stops

Cheapest Flights Within K Stops allows at most k stops, so at most k + 1 flights. Bellman-Ford's rounds count flights: after round r, every trip of at most r flights has its price. Run k + 1 rounds, each reading the costs of the round before from a copy, so one round can't string several flights together:

def cheapest(n, flights, src, dst, k):        # Cheapest Flights Within K Stops: at most k stops is k + 1 flights
    cost = [float("inf")] * n
    cost[src] = 0
    for _ in range(k + 1):                     # round r finds the cheapest trips of at most r flights
        before = cost[:]                       # read last round's costs, so a round adds one flight, not several
        for u, v, price in flights:
            if before[u] + price < cost[v]:
                cost[v] = before[u] + price
    return -1 if cost[dst] == float("inf") else cost[dst]


flights = [[0, 1, 50], [1, 2, 50], [2, 3, 50], [0, 3, 400], [1, 3, 300], [0, 2, 200]]
print(" | ".join(f"k = {k}: {cheapest(4, flights, 0, 3, k)}" for k in range(3)))

# k = 0: 400 | k = 1: 250 | k = 2: 150
import java.util.*;

public class Main {
    static long cheapest(int n, int[][] flights, int src, int dst, int k) {   // at most k stops is k + 1 flights
        long[] cost = new long[n];
        Arrays.fill(cost, Long.MAX_VALUE);
        cost[src] = 0;
        for (int round = 0; round <= k; round++) {   // round r finds the cheapest trips of at most r flights
            long[] before = cost.clone();            // read last round's costs: a round adds one flight, not several
            for (int[] f : flights)
                if (before[f[0]] != Long.MAX_VALUE && before[f[0]] + f[2] < cost[f[1]]) cost[f[1]] = before[f[0]] + f[2];
        }
        return cost[dst] == Long.MAX_VALUE ? -1 : cost[dst];
    }

    public static void main(String[] args) {
        int[][] flights = {{0, 1, 50}, {1, 2, 50}, {2, 3, 50}, {0, 3, 400}, {1, 3, 300}, {0, 2, 200}};
        StringJoiner s = new StringJoiner(" | ");
        for (int k = 0; k <= 2; k++) s.add("k = " + k + ": " + cheapest(4, flights, 0, 3, k));
        System.out.println(s);
    }
}

// k = 0: 400 | k = 1: 250 | k = 2: 150
#include <bits/stdc++.h>
using namespace std;

long long cheapest(int n, const vector<array<int, 3>>& flights, int src, int dst, int k) {   // k stops: k + 1 flights
    vector<long long> cost(n, LLONG_MAX);
    cost[src] = 0;
    for (int round = 0; round <= k; round++) {       // round r finds the cheapest trips of at most r flights
        auto before = cost;                          // read last round's costs: a round adds one flight, not several
        for (auto& f : flights)
            if (before[f[0]] != LLONG_MAX && before[f[0]] + f[2] < cost[f[1]]) cost[f[1]] = before[f[0]] + f[2];
    }
    return cost[dst] == LLONG_MAX ? -1 : cost[dst];
}

int main() {
    vector<array<int, 3>> flights = {{0, 1, 50}, {1, 2, 50}, {2, 3, 50}, {0, 3, 400}, {1, 3, 300}, {0, 2, 200}};
    for (int k = 0; k <= 2; k++) cout << (k ? " | " : "") << "k = " << k << ": " << cheapest(4, flights, 0, 3, k);
    cout << "\n";
}

// k = 0: 400 | k = 1: 250 | k = 2: 150
function cheapest(n, flights, src, dst, k) {   // Cheapest Flights Within K Stops: k stops is k + 1 flights
    const cost = new Array(n).fill(Infinity);
    cost[src] = 0;
    for (let round = 0; round <= k; round++) { // round r finds the cheapest trips of at most r flights
        const before = [...cost];              // read last round's costs, so a round adds one flight, not several
        for (const [u, v, price] of flights) if (before[u] + price < cost[v]) cost[v] = before[u] + price;
    }
    return cost[dst] === Infinity ? -1 : cost[dst];
}

const flights = [[0, 1, 50], [1, 2, 50], [2, 3, 50], [0, 3, 400], [1, 3, 300], [0, 2, 200]];
console.log([0, 1, 2].map((k) => `k = ${k}: ${cheapest(4, flights, 0, 3, k)}`).join(" | "));

// k = 0: 400 | k = 1: 250 | k = 2: 150
function cheapest(n: number, flights: [number, number, number][], src: number, dst: number, k: number): number {
    const cost: number[] = new Array(n).fill(Infinity);   // Cheapest Flights Within K Stops: k + 1 flights at most
    cost[src] = 0;
    for (let round = 0; round <= k; round++) { // round r finds the cheapest trips of at most r flights
        const before = [...cost];              // read last round's costs, so a round adds one flight, not several
        for (const [u, v, price] of flights) if (before[u] + price < cost[v]) cost[v] = before[u] + price;
    }
    return cost[dst] === Infinity ? -1 : cost[dst];
}

const flights: [number, number, number][] = [[0, 1, 50], [1, 2, 50], [2, 3, 50], [0, 3, 400], [1, 3, 300], [0, 2, 200]];
console.log([0, 1, 2].map((k) => `k = ${k}: ${cheapest(4, flights, 0, 3, k)}`).join(" | "));

// k = 0: 400 | k = 1: 250 | k = 2: 150
using System;
using System.Linq;

class Program {
    static long Cheapest(int n, int[][] flights, int src, int dst, int k) {   // at most k stops is k + 1 flights
        var cost = new long[n];
        Array.Fill(cost, long.MaxValue);
        cost[src] = 0;
        for (int round = 0; round <= k; round++) {    // round r finds the cheapest trips of at most r flights
            var before = (long[]) cost.Clone();       // read last round's costs: a round adds one flight, not several
            foreach (var f in flights)
                if (before[f[0]] != long.MaxValue && before[f[0]] + f[2] < cost[f[1]]) cost[f[1]] = before[f[0]] + f[2];
        }
        return cost[dst] == long.MaxValue ? -1 : cost[dst];
    }

    static void Main() {
        int[][] flights = { new[] {0, 1, 50}, new[] {1, 2, 50}, new[] {2, 3, 50}, new[] {0, 3, 400}, new[] {1, 3, 300},
                            new[] {0, 2, 200} };
        Console.WriteLine(string.Join(" | ", Enumerable.Range(0, 3).Select(k => $"k = {k}: {Cheapest(4, flights, 0, 3, k)}")));
    }
}

// k = 0: 400 | k = 1: 250 | k = 2: 150
package main

import (
	"fmt"
	"math"
	"strings"
)

func cheapest(n int, flights [][3]int, src, dst, k int) int { // at most k stops is k + 1 flights
	cost := make([]int, n)
	for i := range cost {
		cost[i] = math.MaxInt64
	}
	cost[src] = 0
	for round := 0; round <= k; round++ { // round r finds the cheapest trips of at most r flights
		before := append([]int{}, cost...) // read last round's costs: a round adds one flight, not several
		for _, f := range flights {
			if before[f[0]] != math.MaxInt64 && before[f[0]]+f[2] < cost[f[1]] {
				cost[f[1]] = before[f[0]] + f[2]
			}
		}
	}
	if cost[dst] == math.MaxInt64 {
		return -1
	}
	return cost[dst]
}

func main() {
	flights := [][3]int{{0, 1, 50}, {1, 2, 50}, {2, 3, 50}, {0, 3, 400}, {1, 3, 300}, {0, 2, 200}}
	parts := []string{}
	for k := 0; k <= 2; k++ {
		parts = append(parts, fmt.Sprintf("k = %d: %d", k, cheapest(4, flights, 0, 3, k)))
	}
	fmt.Println(strings.Join(parts, " | "))
}

// k = 0: 400 | k = 1: 250 | k = 2: 150

With no stop there's only the direct flight, 400. One stop finds 0 → 2 → 3 for 250, and two stops 0 → 1 → 2 → 3 for 150. Dijkstra on price alone would answer 150 every time: the cheapest route with any number of stops.

A DAG: one pass in order

When the graph is a DAG, there's an order with every edge pointing forward, the topological order of Cycles, Bipartite Graphs and Topological Order. Relax each vertex's edges in that order, once. When a vertex's turn comes, everything leading into it has been relaxed already, so its distance is final, negative edges and all, in O(V + E):

from collections import deque


def dag_shortest(n, edges, src):               # Shortest Path in DAG: relax each vertex's edges once, in order
    adj, indeg = [[] for _ in range(n)], [0] * n
    for u, v, w in edges:
        adj[u].append((v, w))
        indeg[v] += 1
    order, queue = [], deque(u for u in range(n) if indeg[u] == 0)
    while queue:                               # Kahn's algorithm, from Cycles, Bipartite Graphs and Topological Order
        u = queue.popleft()
        order.append(u)
        for v, _ in adj[u]:
            indeg[v] -= 1
            if indeg[v] == 0:
                queue.append(v)
    dist = [float("inf")] * n
    dist[src] = 0
    for u in order:                            # everything before u is done, so dist[u] is final now
        if dist[u] != float("inf"):
            for v, w in adj[u]:
                dist[v] = min(dist[v], dist[u] + w)
    return order, dist


edges = [(0, 1, 5), (0, 2, 3), (1, 3, 6), (1, 2, 2), (2, 4, 4), (2, 5, 2), (2, 3, 7), (3, 4, -1), (4, 5, -3)]
order, dist = dag_shortest(6, edges, 0)
print("topological order:", " ".join(map(str, order)), "| from 0:", " ".join(map(str, dist)))

# topological order: 0 1 2 3 4 5 | from 0: 0 5 3 10 7 4
import java.util.*;

public class Main {
    public static void main(String[] args) {
        int n = 6;                                   // Shortest Path in DAG: relax each vertex's edges once, in order
        int[][] edges = {{0, 1, 5}, {0, 2, 3}, {1, 3, 6}, {1, 2, 2}, {2, 4, 4}, {2, 5, 2}, {2, 3, 7}, {3, 4, -1}, {4, 5, -3}};
        List<List<int[]>> adj = new ArrayList<>();
        for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
        int[] indeg = new int[n];
        for (int[] e : edges) {
            adj.get(e[0]).add(new int[] {e[1], e[2]});
            indeg[e[1]]++;
        }
        List<Integer> order = new ArrayList<>();
        ArrayDeque<Integer> queue = new ArrayDeque<>();
        for (int u = 0; u < n; u++) if (indeg[u] == 0) queue.add(u);
        while (!queue.isEmpty()) {                   // Kahn's algorithm, from Cycles, Bipartite Graphs and Topological Order
            int u = queue.poll();
            order.add(u);
            for (int[] e : adj.get(u)) if (--indeg[e[0]] == 0) queue.add(e[0]);
        }
        long[] dist = new long[n];
        Arrays.fill(dist, Long.MAX_VALUE);
        dist[0] = 0;
        for (int u : order) {                        // everything before u is done, so dist[u] is final now
            if (dist[u] == Long.MAX_VALUE) continue;
            for (int[] e : adj.get(u)) dist[e[0]] = Math.min(dist[e[0]], dist[u] + e[1]);
        }
        StringJoiner a = new StringJoiner(" "), b = new StringJoiner(" ");
        for (int u : order) a.add(String.valueOf(u));
        for (long d : dist) b.add(String.valueOf(d));
        System.out.println("topological order: " + a + " | from 0: " + b);
    }
}

// topological order: 0 1 2 3 4 5 | from 0: 0 5 3 10 7 4
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n = 6;                                       // Shortest Path in DAG: relax each vertex's edges once, in order
    vector<array<int, 3>> edges = {{0, 1, 5}, {0, 2, 3}, {1, 3, 6}, {1, 2, 2}, {2, 4, 4}, {2, 5, 2}, {2, 3, 7}, {3, 4, -1},
                                   {4, 5, -3}};
    vector<vector<pair<int, int>>> adj(n);
    vector<int> indeg(n, 0), order;
    for (auto& e : edges) {
        adj[e[0]].push_back({e[1], e[2]});
        indeg[e[1]]++;
    }
    queue<int> q;
    for (int u = 0; u < n; u++) if (indeg[u] == 0) q.push(u);
    while (!q.empty()) {                             // Kahn's algorithm, from Cycles, Bipartite Graphs and Topological Order
        int u = q.front();
        q.pop();
        order.push_back(u);
        for (auto [v, w] : adj[u]) if (--indeg[v] == 0) q.push(v);
    }
    vector<long long> dist(n, LLONG_MAX);
    dist[0] = 0;
    for (int u : order) {                            // everything before u is done, so dist[u] is final now
        if (dist[u] == LLONG_MAX) continue;
        for (auto [v, w] : adj[u]) dist[v] = min(dist[v], dist[u] + w);
    }
    cout << "topological order:";
    for (int u : order) cout << " " << u;
    cout << " | from 0:";
    for (long long d : dist) cout << " " << d;
    cout << "\n";
}

// topological order: 0 1 2 3 4 5 | from 0: 0 5 3 10 7 4
function dagShortest(n, edges, src) {         // Shortest Path in DAG: relax each vertex's edges once, in order
    const adj = Array.from({ length: n }, () => []), indeg = new Array(n).fill(0);
    for (const [u, v, w] of edges) {
        adj[u].push([v, w]);
        indeg[v]++;
    }
    const order = [];
    for (let u = 0; u < n; u++) if (indeg[u] === 0) order.push(u);
    for (let head = 0; head < order.length; head++)   // Kahn's algorithm: the order is its own queue
        for (const [v] of adj[order[head]]) if (--indeg[v] === 0) order.push(v);
    const dist = new Array(n).fill(Infinity);
    dist[src] = 0;
    for (const u of order) {                   // everything before u is done, so dist[u] is final now
        if (dist[u] === Infinity) continue;
        for (const [v, w] of adj[u]) dist[v] = Math.min(dist[v], dist[u] + w);
    }
    return { order, dist };
}

const edges = [[0, 1, 5], [0, 2, 3], [1, 3, 6], [1, 2, 2], [2, 4, 4], [2, 5, 2], [2, 3, 7], [3, 4, -1], [4, 5, -3]];
const { order, dist } = dagShortest(6, edges, 0);
console.log(`topological order: ${order.join(" ")} | from 0: ${dist.join(" ")}`);

// topological order: 0 1 2 3 4 5 | from 0: 0 5 3 10 7 4
function dagShortest(n: number, edges: [number, number, number][], src: number): { order: number[]; dist: number[] } {
    const adj: [number, number][][] = Array.from({ length: n }, () => []);   // relax each vertex's edges once, in order
    const indeg = new Array<number>(n).fill(0);
    for (const [u, v, w] of edges) {
        adj[u].push([v, w]);
        indeg[v]++;
    }
    const order: number[] = [];
    for (let u = 0; u < n; u++) if (indeg[u] === 0) order.push(u);
    for (let head = 0; head < order.length; head++)   // Kahn's algorithm: the order is its own queue
        for (const [v] of adj[order[head]]) if (--indeg[v] === 0) order.push(v);
    const dist: number[] = new Array(n).fill(Infinity);
    dist[src] = 0;
    for (const u of order) {                   // everything before u is done, so dist[u] is final now
        if (dist[u] === Infinity) continue;
        for (const [v, w] of adj[u]) dist[v] = Math.min(dist[v], dist[u] + w);
    }
    return { order, dist };
}

const edges: [number, number, number][] = [[0, 1, 5], [0, 2, 3], [1, 3, 6], [1, 2, 2], [2, 4, 4], [2, 5, 2], [2, 3, 7],
                                           [3, 4, -1], [4, 5, -3]];
const { order, dist } = dagShortest(6, edges, 0);
console.log(`topological order: ${order.join(" ")} | from 0: ${dist.join(" ")}`);

// topological order: 0 1 2 3 4 5 | from 0: 0 5 3 10 7 4
using System;
using System.Collections.Generic;

class Program {
    static void Main() {
        int n = 6;                                    // Shortest Path in DAG: relax each vertex's edges once, in order
        int[][] edges = { new[] {0, 1, 5}, new[] {0, 2, 3}, new[] {1, 3, 6}, new[] {1, 2, 2}, new[] {2, 4, 4},
                          new[] {2, 5, 2}, new[] {2, 3, 7}, new[] {3, 4, -1}, new[] {4, 5, -3} };
        var adj = new List<(int v, int w)>[n];
        for (int i = 0; i < n; i++) adj[i] = new List<(int, int)>();
        var indeg = new int[n];
        foreach (var e in edges) {
            adj[e[0]].Add((e[1], e[2]));
            indeg[e[1]]++;
        }
        var order = new List<int>();
        var queue = new Queue<int>();
        for (int u = 0; u < n; u++) if (indeg[u] == 0) queue.Enqueue(u);
        while (queue.Count > 0) {                     // Kahn's algorithm, from Cycles, Bipartite Graphs and Topological Order
            int u = queue.Dequeue();
            order.Add(u);
            foreach (var (v, _) in adj[u]) if (--indeg[v] == 0) queue.Enqueue(v);
        }
        var dist = new long[n];
        Array.Fill(dist, long.MaxValue);
        dist[0] = 0;
        foreach (int u in order) {                    // everything before u is done, so dist[u] is final now
            if (dist[u] == long.MaxValue) continue;
            foreach (var (v, w) in adj[u]) dist[v] = Math.Min(dist[v], dist[u] + w);
        }
        Console.WriteLine($"topological order: {string.Join(" ", order)} | from 0: {string.Join(" ", dist)}");
    }
}

// topological order: 0 1 2 3 4 5 | from 0: 0 5 3 10 7 4
package main

import (
	"fmt"
	"math"
	"strconv"
	"strings"
)

func joined(xs []int) string {
	parts := make([]string, len(xs))
	for i, x := range xs {
		parts[i] = strconv.Itoa(x)
	}
	return strings.Join(parts, " ")
}

func main() {
	n := 6 // Shortest Path in DAG: relax each vertex's edges once, in order
	edges := [][3]int{{0, 1, 5}, {0, 2, 3}, {1, 3, 6}, {1, 2, 2}, {2, 4, 4}, {2, 5, 2}, {2, 3, 7}, {3, 4, -1}, {4, 5, -3}}
	adj := make([][][2]int, n)
	indeg := make([]int, n)
	for _, e := range edges {
		adj[e[0]] = append(adj[e[0]], [2]int{e[1], e[2]})
		indeg[e[1]]++
	}
	order := []int{}
	for u := 0; u < n; u++ {
		if indeg[u] == 0 {
			order = append(order, u)
		}
	}
	for head := 0; head < len(order); head++ { // Kahn's algorithm: the order is its own queue
		for _, e := range adj[order[head]] {
			indeg[e[0]]--
			if indeg[e[0]] == 0 {
				order = append(order, e[0])
			}
		}
	}
	dist := make([]int, n)
	for i := range dist {
		dist[i] = math.MaxInt64
	}
	dist[0] = 0
	for _, u := range order { // everything before u is done, so dist[u] is final now
		if dist[u] == math.MaxInt64 {
			continue
		}
		for _, e := range adj[u] {
			if dist[u]+e[1] < dist[e[0]] {
				dist[e[0]] = dist[u] + e[1]
			}
		}
	}
	fmt.Printf("topological order: %s | from 0: %s\n", joined(order), joined(dist))
}

// topological order: 0 1 2 3 4 5 | from 0: 0 5 3 10 7 4

5 is reached for 4: 0 → 2 → 4 costs 7, and the edge 4 → 5 of −3 brings it to 4, cheaper than the edge 2 → 5, which gives 5. Shortest Path in DAG is this.

Floyd-Warshall: every pair

For the distance between every pair, Floyd-Warshall grows the set of vertices a path may pass through, one at a time: after round k, d[i][j] is the shortest way from i to j through any of 0 to k. Passing through k as well means d[i][k] + d[k][j], two distances already known for the smaller set. Three loops, k outermost, O(n³):

def floyd_warshall(n, edges):                  # every pair at once; a two-way road [u, v, w]
    INF = float("inf")
    d = [[0 if i == j else INF for j in range(n)] for i in range(n)]
    for u, v, w in edges:
        d[u][v] = d[v][u] = min(d[u][v], w)
    for k in range(n):                         # after round k, d[i][j] may pass through any of 0..k on the way
        for i in range(n):
            for j in range(n):
                if d[i][k] + d[k][j] < d[i][j]:
                    d[i][j] = d[i][k] + d[k][j]
    return d


n, threshold = 5, 6
d = floyd_warshall(n, [(0, 1, 3), (1, 2, 4), (0, 3, 9), (2, 3, 1), (3, 4, 2), (1, 4, 8)])
for row in d:
    print(" ".join(map(str, row)))
near = [sum(1 for j in range(n) if j != i and d[i][j] <= threshold) for i in range(n)]
city = 0
for i in range(n):                             # the fewest within reach; on a tie, the larger number
    if near[i] <= near[city]:
        city = i
print(f"within {threshold}:", " ".join(map(str, near)), "| the city:", city)

# 0 3 7 8 10
# 3 0 4 5 7
# 7 4 0 1 3
# 8 5 1 0 2
# 10 7 3 2 0
# within 6: 1 3 3 3 2 | the city: 0
import java.util.*;

public class Main {
    public static void main(String[] args) {
        int n = 5, threshold = 6;                    // every pair at once; two-way roads {u, v, w}
        int[][] edges = {{0, 1, 3}, {1, 2, 4}, {0, 3, 9}, {2, 3, 1}, {3, 4, 2}, {1, 4, 8}};
        final long INF = Long.MAX_VALUE / 4;
        long[][] d = new long[n][n];
        for (int i = 0; i < n; i++) {
            Arrays.fill(d[i], INF);
            d[i][i] = 0;
        }
        for (int[] e : edges) d[e[0]][e[1]] = d[e[1]][e[0]] = Math.min(d[e[0]][e[1]], e[2]);
        for (int k = 0; k < n; k++)                  // after round k, d[i][j] may pass through any of 0..k
            for (int i = 0; i < n; i++)
                for (int j = 0; j < n; j++)
                    if (d[i][k] + d[k][j] < d[i][j]) d[i][j] = d[i][k] + d[k][j];
        for (long[] row : d) {
            StringJoiner s = new StringJoiner(" ");
            for (long x : row) s.add(String.valueOf(x));
            System.out.println(s);
        }
        int[] near = new int[n];
        int city = 0;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) if (j != i && d[i][j] <= threshold) near[i]++;
        }
        for (int i = 0; i < n; i++) if (near[i] <= near[city]) city = i;   // the fewest; on a tie, the larger number
        StringJoiner s = new StringJoiner(" ");
        for (int x : near) s.add(String.valueOf(x));
        System.out.println("within " + threshold + ": " + s + " | the city: " + city);
    }
}

// 0 3 7 8 10
// 3 0 4 5 7
// 7 4 0 1 3
// 8 5 1 0 2
// 10 7 3 2 0
// within 6: 1 3 3 3 2 | the city: 0
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n = 5, threshold = 6;                        // every pair at once; two-way roads {u, v, w}
    vector<array<int, 3>> edges = {{0, 1, 3}, {1, 2, 4}, {0, 3, 9}, {2, 3, 1}, {3, 4, 2}, {1, 4, 8}};
    const long long INF = LLONG_MAX / 4;
    vector<vector<long long>> d(n, vector<long long>(n, INF));
    for (int i = 0; i < n; i++) d[i][i] = 0;
    for (auto& e : edges) d[e[0]][e[1]] = d[e[1]][e[0]] = min<long long>(d[e[0]][e[1]], e[2]);
    for (int k = 0; k < n; k++)                      // after round k, d[i][j] may pass through any of 0..k
        for (int i = 0; i < n; i++)
            for (int j = 0; j < n; j++)
                if (d[i][k] + d[k][j] < d[i][j]) d[i][j] = d[i][k] + d[k][j];
    for (auto& row : d) {
        for (int j = 0; j < n; j++) cout << (j ? " " : "") << row[j];
        cout << "\n";
    }
    vector<int> near(n, 0);
    int city = 0;
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++) if (j != i && d[i][j] <= threshold) near[i]++;
    for (int i = 0; i < n; i++) if (near[i] <= near[city]) city = i;   // the fewest; on a tie, the larger number
    cout << "within " << threshold << ":";
    for (int x : near) cout << " " << x;
    cout << " | the city: " << city << "\n";
}

// 0 3 7 8 10
// 3 0 4 5 7
// 7 4 0 1 3
// 8 5 1 0 2
// 10 7 3 2 0
// within 6: 1 3 3 3 2 | the city: 0
function floydWarshall(n, edges) {             // every pair at once; a two-way road [u, v, w]
    const d = Array.from({ length: n }, (_, i) => Array.from({ length: n }, (_, j) => (i === j ? 0 : Infinity)));
    for (const [u, v, w] of edges) d[u][v] = d[v][u] = Math.min(d[u][v], w);
    for (let k = 0; k < n; k++)                // after round k, d[i][j] may pass through any of 0..k on the way
        for (let i = 0; i < n; i++)
            for (let j = 0; j < n; j++)
                if (d[i][k] + d[k][j] < d[i][j]) d[i][j] = d[i][k] + d[k][j];
    return d;
}

const n = 5, threshold = 6;
const d = floydWarshall(n, [[0, 1, 3], [1, 2, 4], [0, 3, 9], [2, 3, 1], [3, 4, 2], [1, 4, 8]]);
for (const row of d) console.log(row.join(" "));
const near = d.map((row, i) => row.filter((x, j) => j !== i && x <= threshold).length);
let city = 0;
for (let i = 0; i < n; i++) if (near[i] <= near[city]) city = i;   // the fewest; on a tie, the larger number
console.log(`within ${threshold}: ${near.join(" ")} | the city: ${city}`);

// 0 3 7 8 10
// 3 0 4 5 7
// 7 4 0 1 3
// 8 5 1 0 2
// 10 7 3 2 0
// within 6: 1 3 3 3 2 | the city: 0
function floydWarshall(n: number, edges: [number, number, number][]): number[][] {   // every pair at once
    const d: number[][] = Array.from({ length: n }, (_, i) => Array.from({ length: n }, (_, j) => (i === j ? 0 : Infinity)));
    for (const [u, v, w] of edges) d[u][v] = d[v][u] = Math.min(d[u][v], w);   // a two-way road [u, v, w]
    for (let k = 0; k < n; k++)                // after round k, d[i][j] may pass through any of 0..k on the way
        for (let i = 0; i < n; i++)
            for (let j = 0; j < n; j++)
                if (d[i][k] + d[k][j] < d[i][j]) d[i][j] = d[i][k] + d[k][j];
    return d;
}

const n = 5, threshold = 6;
const d = floydWarshall(n, [[0, 1, 3], [1, 2, 4], [0, 3, 9], [2, 3, 1], [3, 4, 2], [1, 4, 8]]);
for (const row of d) console.log(row.join(" "));
const near = d.map((row, i) => row.filter((x, j) => j !== i && x <= threshold).length);
let city = 0;
for (let i = 0; i < n; i++) if (near[i] <= near[city]) city = i;   // the fewest; on a tie, the larger number
console.log(`within ${threshold}: ${near.join(" ")} | the city: ${city}`);

// 0 3 7 8 10
// 3 0 4 5 7
// 7 4 0 1 3
// 8 5 1 0 2
// 10 7 3 2 0
// within 6: 1 3 3 3 2 | the city: 0
using System;
using System.Collections.Generic;

class Program {
    static void Main() {
        int n = 5, threshold = 6;                     // every pair at once; two-way roads {u, v, w}
        int[][] edges = { new[] {0, 1, 3}, new[] {1, 2, 4}, new[] {0, 3, 9}, new[] {2, 3, 1}, new[] {3, 4, 2},
                          new[] {1, 4, 8} };
        const long INF = long.MaxValue / 4;
        var d = new long[n, n];
        for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) d[i, j] = i == j ? 0 : INF;
        foreach (var e in edges) d[e[0], e[1]] = d[e[1], e[0]] = Math.Min(d[e[0], e[1]], e[2]);
        for (int k = 0; k < n; k++)                   // after round k, d[i, j] may pass through any of 0..k
            for (int i = 0; i < n; i++)
                for (int j = 0; j < n; j++)
                    if (d[i, k] + d[k, j] < d[i, j]) d[i, j] = d[i, k] + d[k, j];
        var near = new int[n];
        for (int i = 0; i < n; i++) {
            var row = new List<long>();
            for (int j = 0; j < n; j++) {
                row.Add(d[i, j]);
                if (j != i && d[i, j] <= threshold) near[i]++;
            }
            Console.WriteLine(string.Join(" ", row));
        }
        int city = 0;
        for (int i = 0; i < n; i++) if (near[i] <= near[city]) city = i;   // the fewest; on a tie, the larger number
        Console.WriteLine($"within {threshold}: {string.Join(" ", near)} | the city: {city}");
    }
}

// 0 3 7 8 10
// 3 0 4 5 7
// 7 4 0 1 3
// 8 5 1 0 2
// 10 7 3 2 0
// within 6: 1 3 3 3 2 | the city: 0
package main

import (
	"fmt"
	"math"
	"strconv"
	"strings"
)

func main() {
	n, threshold := 5, 6 // every pair at once; two-way roads {u, v, w}
	edges := [][3]int{{0, 1, 3}, {1, 2, 4}, {0, 3, 9}, {2, 3, 1}, {3, 4, 2}, {1, 4, 8}}
	INF := math.MaxInt64 / 4
	d := make([][]int, n)
	for i := range d {
		d[i] = make([]int, n)
		for j := range d[i] {
			if i != j {
				d[i][j] = INF
			}
		}
	}
	for _, e := range edges {
		if e[2] < d[e[0]][e[1]] {
			d[e[0]][e[1]], d[e[1]][e[0]] = e[2], e[2]
		}
	}
	for k := 0; k < n; k++ { // after round k, d[i][j] may pass through any of 0..k on the way
		for i := 0; i < n; i++ {
			for j := 0; j < n; j++ {
				if d[i][k]+d[k][j] < d[i][j] {
					d[i][j] = d[i][k] + d[k][j]
				}
			}
		}
	}
	near := make([]int, n)
	for i, row := range d {
		parts := []string{}
		for j, x := range row {
			parts = append(parts, strconv.Itoa(x))
			if j != i && x <= threshold {
				near[i]++
			}
		}
		fmt.Println(strings.Join(parts, " "))
	}
	city := 0
	for i := 0; i < n; i++ {
		if near[i] <= near[city] { // the fewest; on a tie, the larger number
			city = i
		}
	}
	parts := []string{}
	for _, x := range near {
		parts = append(parts, strconv.Itoa(x))
	}
	fmt.Printf("within %d: %s | the city: %d\n", threshold, strings.Join(parts, " "), city)
}

// 0 3 7 8 10
// 3 0 4 5 7
// 7 4 0 1 3
// 8 5 1 0 2
// 10 7 3 2 0
// within 6: 1 3 3 3 2 | the city: 0

Find the City With the Smallest Number of Neighbors at a Threshold Distance reads the matrix. Within 6, city 0 reaches only 1, at 3; cities 1, 2 and 3 reach three others each; city 4 reaches two. So the city is 0; on a tie it would be the larger number. Floyd-Warshall Algorithm asks for the matrix itself. With k innermost instead, a pair is finished before the paths it needs, which Mistakes that look right runs.

The lesson's five cities with two-way roads: 0 to 1 of 3, 1 to 2 of 4, 0 to 3 of 9, 2 to 3 of 1, 3 to 4 of 2 and 1 to 4 of 8, the route 0, 1, 2, 3, 4 drawn in green. Beside it, from 0, round by round: k = 1, 0 to 2 = 7, the first way, as 3 + 4 through 1; k = 2, 0 to 3 = 8, beating the road's 9, as the 7 found before plus 1; k = 3, 0 to 4 = 10, beating the 11 found through 1, as the 8 found before plus 2. A card: from 0, at the end: 0 to 0 is 0, to 1 is 3, to 2 is 7, to 3 is 8, to 4 is 10; after round k, any of 0 to k may lie on the way. Focal line: each round builds on what the rounds before found. Title: k on the outside.

Ratios as weights

Evaluate Division gives equations like x / y = 4 and asks for other quotients. Each equation is two edges, x → y weighing 4 and y → x weighing 1/4, and a quotient is the product of the weights along any path between the two: a search, with multiplying where the adding was:

from collections import deque


def calc_equation(equations, values, queries):  # Evaluate Division: a / b = x is an edge a -> b of x, b -> a of 1/x
    graph = {}
    for (a, b), x in zip(equations, values):
        graph.setdefault(a, []).append((b, x))
        graph.setdefault(b, []).append((a, 1 / x))
    answers = []
    for s, t in queries:
        if s not in graph or t not in graph:
            answers.append(-1.0)
            continue
        seen, queue, found = {s}, deque([(s, 1.0)]), -1.0
        while queue:                           # a path's value is the product of its edges
            u, product = queue.popleft()
            if u == t:
                found = product
                break
            for v, x in graph[u]:
                if v not in seen:
                    seen.add(v)
                    queue.append((v, product * x))
        answers.append(found)
    return answers


queries = [["x", "w"], ["w", "y"], ["y", "x"], ["x", "q"], ["z", "z"]]
answers = calc_equation([["x", "y"], ["y", "z"], ["z", "w"]], [4.0, 0.5, 3.0], queries)
print(" | ".join(f"{s} / {t} = {a:.5f}" for (s, t), a in zip(queries, answers)))

# x / w = 6.00000 | w / y = 0.66667 | y / x = 0.25000 | x / q = -1.00000 | z / z = 1.00000
import java.util.*;

public class Main {
    static double[] calcEquation(String[][] equations, double[] values, String[][] queries) {   // a / b = x
        Map<String, Map<String, Double>> graph = new HashMap<>();   // an edge a -> b of x, and b -> a of 1/x
        for (int i = 0; i < equations.length; i++) {
            String a = equations[i][0], b = equations[i][1];
            graph.computeIfAbsent(a, k -> new HashMap<>()).put(b, values[i]);
            graph.computeIfAbsent(b, k -> new HashMap<>()).put(a, 1 / values[i]);
        }
        double[] answers = new double[queries.length];
        for (int q = 0; q < queries.length; q++) {
            String s = queries[q][0], t = queries[q][1];
            answers[q] = -1.0;
            if (!graph.containsKey(s) || !graph.containsKey(t)) continue;
            Set<String> seen = new HashSet<>(List.of(s));
            ArrayDeque<Object[]> queue = new ArrayDeque<>();
            queue.add(new Object[] {s, 1.0});
            while (!queue.isEmpty()) {               // a path's value is the product of its edges
                Object[] top = queue.poll();
                String u = (String) top[0];
                double product = (Double) top[1];
                if (u.equals(t)) {
                    answers[q] = product;
                    break;
                }
                for (Map.Entry<String, Double> e : graph.get(u).entrySet()) {
                    if (seen.add(e.getKey())) queue.add(new Object[] {e.getKey(), product * e.getValue()});
                }
            }
        }
        return answers;
    }

    public static void main(String[] args) {
        String[][] queries = {{"x", "w"}, {"w", "y"}, {"y", "x"}, {"x", "q"}, {"z", "z"}};
        double[] answers = calcEquation(new String[][] {{"x", "y"}, {"y", "z"}, {"z", "w"}}, new double[] {4.0, 0.5, 3.0},
                queries);
        StringJoiner s = new StringJoiner(" | ");
        for (int i = 0; i < queries.length; i++)
            s.add(queries[i][0] + " / " + queries[i][1] + " = " + String.format(Locale.ROOT, "%.5f", answers[i]));
        System.out.println(s);
    }
}

// x / w = 6.00000 | w / y = 0.66667 | y / x = 0.25000 | x / q = -1.00000 | z / z = 1.00000
#include <bits/stdc++.h>
using namespace std;

vector<double> calcEquation(const vector<pair<string, string>>& equations, const vector<double>& values,
                            const vector<pair<string, string>>& queries) {   // a / b = x: a -> b of x, b -> a of 1/x
    map<string, vector<pair<string, double>>> graph;
    for (size_t i = 0; i < equations.size(); i++) {
        auto& [a, b] = equations[i];
        graph[a].push_back({b, values[i]});
        graph[b].push_back({a, 1 / values[i]});
    }
    vector<double> answers;
    for (auto& [s, t] : queries) {
        double found = -1.0;
        if (graph.count(s) && graph.count(t)) {
            set<string> seen = {s};
            queue<pair<string, double>> q;
            q.push({s, 1.0});
            while (!q.empty()) {                     // a path's value is the product of its edges
                auto [u, product] = q.front();
                q.pop();
                if (u == t) {
                    found = product;
                    break;
                }
                for (auto& [v, x] : graph[u])
                    if (seen.insert(v).second) q.push({v, product * x});
            }
        }
        answers.push_back(found);
    }
    return answers;
}

int main() {
    vector<pair<string, string>> queries = {{"x", "w"}, {"w", "y"}, {"y", "x"}, {"x", "q"}, {"z", "z"}};
    auto answers = calcEquation({{"x", "y"}, {"y", "z"}, {"z", "w"}}, {4.0, 0.5, 3.0}, queries);
    for (size_t i = 0; i < queries.size(); i++)
        printf("%s%s / %s = %.5f", i ? " | " : "", queries[i].first.c_str(), queries[i].second.c_str(), answers[i]);
    printf("\n");
}

// x / w = 6.00000 | w / y = 0.66667 | y / x = 0.25000 | x / q = -1.00000 | z / z = 1.00000
function calcEquation(equations, values, queries) {   // Evaluate Division: a / b = x is a -> b of x, b -> a of 1/x
    const graph = new Map();
    equations.forEach(([a, b], i) => {
        if (!graph.has(a)) graph.set(a, []);
        if (!graph.has(b)) graph.set(b, []);
        graph.get(a).push([b, values[i]]);
        graph.get(b).push([a, 1 / values[i]]);
    });
    return queries.map(([s, t]) => {
        if (!graph.has(s) || !graph.has(t)) return -1.0;
        const seen = new Set([s]), queue = [[s, 1.0]];
        for (let head = 0; head < queue.length; head++) {   // a path's value is the product of its edges
            const [u, product] = queue[head];
            if (u === t) return product;
            for (const [v, x] of graph.get(u)) {
                if (!seen.has(v)) {
                    seen.add(v);
                    queue.push([v, product * x]);
                }
            }
        }
        return -1.0;
    });
}

const queries = [["x", "w"], ["w", "y"], ["y", "x"], ["x", "q"], ["z", "z"]];
const answers = calcEquation([["x", "y"], ["y", "z"], ["z", "w"]], [4.0, 0.5, 3.0], queries);
console.log(queries.map(([s, t], i) => `${s} / ${t} = ${answers[i].toFixed(5)}`).join(" | "));

// x / w = 6.00000 | w / y = 0.66667 | y / x = 0.25000 | x / q = -1.00000 | z / z = 1.00000
function calcEquation(equations: [string, string][], values: number[], queries: [string, string][]): number[] {
    const graph = new Map<string, [string, number][]>();   // a / b = x is an edge a -> b of x, and b -> a of 1/x
    equations.forEach(([a, b], i) => {
        if (!graph.has(a)) graph.set(a, []);
        if (!graph.has(b)) graph.set(b, []);
        graph.get(a)!.push([b, values[i]]);
        graph.get(b)!.push([a, 1 / values[i]]);
    });
    return queries.map(([s, t]) => {
        if (!graph.has(s) || !graph.has(t)) return -1.0;
        const seen = new Set<string>([s]), queue: [string, number][] = [[s, 1.0]];
        for (let head = 0; head < queue.length; head++) {   // a path's value is the product of its edges
            const [u, product] = queue[head];
            if (u === t) return product;
            for (const [v, x] of graph.get(u)!) {
                if (!seen.has(v)) {
                    seen.add(v);
                    queue.push([v, product * x]);
                }
            }
        }
        return -1.0;
    });
}

const queries: [string, string][] = [["x", "w"], ["w", "y"], ["y", "x"], ["x", "q"], ["z", "z"]];
const answers = calcEquation([["x", "y"], ["y", "z"], ["z", "w"]], [4.0, 0.5, 3.0], queries);
console.log(queries.map(([s, t], i) => `${s} / ${t} = ${answers[i].toFixed(5)}`).join(" | "));

// x / w = 6.00000 | w / y = 0.66667 | y / x = 0.25000 | x / q = -1.00000 | z / z = 1.00000
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;

class Program {
    static double[] CalcEquation(string[][] equations, double[] values, string[][] queries) {   // a / b = x
        var graph = new Dictionary<string, List<(string v, double x)>>();   // a -> b of x, and b -> a of 1/x
        for (int i = 0; i < equations.Length; i++) {
            string a = equations[i][0], b = equations[i][1];
            if (!graph.ContainsKey(a)) graph[a] = new List<(string, double)>();
            if (!graph.ContainsKey(b)) graph[b] = new List<(string, double)>();
            graph[a].Add((b, values[i]));
            graph[b].Add((a, 1 / values[i]));
        }
        return queries.Select(q => {
            string s = q[0], t = q[1];
            if (!graph.ContainsKey(s) || !graph.ContainsKey(t)) return -1.0;
            var seen = new HashSet<string> { s };
            var queue = new Queue<(string u, double product)>();
            queue.Enqueue((s, 1.0));
            while (queue.Count > 0) {                 // a path's value is the product of its edges
                var (u, product) = queue.Dequeue();
                if (u == t) return product;
                foreach (var (v, x) in graph[u])
                    if (seen.Add(v)) queue.Enqueue((v, product * x));
            }
            return -1.0;
        }).ToArray();
    }

    static void Main() {
        var queries = new[] { new[] {"x", "w"}, new[] {"w", "y"}, new[] {"y", "x"}, new[] {"x", "q"}, new[] {"z", "z"} };
        var answers = CalcEquation(new[] { new[] {"x", "y"}, new[] {"y", "z"}, new[] {"z", "w"} }, new[] {4.0, 0.5, 3.0},
                                   queries);
        Console.WriteLine(string.Join(" | ", queries.Select((q, i) =>
            $"{q[0]} / {q[1]} = {answers[i].ToString("F5", CultureInfo.InvariantCulture)}")));
    }
}

// x / w = 6.00000 | w / y = 0.66667 | y / x = 0.25000 | x / q = -1.00000 | z / z = 1.00000
package main

import (
	"fmt"
	"strings"
)

type edge struct {
	v string
	x float64
}

func calcEquation(equations [][2]string, values []float64, queries [][2]string) []float64 { // a / b = x
	graph := map[string][]edge{} // an edge a -> b of x, and b -> a of 1/x
	for i, e := range equations {
		graph[e[0]] = append(graph[e[0]], edge{e[1], values[i]})
		graph[e[1]] = append(graph[e[1]], edge{e[0], 1 / values[i]})
	}
	answers := []float64{}
	for _, q := range queries {
		s, t := q[0], q[1]
		found := -1.0
		_, okS := graph[s]
		_, okT := graph[t]
		if okS && okT {
			seen := map[string]bool{s: true}
			queue := []edge{{s, 1.0}}
			for head := 0; head < len(queue); head++ { // a path's value is the product of its edges
				u, product := queue[head].v, queue[head].x
				if u == t {
					found = product
					break
				}
				for _, e := range graph[u] {
					if !seen[e.v] {
						seen[e.v] = true
						queue = append(queue, edge{e.v, product * e.x})
					}
				}
			}
		}
		answers = append(answers, found)
	}
	return answers
}

func main() {
	queries := [][2]string{{"x", "w"}, {"w", "y"}, {"y", "x"}, {"x", "q"}, {"z", "z"}}
	answers := calcEquation([][2]string{{"x", "y"}, {"y", "z"}, {"z", "w"}}, []float64{4.0, 0.5, 3.0}, queries)
	parts := []string{}
	for i, q := range queries {
		parts = append(parts, fmt.Sprintf("%s / %s = %.5f", q[0], q[1], answers[i]))
	}
	fmt.Println(strings.Join(parts, " | "))
}

// x / w = 6.00000 | w / y = 0.66667 | y / x = 0.25000 | x / q = -1.00000 | z / z = 1.00000

x / w multiplies 4, 0.5 and 3: 6. w / y goes back along two edges, 1/3 and 1/0.5, which is 0.66667. q is in no equation, so −1, and z / z is 1.

A*: aim the search

Dijkstra spreads out in rings, whichever way the target lies. A* orders the heap by the distance so far plus a guess of the distance left, here the Manhattan distance, on a grid where every step costs 1. The guess never overestimates, so the first time the target comes off, its distance is still the shortest; but the search heads for the target instead of everywhere:

import heapq

R = C = 40                                     # a grid of 40 x 40, every step costing 1; from row 5, column 5 to column 35
START, GOAL = (5, 5), (5, 35)


def search(walls, aim):                        # aim: order by steps so far + the Manhattan distance left (A*)
    def h(r, c):
        return abs(r - GOAL[0]) + abs(c - GOAL[1]) if aim else 0
    g = {START: 0}
    heap = [(h(*START), 0, START[0] * C + START[1])]   # (f, g, cell): a tie goes to the smaller g, then the cell
    done, taken = set(), 0
    while heap:
        f, steps, cell = heapq.heappop(heap)
        if cell in done:
            continue
        done.add(cell)
        taken += 1
        r, c = divmod(cell, C)
        if (r, c) == GOAL:
            return steps, taken
        for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
            nr, nc = r + dr, c + dc
            if 0 <= nr < R and 0 <= nc < C and (nr, nc) not in walls and steps + 1 < g.get((nr, nc), float("inf")):
                g[(nr, nc)] = steps + 1
                heapq.heappush(heap, (steps + 1 + h(nr, nc), steps + 1, nr * C + nc))
    return -1, taken


for name, walls in (("an open grid", set()), ("a wall in the way", {(r, 20) for r in range(30)})):
    (d1, dijkstra), (d2, astar) = search(walls, False), search(walls, True)
    print(f"{name}: Dijkstra takes {dijkstra} cells off, A* {astar}; both find {d1} steps" if d1 == d2 else "different")

# an open grid: Dijkstra takes 766 cells off, A* 31; both find 30 steps
# a wall in the way: Dijkstra takes 1521 cells off, A* 996; both find 80 steps
import java.util.*;

public class Main {
    static final int R = 40, C = 40;                 // a grid of 40 x 40, every step costing 1
    static final int[] START = {5, 5}, GOAL = {5, 35};

    static int[] search(boolean[][] wall, boolean aim) {   // aim: by steps so far + the Manhattan distance left (A*)
        int[] g = new int[R * C];
        Arrays.fill(g, Integer.MAX_VALUE);
        boolean[] done = new boolean[R * C];
        PriorityQueue<int[]> heap = new PriorityQueue<>((a, b) -> a[0] != b[0] ? Integer.compare(a[0], b[0])
                : a[1] != b[1] ? Integer.compare(a[1], b[1]) : Integer.compare(a[2], b[2]));   // {f, g, cell}
        int start = START[0] * C + START[1];
        g[start] = 0;
        heap.add(new int[] {aim ? h(START[0], START[1]) : 0, 0, start});
        int taken = 0;
        int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
        while (!heap.isEmpty()) {
            int[] top = heap.poll();
            int steps = top[1], cell = top[2];
            if (done[cell]) continue;
            done[cell] = true;
            taken++;
            int r = cell / C, c = cell % C;
            if (r == GOAL[0] && c == GOAL[1]) return new int[] {steps, taken};
            for (int[] d : dirs) {
                int nr = r + d[0], nc = c + d[1];
                if (nr < 0 || nr >= R || nc < 0 || nc >= C || wall[nr][nc] || steps + 1 >= g[nr * C + nc]) continue;
                g[nr * C + nc] = steps + 1;
                heap.add(new int[] {steps + 1 + (aim ? h(nr, nc) : 0), steps + 1, nr * C + nc});
            }
        }
        return new int[] {-1, taken};
    }

    static int h(int r, int c) {
        return Math.abs(r - GOAL[0]) + Math.abs(c - GOAL[1]);
    }

    public static void main(String[] args) {
        boolean[][] open = new boolean[R][C], walled = new boolean[R][C];
        for (int r = 0; r < 30; r++) walled[r][20] = true;
        String[] names = {"an open grid", "a wall in the way"};
        boolean[][][] grids = {open, walled};
        for (int i = 0; i < 2; i++) {
            int[] dijkstra = search(grids[i], false), astar = search(grids[i], true);
            System.out.println(dijkstra[0] == astar[0] ? names[i] + ": Dijkstra takes " + dijkstra[1] + " cells off, A* "
                    + astar[1] + "; both find " + dijkstra[0] + " steps" : "different");
        }
    }
}

// an open grid: Dijkstra takes 766 cells off, A* 31; both find 30 steps
// a wall in the way: Dijkstra takes 1521 cells off, A* 996; both find 80 steps
#include <bits/stdc++.h>
using namespace std;

const int R = 40, C = 40;                            // a grid of 40 x 40, every step costing 1
const int SR = 5, SC = 5, GR = 5, GC = 35;             // from row 5, column 5 to row 5, column 35

int h(int r, int c) { return abs(r - GR) + abs(c - GC); }

pair<int, int> search(const vector<vector<char>>& wall, bool aim) {   // aim: steps so far + Manhattan distance left
    vector<int> g(R * C, INT_MAX);
    vector<char> done(R * C, 0);
    priority_queue<tuple<int, int, int>, vector<tuple<int, int, int>>, greater<>> heap;   // {f, g, cell}
    g[SR * C + SC] = 0;
    heap.push({aim ? h(SR, SC) : 0, 0, SR * C + SC});
    int taken = 0;
    const int dirs[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
    while (!heap.empty()) {
        auto [f, steps, cell] = heap.top();
        heap.pop();
        if (done[cell]) continue;
        done[cell] = 1;
        taken++;
        int r = cell / C, c = cell % C;
        if (r == GR && c == GC) return {steps, taken};
        for (auto& d : dirs) {
            int nr = r + d[0], nc = c + d[1];
            if (nr < 0 || nr >= R || nc < 0 || nc >= C || wall[nr][nc] || steps + 1 >= g[nr * C + nc]) continue;
            g[nr * C + nc] = steps + 1;
            heap.push({steps + 1 + (aim ? h(nr, nc) : 0), steps + 1, nr * C + nc});
        }
    }
    return {-1, taken};
}

int main() {
    vector<vector<char>> open(R, vector<char>(C, 0)), walled = open;
    for (int r = 0; r < 30; r++) walled[r][20] = 1;
    vector<pair<string, vector<vector<char>>>> grids = {{"an open grid", open}, {"a wall in the way", walled}};
    for (auto& [name, wall] : grids) {
        auto [d1, dijkstra] = search(wall, false);
        auto [d2, astar] = search(wall, true);
        if (d1 != d2) cout << "different\n";
        else cout << name << ": Dijkstra takes " << dijkstra << " cells off, A* " << astar << "; both find " << d1 << " steps\n";
    }
}

// an open grid: Dijkstra takes 766 cells off, A* 31; both find 30 steps
// a wall in the way: Dijkstra takes 1521 cells off, A* 996; both find 80 steps
class Heap {                                   // a min-heap of arrays, compared item by item
    constructor() { this.a = []; }
    get size() { return this.a.length; }
    less(i, j) {
        const x = this.a[i], y = this.a[j];
        for (let k = 0; k < x.length; k++) if (x[k] !== y[k]) return x[k] < y[k];
        return false;
    }
    push(item) {
        const a = this.a;
        a.push(item);
        for (let i = a.length - 1; i > 0;) {
            const p = (i - 1) >> 1;
            if (!this.less(i, p)) break;
            [a[i], a[p]] = [a[p], a[i]];
            i = p;
        }
    }
    pop() {
        const a = this.a, top = a[0], last = a.pop();
        if (a.length) {
            a[0] = last;
            for (let i = 0; ;) {
                const l = 2 * i + 1, r = l + 1;
                let m = i;
                if (l < a.length && this.less(l, m)) m = l;
                if (r < a.length && this.less(r, m)) m = r;
                if (m === i) break;
                [a[i], a[m]] = [a[m], a[i]];
                i = m;
            }
        }
        return top;
    }
}

const R = 40, C = 40;                          // a grid of 40 x 40, every step costing 1; row 5: column 5 to column 35
const START = [5, 5], GOAL = [5, 35];
const h = (r, c) => Math.abs(r - GOAL[0]) + Math.abs(c - GOAL[1]);

function search(wall, aim) {                   // aim: order by steps so far + the Manhattan distance left (A*)
    const g = new Array(R * C).fill(Infinity), done = new Array(R * C).fill(false);
    const heap = new Heap();                   // [f, g, cell]: a tie goes to the smaller g, then the cell
    const start = START[0] * C + START[1];
    g[start] = 0;
    heap.push([aim ? h(START[0], START[1]) : 0, 0, start]);
    let taken = 0;
    while (heap.size) {
        const [, steps, cell] = heap.pop();
        if (done[cell]) continue;
        done[cell] = true;
        taken++;
        const r = Math.floor(cell / C), c = cell % C;
        if (r === GOAL[0] && c === GOAL[1]) return [steps, taken];
        for (const [dr, dc] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
            const nr = r + dr, nc = c + dc;
            if (nr < 0 || nr >= R || nc < 0 || nc >= C || wall[nr][nc] || steps + 1 >= g[nr * C + nc]) continue;
            g[nr * C + nc] = steps + 1;
            heap.push([steps + 1 + (aim ? h(nr, nc) : 0), steps + 1, nr * C + nc]);
        }
    }
    return [-1, taken];
}

const open = Array.from({ length: R }, () => new Array(C).fill(false));
const walled = Array.from({ length: R }, (_, r) => Array.from({ length: C }, (_, c) => c === 20 && r < 30));
for (const [name, wall] of [["an open grid", open], ["a wall in the way", walled]]) {
    const [d1, dijkstra] = search(wall, false), [d2, astar] = search(wall, true);
    console.log(d1 === d2 ? `${name}: Dijkstra takes ${dijkstra} cells off, A* ${astar}; both find ${d1} steps` : "different");
}

// an open grid: Dijkstra takes 766 cells off, A* 31; both find 30 steps
// a wall in the way: Dijkstra takes 1521 cells off, A* 996; both find 80 steps
class Heap {                                   // a min-heap of arrays, compared item by item
    private a: number[][] = [];
    get size(): number { return this.a.length; }
    private less(i: number, j: number): boolean {
        const x = this.a[i], y = this.a[j];
        for (let k = 0; k < x.length; k++) if (x[k] !== y[k]) return x[k] < y[k];
        return false;
    }
    push(item: number[]): void {
        const a = this.a;
        a.push(item);
        for (let i = a.length - 1; i > 0;) {
            const p = (i - 1) >> 1;
            if (!this.less(i, p)) break;
            [a[i], a[p]] = [a[p], a[i]];
            i = p;
        }
    }
    pop(): number[] {
        const a = this.a, top = a[0], last = a.pop()!;
        if (a.length) {
            a[0] = last;
            for (let i = 0; ;) {
                const l = 2 * i + 1, r = l + 1;
                let m = i;
                if (l < a.length && this.less(l, m)) m = l;
                if (r < a.length && this.less(r, m)) m = r;
                if (m === i) break;
                [a[i], a[m]] = [a[m], a[i]];
                i = m;
            }
        }
        return top;
    }
}

const R = 40, C = 40;                          // a grid of 40 x 40, every step costing 1; row 5: column 5 to column 35
const START = [5, 5], GOAL = [5, 35];
const h = (r: number, c: number): number => Math.abs(r - GOAL[0]) + Math.abs(c - GOAL[1]);

function search(wall: boolean[][], aim: boolean): [number, number] {   // aim: steps so far + Manhattan left (A*)
    const g: number[] = new Array(R * C).fill(Infinity), done: boolean[] = new Array(R * C).fill(false);
    const heap = new Heap();                   // [f, g, cell]: a tie goes to the smaller g, then the cell
    const start = START[0] * C + START[1];
    g[start] = 0;
    heap.push([aim ? h(START[0], START[1]) : 0, 0, start]);
    let taken = 0;
    while (heap.size) {
        const [, steps, cell] = heap.pop();
        if (done[cell]) continue;
        done[cell] = true;
        taken++;
        const r = Math.floor(cell / C), c = cell % C;
        if (r === GOAL[0] && c === GOAL[1]) return [steps, taken];
        for (const [dr, dc] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
            const nr = r + dr, nc = c + dc;
            if (nr < 0 || nr >= R || nc < 0 || nc >= C || wall[nr][nc] || steps + 1 >= g[nr * C + nc]) continue;
            g[nr * C + nc] = steps + 1;
            heap.push([steps + 1 + (aim ? h(nr, nc) : 0), steps + 1, nr * C + nc]);
        }
    }
    return [-1, taken];
}

const open: boolean[][] = Array.from({ length: R }, () => new Array<boolean>(C).fill(false));
const walled: boolean[][] = Array.from({ length: R }, (_, r) => Array.from({ length: C }, (_, c) => c === 20 && r < 30));
const grids: [string, boolean[][]][] = [["an open grid", open], ["a wall in the way", walled]];
for (const [name, wall] of grids) {
    const [d1, dijkstra] = search(wall, false), [d2, astar] = search(wall, true);
    console.log(d1 === d2 ? `${name}: Dijkstra takes ${dijkstra} cells off, A* ${astar}; both find ${d1} steps` : "different");
}

// an open grid: Dijkstra takes 766 cells off, A* 31; both find 30 steps
// a wall in the way: Dijkstra takes 1521 cells off, A* 996; both find 80 steps
using System;
using System.Collections.Generic;

class Program {
    const int R = 40, C = 40;                         // a grid of 40 x 40, every step costing 1
    static readonly int[] Start = {5, 5}, Goal = {5, 35};

    static int H(int r, int c) => Math.Abs(r - Goal[0]) + Math.Abs(c - Goal[1]);

    static (int, int) Search(bool[,] wall, bool aim) {   // aim: by steps so far + the Manhattan distance left (A*)
        var g = new int[R * C];
        Array.Fill(g, int.MaxValue);
        var done = new bool[R * C];
        var heap = new PriorityQueue<int, (int, int, int)>();   // a cell, ordered by (f, g, cell)
        int start = Start[0] * C + Start[1];
        g[start] = 0;
        heap.Enqueue(start, (aim ? H(Start[0], Start[1]) : 0, 0, start));
        int taken = 0;
        int[][] dirs = { new[] {1, 0}, new[] {-1, 0}, new[] {0, 1}, new[] {0, -1} };
        while (heap.TryDequeue(out int cell, out var key)) {
            int steps = key.Item2;
            if (done[cell]) continue;
            done[cell] = true;
            taken++;
            int r = cell / C, c = cell % C;
            if (r == Goal[0] && c == Goal[1]) return (steps, taken);
            foreach (var d in dirs) {
                int nr = r + d[0], nc = c + d[1];
                if (nr < 0 || nr >= R || nc < 0 || nc >= C || wall[nr, nc] || steps + 1 >= g[nr * C + nc]) continue;
                g[nr * C + nc] = steps + 1;
                heap.Enqueue(nr * C + nc, (steps + 1 + (aim ? H(nr, nc) : 0), steps + 1, nr * C + nc));
            }
        }
        return (-1, taken);
    }

    static void Main() {
        var open = new bool[R, C];
        var walled = new bool[R, C];
        for (int r = 0; r < 30; r++) walled[r, 20] = true;
        foreach (var (name, wall) in new[] { ("an open grid", open), ("a wall in the way", walled) }) {
            var (d1, dijkstra) = Search(wall, false);
            var (d2, astar) = Search(wall, true);
            Console.WriteLine(d1 == d2 ? $"{name}: Dijkstra takes {dijkstra} cells off, A* {astar}; both find {d1} steps" : "different");
        }
    }
}

// an open grid: Dijkstra takes 766 cells off, A* 31; both find 30 steps
// a wall in the way: Dijkstra takes 1521 cells off, A* 996; both find 80 steps
package main

import (
	"container/heap"
	"fmt"
	"math"
)

type node struct{ f, g, cell int }
type nodeHeap []node // ordered by f, then g, then cell

func (h nodeHeap) Len() int { return len(h) }
func (h nodeHeap) Less(i, j int) bool {
	a, b := h[i], h[j]
	if a.f != b.f {
		return a.f < b.f
	}
	if a.g != b.g {
		return a.g < b.g
	}
	return a.cell < b.cell
}
func (h nodeHeap) Swap(i, j int)       { h[i], h[j] = h[j], h[i] }
func (h *nodeHeap) Push(x interface{}) { *h = append(*h, x.(node)) }
func (h *nodeHeap) Pop() interface{} {
	old := *h
	x := old[len(old)-1]
	*h = old[:len(old)-1]
	return x
}

const R, C = 40, 40 // a grid of 40 x 40, every step costing 1; row 5: column 5 to column 35
var start, goal = [2]int{5, 5}, [2]int{5, 35}

func manhattan(r, c int) int {
	dr, dc := r-goal[0], c-goal[1]
	if dr < 0 {
		dr = -dr
	}
	if dc < 0 {
		dc = -dc
	}
	return dr + dc
}

func search(wall [][]bool, aim bool) (int, int) { // aim: order by steps so far + the Manhattan distance left (A*)
	h := func(r, c int) int {
		if aim {
			return manhattan(r, c)
		}
		return 0
	}
	g := make([]int, R*C)
	for i := range g {
		g[i] = math.MaxInt64
	}
	done := make([]bool, R*C)
	s := start[0]*C + start[1]
	g[s] = 0
	pq := &nodeHeap{{h(start[0], start[1]), 0, s}}
	taken := 0
	dirs := [4][2]int{{1, 0}, {-1, 0}, {0, 1}, {0, -1}}
	for pq.Len() > 0 {
		top := heap.Pop(pq).(node)
		if done[top.cell] {
			continue
		}
		done[top.cell] = true
		taken++
		r, c := top.cell/C, top.cell%C
		if r == goal[0] && c == goal[1] {
			return top.g, taken
		}
		for _, d := range dirs {
			nr, nc := r+d[0], c+d[1]
			if nr < 0 || nr >= R || nc < 0 || nc >= C || wall[nr][nc] || top.g+1 >= g[nr*C+nc] {
				continue
			}
			g[nr*C+nc] = top.g + 1
			heap.Push(pq, node{top.g + 1 + h(nr, nc), top.g + 1, nr*C + nc})
		}
	}
	return -1, taken
}

func main() {
	open, walled := make([][]bool, R), make([][]bool, R)
	for r := range open {
		open[r], walled[r] = make([]bool, C), make([]bool, C)
		walled[r][20] = r < 30
	}
	names := []string{"an open grid", "a wall in the way"}
	for i, wall := range [][][]bool{open, walled} {
		d1, dijkstra := search(wall, false)
		d2, astar := search(wall, true)
		if d1 != d2 {
			fmt.Println("different")
			continue
		}
		fmt.Printf("%s: Dijkstra takes %d cells off, A* %d; both find %d steps\n", names[i], dijkstra, astar, d1)
	}
}

// an open grid: Dijkstra takes 766 cells off, A* 31; both find 30 steps
// a wall in the way: Dijkstra takes 1521 cells off, A* 996; both find 80 steps

On the open grid A* took 31 cells off, the straight line, where Dijkstra took 766. With a wall in the way the guess is too hopeful beside the wall, so A* spreads around it too: 996 cells against 1,521, and both find the same 80 steps. The better the guess, the less A* searches; a guess of 0 is Dijkstra.

Two cases of the lesson's grid of 40 by 40, every step costing 1, from row 5, column 5 to row 5, column 35, both marked with a dark dot; each case drawn twice, the cells Dijkstra takes off before the target in orange and the cells A* takes off in green. An open grid: Dijkstra takes 766 cells off, spreading out around the start; A* takes 31, the straight line along row 5. A wall in the way, down column 20 from row 0 to row 29, drawn dark: Dijkstra takes 1,521 cells off, nearly the whole grid; A* takes 996, spreading beside the wall. Two cards: both find 30 steps, the guess points straight at the target; both find 80 steps, by the wall the guess is too hopeful. Focal line: the better the guess, the less A* searches. Title: a heuristic aims the search.

Which one to use

The graphUseTime
Every step costs the sameBFSO(V + E)
Steps cost 0 or 10-1 BFS, a dequeO(V + E)
No negative weight, one sourceDijkstra, a heapO((V + E) log V)
Negative weights, or at most k edgesBellman-FordO(V × E)
A DAG, any weightsRelax in topological orderO(V + E)
Every pair, n in the hundredsFloyd-WarshallO(n³)
One target and a guess of what's leftA*Less than Dijkstra

Near misses: BFS and 0-1 BFS are Shortest Paths with BFS; the cheapest way to connect every vertex, rather than to reach one, is a minimum spanning tree, in Union-Find and Minimum Spanning Trees.

Mistakes that look right

Two of these run below in every language. The others are just as common:

  • Relaxing in place for k stops: one round strings several flights together.
  • k as the inner loop of Floyd-Warshall: a pair is finished before the paths it needs.
  • Dijkstra with a negative edge: a vertex settled early is never undercut, as above.
  • No stale check: a vertex settled again from an old entry, its edges relaxed again for nothing.
  • A plain queue for weighted edges: right, but 1,595,745 cells taken off for 90,000.
  • Infinity plus a weight: with the largest integer standing for infinity, adding a weight overflows to a negative number in languages with fixed-size integers; test for infinity first, or pick a smaller infinity.
  • The sum where the largest step counts: Path With Minimum Effort's cost is a max, not a total.
def cheapest(n, flights, src, dst, k, from_copy):   # Cheapest Flights, each round from a copy or in place
    cost = [float("inf")] * n
    cost[src] = 0
    for _ in range(k + 1):
        before = cost[:] if from_copy else cost
        for u, v, price in flights:
            if before[u] + price < cost[v]:
                cost[v] = before[u] + price
    return -1 if cost[dst] == float("inf") else cost[dst]


def floyd(n, edges, k_outside):                # Floyd-Warshall with k as the outer loop, or the inner one
    d = [[0 if i == j else float("inf") for j in range(n)] for i in range(n)]
    for u, v, w in edges:
        d[u][v] = w
    rounds = range(n)
    if k_outside:
        for k in rounds:
            for i in rounds:
                for j in rounds:
                    d[i][j] = min(d[i][j], d[i][k] + d[k][j])
    else:
        for i in rounds:
            for j in rounds:
                for k in rounds:
                    d[i][j] = min(d[i][j], d[i][k] + d[k][j])
    return d


flights = [[0, 1, 50], [1, 2, 50], [2, 3, 50], [0, 3, 400], [1, 3, 300], [0, 2, 200]]
print("k = 0, relaxed in place:", cheapest(4, flights, 0, 3, 0, False), "| from a copy:", cheapest(4, flights, 0, 3, 0, True))
edges = [(0, 1, 1), (1, 3, 1), (3, 2, 1), (0, 2, 10)]
print("0 to 2 with k inside:", floyd(4, edges, False)[0][2], "| with k outside:", floyd(4, edges, True)[0][2])

# k = 0, relaxed in place: 150 | from a copy: 400
# 0 to 2 with k inside: 10 | with k outside: 3
import java.util.*;

public class Main {
    static final long INF = Long.MAX_VALUE / 4;

    static long cheapest(int n, int[][] flights, int src, int dst, int k, boolean fromCopy) {   // copy or in place
        long[] cost = new long[n];
        Arrays.fill(cost, INF);
        cost[src] = 0;
        for (int round = 0; round <= k; round++) {
            long[] before = fromCopy ? cost.clone() : cost;
            for (int[] f : flights) if (before[f[0]] + f[2] < cost[f[1]]) cost[f[1]] = before[f[0]] + f[2];
        }
        return cost[dst] >= INF ? -1 : cost[dst];
    }

    static long[][] floyd(int n, int[][] edges, boolean kOutside) {   // k as the outer loop, or the inner one
        long[][] d = new long[n][n];
        for (int i = 0; i < n; i++) {
            Arrays.fill(d[i], INF);
            d[i][i] = 0;
        }
        for (int[] e : edges) d[e[0]][e[1]] = e[2];
        if (kOutside) {
            for (int k = 0; k < n; k++)
                for (int i = 0; i < n; i++)
                    for (int j = 0; j < n; j++) d[i][j] = Math.min(d[i][j], d[i][k] + d[k][j]);
        } else {
            for (int i = 0; i < n; i++)
                for (int j = 0; j < n; j++)
                    for (int k = 0; k < n; k++) d[i][j] = Math.min(d[i][j], d[i][k] + d[k][j]);
        }
        return d;
    }

    public static void main(String[] args) {
        int[][] flights = {{0, 1, 50}, {1, 2, 50}, {2, 3, 50}, {0, 3, 400}, {1, 3, 300}, {0, 2, 200}};
        System.out.println("k = 0, relaxed in place: " + cheapest(4, flights, 0, 3, 0, false) + " | from a copy: "
                + cheapest(4, flights, 0, 3, 0, true));
        int[][] edges = {{0, 1, 1}, {1, 3, 1}, {3, 2, 1}, {0, 2, 10}};
        System.out.println("0 to 2 with k inside: " + floyd(4, edges, false)[0][2] + " | with k outside: "
                + floyd(4, edges, true)[0][2]);
    }
}

// k = 0, relaxed in place: 150 | from a copy: 400
// 0 to 2 with k inside: 10 | with k outside: 3
#include <bits/stdc++.h>
using namespace std;

const long long INF = LLONG_MAX / 4;

long long cheapest(int n, const vector<array<int, 3>>& flights, int src, int dst, int k, bool fromCopy) {
    vector<long long> cost(n, INF);                  // Cheapest Flights, each round from a copy or in place
    cost[src] = 0;
    for (int round = 0; round <= k; round++) {
        vector<long long> copy = cost;
        vector<long long>& before = fromCopy ? copy : cost;
        for (auto& f : flights) if (before[f[0]] + f[2] < cost[f[1]]) cost[f[1]] = before[f[0]] + f[2];
    }
    return cost[dst] >= INF ? -1 : cost[dst];
}

vector<vector<long long>> floyd(int n, const vector<array<int, 3>>& edges, bool kOutside) {   // k outer, or inner
    vector<vector<long long>> d(n, vector<long long>(n, INF));
    for (int i = 0; i < n; i++) d[i][i] = 0;
    for (auto& e : edges) d[e[0]][e[1]] = e[2];
    if (kOutside) {
        for (int k = 0; k < n; k++)
            for (int i = 0; i < n; i++)
                for (int j = 0; j < n; j++) d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
    } else {
        for (int i = 0; i < n; i++)
            for (int j = 0; j < n; j++)
                for (int k = 0; k < n; k++) d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
    }
    return d;
}

int main() {
    vector<array<int, 3>> flights = {{0, 1, 50}, {1, 2, 50}, {2, 3, 50}, {0, 3, 400}, {1, 3, 300}, {0, 2, 200}};
    cout << "k = 0, relaxed in place: " << cheapest(4, flights, 0, 3, 0, false) << " | from a copy: "
         << cheapest(4, flights, 0, 3, 0, true) << "\n";
    vector<array<int, 3>> edges = {{0, 1, 1}, {1, 3, 1}, {3, 2, 1}, {0, 2, 10}};
    cout << "0 to 2 with k inside: " << floyd(4, edges, false)[0][2] << " | with k outside: " << floyd(4, edges, true)[0][2]
         << "\n";
}

// k = 0, relaxed in place: 150 | from a copy: 400
// 0 to 2 with k inside: 10 | with k outside: 3
function cheapest(n, flights, src, dst, k, fromCopy) {   // Cheapest Flights, each round from a copy or in place
    const cost = new Array(n).fill(Infinity);
    cost[src] = 0;
    for (let round = 0; round <= k; round++) {
        const before = fromCopy ? [...cost] : cost;
        for (const [u, v, price] of flights) if (before[u] + price < cost[v]) cost[v] = before[u] + price;
    }
    return cost[dst] === Infinity ? -1 : cost[dst];
}

function floyd(n, edges, kOutside) {           // Floyd-Warshall with k as the outer loop, or the inner one
    const d = Array.from({ length: n }, (_, i) => Array.from({ length: n }, (_, j) => (i === j ? 0 : Infinity)));
    for (const [u, v, w] of edges) d[u][v] = w;
    const relax = (i, j, k) => { d[i][j] = Math.min(d[i][j], d[i][k] + d[k][j]); };
    for (let a = 0; a < n; a++)
        for (let b = 0; b < n; b++)
            for (let c = 0; c < n; c++) {
                if (kOutside) relax(b, c, a);  // k = a, the outermost
                else relax(a, b, c);           // k = c, the innermost
            }
    return d;
}

const flights = [[0, 1, 50], [1, 2, 50], [2, 3, 50], [0, 3, 400], [1, 3, 300], [0, 2, 200]];
console.log(`k = 0, relaxed in place: ${cheapest(4, flights, 0, 3, 0, false)} | from a copy: ${cheapest(4, flights, 0, 3, 0, true)}`);
const edges = [[0, 1, 1], [1, 3, 1], [3, 2, 1], [0, 2, 10]];
console.log(`0 to 2 with k inside: ${floyd(4, edges, false)[0][2]} | with k outside: ${floyd(4, edges, true)[0][2]}`);

// k = 0, relaxed in place: 150 | from a copy: 400
// 0 to 2 with k inside: 10 | with k outside: 3
function cheapest(n: number, flights: number[][], src: number, dst: number, k: number, fromCopy: boolean): number {
    const cost: number[] = new Array(n).fill(Infinity);   // Cheapest Flights, each round from a copy or in place
    cost[src] = 0;
    for (let round = 0; round <= k; round++) {
        const before = fromCopy ? [...cost] : cost;
        for (const [u, v, price] of flights) if (before[u] + price < cost[v]) cost[v] = before[u] + price;
    }
    return cost[dst] === Infinity ? -1 : cost[dst];
}

function floyd(n: number, edges: number[][], kOutside: boolean): number[][] {   // k as the outer loop, or inner
    const d: number[][] = Array.from({ length: n }, (_, i) => Array.from({ length: n }, (_, j) => (i === j ? 0 : Infinity)));
    for (const [u, v, w] of edges) d[u][v] = w;
    const relax = (i: number, j: number, k: number): void => { d[i][j] = Math.min(d[i][j], d[i][k] + d[k][j]); };
    for (let a = 0; a < n; a++)
        for (let b = 0; b < n; b++)
            for (let c = 0; c < n; c++) {
                if (kOutside) relax(b, c, a);  // k = a, the outermost
                else relax(a, b, c);           // k = c, the innermost
            }
    return d;
}

const flights = [[0, 1, 50], [1, 2, 50], [2, 3, 50], [0, 3, 400], [1, 3, 300], [0, 2, 200]];
console.log(`k = 0, relaxed in place: ${cheapest(4, flights, 0, 3, 0, false)} | from a copy: ${cheapest(4, flights, 0, 3, 0, true)}`);
const edges = [[0, 1, 1], [1, 3, 1], [3, 2, 1], [0, 2, 10]];
console.log(`0 to 2 with k inside: ${floyd(4, edges, false)[0][2]} | with k outside: ${floyd(4, edges, true)[0][2]}`);

// k = 0, relaxed in place: 150 | from a copy: 400
// 0 to 2 with k inside: 10 | with k outside: 3
using System;

class Program {
    const long INF = long.MaxValue / 4;

    static long Cheapest(int n, int[][] flights, int src, int dst, int k, bool fromCopy) {   // a copy, or in place
        var cost = new long[n];
        Array.Fill(cost, INF);
        cost[src] = 0;
        for (int round = 0; round <= k; round++) {
            var before = fromCopy ? (long[]) cost.Clone() : cost;
            foreach (var f in flights) if (before[f[0]] + f[2] < cost[f[1]]) cost[f[1]] = before[f[0]] + f[2];
        }
        return cost[dst] >= INF ? -1 : cost[dst];
    }

    static long[,] Floyd(int n, int[][] edges, bool kOutside) {   // k as the outer loop, or the inner one
        var d = new long[n, n];
        for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) d[i, j] = i == j ? 0 : INF;
        foreach (var e in edges) d[e[0], e[1]] = e[2];
        if (kOutside) {
            for (int k = 0; k < n; k++)
                for (int i = 0; i < n; i++)
                    for (int j = 0; j < n; j++) d[i, j] = Math.Min(d[i, j], d[i, k] + d[k, j]);
        } else {
            for (int i = 0; i < n; i++)
                for (int j = 0; j < n; j++)
                    for (int k = 0; k < n; k++) d[i, j] = Math.Min(d[i, j], d[i, k] + d[k, j]);
        }
        return d;
    }

    static void Main() {
        int[][] flights = { new[] {0, 1, 50}, new[] {1, 2, 50}, new[] {2, 3, 50}, new[] {0, 3, 400}, new[] {1, 3, 300},
                            new[] {0, 2, 200} };
        Console.WriteLine($"k = 0, relaxed in place: {Cheapest(4, flights, 0, 3, 0, false)} | from a copy: {Cheapest(4, flights, 0, 3, 0, true)}");
        int[][] edges = { new[] {0, 1, 1}, new[] {1, 3, 1}, new[] {3, 2, 1}, new[] {0, 2, 10} };
        Console.WriteLine($"0 to 2 with k inside: {Floyd(4, edges, false)[0, 2]} | with k outside: {Floyd(4, edges, true)[0, 2]}");
    }
}

// k = 0, relaxed in place: 150 | from a copy: 400
// 0 to 2 with k inside: 10 | with k outside: 3
package main

import (
	"fmt"
	"math"
)

const INF = math.MaxInt64 / 4

func cheapest(n int, flights [][3]int, src, dst, k int, fromCopy bool) int { // each round from a copy, or in place
	cost := make([]int, n)
	for i := range cost {
		cost[i] = INF
	}
	cost[src] = 0
	for round := 0; round <= k; round++ {
		before := cost
		if fromCopy {
			before = append([]int{}, cost...)
		}
		for _, f := range flights {
			if before[f[0]]+f[2] < cost[f[1]] {
				cost[f[1]] = before[f[0]] + f[2]
			}
		}
	}
	if cost[dst] >= INF {
		return -1
	}
	return cost[dst]
}

func floyd(n int, edges [][3]int, kOutside bool) [][]int { // Floyd-Warshall with k as the outer loop, or the inner one
	d := make([][]int, n)
	for i := range d {
		d[i] = make([]int, n)
		for j := range d[i] {
			if i != j {
				d[i][j] = INF
			}
		}
	}
	for _, e := range edges {
		d[e[0]][e[1]] = e[2]
	}
	relax := func(i, j, k int) {
		if d[i][k]+d[k][j] < d[i][j] {
			d[i][j] = d[i][k] + d[k][j]
		}
	}
	for a := 0; a < n; a++ {
		for b := 0; b < n; b++ {
			for c := 0; c < n; c++ {
				if kOutside {
					relax(b, c, a) // k = a, the outermost
				} else {
					relax(a, b, c) // k = c, the innermost
				}
			}
		}
	}
	return d
}

func main() {
	flights := [][3]int{{0, 1, 50}, {1, 2, 50}, {2, 3, 50}, {0, 3, 400}, {1, 3, 300}, {0, 2, 200}}
	fmt.Printf("k = 0, relaxed in place: %d | from a copy: %d\n", cheapest(4, flights, 0, 3, 0, false), cheapest(4, flights, 0, 3, 0, true))
	edges := [][3]int{{0, 1, 1}, {1, 3, 1}, {3, 2, 1}, {0, 2, 10}}
	fmt.Printf("0 to 2 with k inside: %d | with k outside: %d\n", floyd(4, edges, false)[0][2], floyd(4, edges, true)[0][2])
}

// k = 0, relaxed in place: 150 | from a copy: 400
// 0 to 2 with k inside: 10 | with k outside: 3

Relaxed in place, the round for k = 0 followed 0 → 1, then 1 → 2, then 2 → 3 in one pass, each flight reading a cost the same round had just written: 150 for three flights, where the direct flight's 400 is the only trip with no stop. With k innermost, d[0][2] was finished at 10 before d[0][3] and d[1][2] were known, so the way 0 → 1 → 3 → 2, for 3, was never seen.

Check yourself

Quiz

1 / 6

Multiple choice

Dijkstra runs from 0 on the edges [0, 1, 4], [0, 2, 1], [2, 1, 2], [1, 3, 1] and [2, 3, 5]. In what order are the vertices settled?

Practice

The problems in this group, each mapped to the idea it trains:

  1. Network Delay Time: Dijkstra from one node; the answer is the largest distance.
  2. Dijkstra's Algorithm: every distance from one source, with a heap.
  3. Dijkstra: Queue vs Priority Queue: why the nearest first.
  4. Path With Minimum Effort: max where the sum was.
  5. Number of Ways to Arrive at Destination: a count beside each distance.
  6. Cheapest Flights Within K Stops: k + 1 rounds from a copy.
  7. Bellman-Ford Algorithm: n − 1 rounds, and one more for a negative cycle.
  8. Shortest Path in DAG: one pass in topological order.
  9. Floyd-Warshall Algorithm: every pair, k outermost.
  10. Find the City With the Smallest Number of Neighbors at a Threshold Distance: read the matrix.
  11. Evaluate Division: ratios as weights, multiplied along a path.