Skip to content
ΣDSA Patterns
Menu
Language

Shortest Path (Weighted)

Guide 6 of 6 · Path 6 of 6

Interactive

Mental model

A worked animation for this problem. Scrub steps or press space to pause; re-tell the invariant out loud.

Step 1 of 6
0src1∞2dst ∞

dist[u][used] · discounts = 1

Min cost with discounts: 0—4—1—11—2, one coupon. A coupon halves one toll (floor). State is (city, couponsUsed).

Demo preview: These solutions pass the automated test suite but have not been human-reviewed. Treat trade-offs and prose as draft.

Minimum Cost to Reach City With Discounts

Problem (restated)

n cities 0…n-1 and bidirectional highways [c1, c2, toll]. You may apply at most discounts half-price coupons (floor of the toll, one highway each). Return the min cost from 0 to n-1, or -1 if unreachable.

Intuition

A coupon is an extra dimension on the node, not a different algorithm. Dijkstra on state (city, couponsUsed). From each state, take the next highway at full price (same coupon count) or at ⌊toll/2⌋ (one more coupon, if any remain). First pop of city n-1 is optimal.

Approaches

Dijkstra with discount state

Unverified
Time O((n+E)·d log (n d))Space O(n d)

Idea. dist[u][used]. Heap of (cost, u, used). Relax the unpaid edge into used, and the halved edge into used+1 when used < discounts. Skip stale pops. Return -1 if the sink is never popped.

Walkthrough. Cities 0-1-4 with tolls 4 and 11, one coupon: pay 4 then ⌊11/2⌋ = 5 → 9, cheaper than saving the coupon for a smaller edge.

Trade-offs. Same extra-state move as LC 787 (stops). Floor-halve, do not round. discounts = 0 is ordinary Dijkstra.

Solution
/**
 * Toy PQ: sort + shift. Interview/production: binary heap.
 * State is (cost, node, discountsUsed).
 */
export function minimumCost(n: number, highways: number[][], discounts: number): number {
  const adj: [number, number][][] = Array.from({ length: n }, () => []);
  for (const [a, b, c] of highways) {
    adj[a]!.push([b, c]);
    adj[b]!.push([a, c]);
  }
  const dist = Array.from({ length: n }, () => new Array<number>(discounts + 1).fill(Infinity));
  dist[0]![0] = 0;
  const pq: [number, number, number][] = [[0, 0, 0]];
  while (pq.length) {
    pq.sort((a, b) => a[0]! - b[0]!);
    const [d, u, used] = pq.shift()!;
    if (d > dist[u]![used]!) continue;
    if (u === n - 1) return d;
    for (const [v, w] of adj[u]!) {
      const nd = d + w;
      if (nd < dist[v]![used]!) {
        dist[v]![used] = nd;
        pq.push([nd, v, used]);
      }
      if (used < discounts) {
        const nd2 = d + Math.floor(w / 2);
        if (nd2 < dist[v]![used + 1]!) {
          dist[v]![used + 1] = nd2;
          pq.push([nd2, v, used + 1]);
        }
      }
    }
  }
  return -1;
}
/**
 * Toy PQ: sort + shift. Interview/production: binary heap.
 * State is (cost, node, discountsUsed).
 */
export function minimumCost(n: number, highways: number[][], discounts: number): number {
  const adj: [number, number][][] = Array.from({ length: n }, () => []);
  for (const [a, b, c] of highways) {
    adj[a]!.push([b, c]);
    adj[b]!.push([a, c]);
  }
  const dist = Array.from({ length: n }, () => new Array<number>(discounts + 1).fill(Infinity));
  dist[0]![0] = 0;
  const pq: [number, number, number][] = [[0, 0, 0]];
  while (pq.length) {
    pq.sort((a, b) => a[0]! - b[0]!);
    const [d, u, used] = pq.shift()!;
    if (d > dist[u]![used]!) continue;
    if (u === n - 1) return d;
    for (const [v, w] of adj[u]!) {
      const nd = d + w;
      if (nd < dist[v]![used]!) {
        dist[v]![used] = nd;
        pq.push([nd, v, used]);
      }
      if (used < discounts) {
        const nd2 = d + Math.floor(w / 2);
        if (nd2 < dist[v]![used + 1]!) {
          dist[v]![used + 1] = nd2;
          pq.push([nd2, v, used + 1]);
        }
      }
    }
  }
  return -1;
}

Template connection

Dijkstra + extra state. Coupons play the role k played in cheapest-flights. Catalog also lists 2093 under MST; the write-up belongs here because the algorithm is shortest path, not spanning tree.

Reflection