Maximum Path Quality of a Graph

HARD

Description

There is an undirected graph with n nodes numbered from 0 to n - 1 (inclusive). You are given a 0-indexed integer array values where values[i] is the value of the ith node. You are also given a 0-indexed 2D integer array edges, where each edges[j] = [uj, vj, timej] indicates that there is an undirected edge between the nodes uj and vj, and it takes timej seconds to travel between the two nodes. Finally, you are given an integer maxTime.

A valid path in the graph is any path that starts at node 0, ends at node 0, and takes at most maxTime seconds to complete. You may visit the same node multiple times. The quality of a valid path is the sum of the values of the unique nodes visited in the path (each node's value is added at most once to the sum).

Return the maximum quality of a valid path.

Note: There are at most four edges connected to each node.

 

Example 1:

Input: values = [0,32,10,43], edges = [[0,1,10],[1,2,15],[0,3,10]], maxTime = 49
Output: 75
Explanation:
One possible path is 0 -> 1 -> 0 -> 3 -> 0. The total time taken is 10 + 10 + 10 + 10 = 40 <= 49.
The nodes visited are 0, 1, and 3, giving a maximal path quality of 0 + 32 + 43 = 75.

Example 2:

Input: values = [5,10,15,20], edges = [[0,1,10],[1,2,10],[0,3,10]], maxTime = 30
Output: 25
Explanation:
One possible path is 0 -> 3 -> 0. The total time taken is 10 + 10 = 20 <= 30.
The nodes visited are 0 and 3, giving a maximal path quality of 5 + 20 = 25.

Example 3:

Input: values = [1,2,3,4], edges = [[0,1,10],[1,2,11],[2,3,12],[1,3,13]], maxTime = 50
Output: 7
Explanation:
One possible path is 0 -> 1 -> 3 -> 1 -> 0. The total time taken is 10 + 13 + 13 + 10 = 46 <= 50.
The nodes visited are 0, 1, and 3, giving a maximal path quality of 1 + 2 + 4 = 7.

 

Constraints:

  • n == values.length
  • 1 <= n <= 1000
  • 0 <= values[i] <= 108
  • 0 <= edges.length <= 2000
  • edges[j].length == 3
  • 0 <= uj < vj <= n - 1
  • 10 <= timej, maxTime <= 100
  • All the pairs [uj, vj] are unique.
  • There are at most four edges connected to each node.
  • The graph may not be connected.

Approaches

Checkout 2 different approaches to solve Maximum Path Quality of a Graph. Click on different approaches to view the approach and algorithm in detail.

Optimized DFS with Pruning via Dijkstra's Algorithm

This approach significantly improves upon the brute-force DFS by incorporating two key optimizations. First, it maintains the current path's quality incrementally, avoiding costly O(N) recalculations. Second, and more importantly, it uses a powerful pruning strategy. By pre-calculating the shortest time from every node back to node 0 using Dijkstra's algorithm, it can intelligently discard paths that have no chance of returning to the origin within the maxTime limit. This drastically reduces the search space.

Algorithm

  • Build an adjacency list adj for the graph.
  • Use Dijkstra's algorithm starting from node 0 to precompute minTimeToZero[i], the shortest time from every node i to node 0.
  • Initialize a global max_quality = values[0] (for the path just staying at 0).
  • Initialize a frequency array visited_counts of size n, with visited_counts[0] = 1.
  • Define a recursive function dfs(u, time_left, visited_counts, current_quality).
  • In dfs(u, time_left, ...):
    • For each neighbor v of u with edge time t:
      • If time_left >= t:
        • Pruning Step: Check if time_left - t >= minTimeToZero[v]. If not, skip this path.
        • If the check passes, update visited_counts and new_quality.
        • If v == 0, update max_quality = max(max_quality, new_quality).
        • Make a recursive call: dfs(v, time_left - t, visited_counts, new_quality).
        • Backtrack by decrementing visited_counts[v].
  • Start the search with dfs(0, maxTime, visited_counts, values[0]).
  • Return max_quality.

The core idea is to avoid exploring futile paths. A path is futile if, from the current node, there isn't enough time left to return to node 0, even by taking the quickest possible route.

1. Preprocessing with Dijkstra's Algorithm: We first compute the shortest time from every node in the graph to node 0. Since the graph is undirected and times are non-negative, we can run Dijkstra's algorithm starting from node 0. The result is an array, minTimeToZero, where minTimeToZero[i] stores this minimum time for node i.

2. Optimized DFS with Pruning: We then perform a DFS, but with a much smarter recursive step. The state of our DFS is (u, timeLeft, visitedCounts, currentQuality).

When at a node u and considering a move to a neighbor v that takes time seconds:

  • We first check if we have enough time for this single step: timeLeft >= time.
  • Pruning: We then apply our key optimization. The time remaining after moving to v would be timeLeft - time. To complete a valid path, we must be able to get from v back to 0. The fastest this can be done is minTimeToZero[v]. Therefore, we must satisfy timeLeft - time >= minTimeToZero[v]. If this condition fails, we prune this entire branch of the search, as no path extending from v can be valid.

If the pruning check passes, we proceed with the recursive call, updating the currentQuality in O(1) time. Whenever a path reaches node 0, we treat it as a valid completed path and update our global maxQuality.

class Solution {
    int maxQuality = 0;
    List<int[]>[] adj;
    int[] values;
    int[] minTimeToZero;

    public int maximalPathQuality(int[] values, int[][] edges, int maxTime) {
        this.values = values;
        int n = values.length;
        adj = new ArrayList[n];
        for (int i = 0; i < n; i++) {
            adj[i] = new ArrayList<>();
        }
        for (int[] edge : edges) {
            adj[edge[0]].add(new int[]{edge[1], edge[2]});
            adj[edge[1]].add(new int[]{edge[0], edge[2]});
        }

        minTimeToZero = new int[n];
        Arrays.fill(minTimeToZero, Integer.MAX_VALUE);
        minTimeToZero[0] = 0;
        PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a[1]));
        pq.offer(new int[]{0, 0}); // {node, time}

        while (!pq.isEmpty()) {
            int[] curr = pq.poll();
            int u = curr[0];
            int time = curr[1];

            if (time > minTimeToZero[u]) {
                continue;
            }

            for (int[] neighbor : adj[u]) {
                int v = neighbor[0];
                int edgeTime = neighbor[1];
                if (minTimeToZero[u] + edgeTime < minTimeToZero[v]) {
                    minTimeToZero[v] = minTimeToZero[u] + edgeTime;
                    pq.offer(new int[]{v, minTimeToZero[v]});
                }
            }
        }

        int[] visitedCounts = new int[n];
        visitedCounts[0] = 1;
        this.maxQuality = values[0];
        dfs(0, maxTime, visitedCounts, values[0]);
        return this.maxQuality;
    }

    private void dfs(int u, int timeLeft, int[] visitedCounts, int currentQuality) {
        for (int[] neighbor : adj[u]) {
            int v = neighbor[0];
            int time = neighbor[1];

            if (timeLeft >= time) {
                int newTimeLeft = timeLeft - time;
                
                if (newTimeLeft >= minTimeToZero[v]) {
                    boolean isNewlyVisited = (visitedCounts[v] == 0);
                    visitedCounts[v]++;
                    
                    int newQuality = currentQuality + (isNewlyVisited ? values[v] : 0);
                    
                    if (v == 0) {
                        this.maxQuality = Math.max(this.maxQuality, newQuality);
                    }
                    
                    dfs(v, newTimeLeft, visitedCounts, newQuality);
                    
                    visitedCounts[v]--; // Backtrack
                }
            }
        }
    }
}

Complexity Analysis

Time Complexity: O(E log N + B'^D'), where `O(E log N)` is the time for Dijkstra's algorithm. The DFS part is hard to analyze precisely, but the pruning drastically reduces the effective branching factor `B'` and depth `D'` compared to the brute-force approach, making it fast enough for the given constraints.Space Complexity: O(N + E), dominated by the storage for the adjacency list, the `minTimeToZero` array, and data structures for Dijkstra's algorithm. The recursion depth adds a factor but is bounded by the time limit.

Pros and Cons

Pros:
  • Significantly more efficient than the brute-force approach due to intelligent pruning.
  • The O(1) update for path quality avoids the expensive O(N) recalculation.
  • Effectively solves the problem within the given constraints.
Cons:
  • More complex to implement due to the need for Dijkstra's algorithm as a preprocessing step.
  • The overhead of Dijkstra's algorithm might be noticeable for graphs where the search space is naturally small.

Code Solutions

Checking out 3 solutions in different languages for Maximum Path Quality of a Graph. Click on different languages to view the code.

class Solution {
private
  List<int[]>[] g;
private
  boolean[] vis;
private
  int[] values;
private
  int maxTime;
private
  int ans;
public
  int maximalPathQuality(int[] values, int[][] edges, int maxTime) {
    int n = values.length;
    g = new List[n];
    Arrays.setAll(g, k->new ArrayList<>());
    for (var e : edges) {
      int u = e[0], v = e[1], t = e[2];
      g[u].add(new int[]{v, t});
      g[v].add(new int[]{u, t});
    }
    vis = new boolean[n];
    vis[0] = true;
    this.values = values;
    this.maxTime = maxTime;
    dfs(0, 0, values[0]);
    return ans;
  }
private
  void dfs(int u, int cost, int value) {
    if (u == 0) {
      ans = Math.max(ans, value);
    }
    for (var e : g[u]) {
      int v = e[0], t = e[1];
      if (cost + t <= maxTime) {
        if (vis[v]) {
          dfs(v, cost + t, value);
        } else {
          vis[v] = true;
          dfs(v, cost + t, value + values[v]);
          vis[v] = false;
        }
      }
    }
  }
}

Video Solution

Watch the video walkthrough for Maximum Path Quality of a Graph



Patterns:

Backtracking

Data Structures:

ArrayGraph

Subscribe to Scale Engineer newsletter

Learn about System Design, Software Engineering, and interview experiences every week.

No spam, unsubscribe at any time.