0

Suppose we are given a graph with a predefined source node (S) and a target node (T). Each edge in the graph is associated with a pair of values (X, Y) where X denotes the distance and Y denotes the energy cost. See image for illustration: Example graph (Assume S = '1', T = '6')

The job is to find the shortest path between S and T such that the accumulated energy cost along the path does not exceed an energy budget. Specifically, we want to find the shortest path from '1' to '6' not exceeding an energy budget 17.

We can observe that although the path 1->2->4->6 has the shortest travel distance 3, its accumulated energy cost 18 exceeds the energy budget 17. Thus, this path is infeasible. In this example, the path 1->2->5->6 is the shortest feasible path.

Now, I've used a slightly modified uniform cost search (UCS) algorithm and was able to find the shortest distance and total energy cost but I'm still having trouble with printing the shortest path itself:

Shortest path: 1->2->4->5->6.  # Should be 1->2->5->6
Shortest distance: 5.
Total energy cost: 15.

Here's what I've tried:

import heapq

# Constants
START = '1'
END = '6'
BUDGET = 17
NO_PATH = (START, 0, 0)  # Output to print if no path

# Dictionaries for graph
G = {
    '1': ['2', '3'], 
    '2': ['4', '5'], 
    '3': ['2', '4', '5'], 
    '4': ['5', '6'], 
    '5': ['6'], 
    '6': []
}

Dist = {
    '1,2': 1, 
    '1,3': 10, 
    '2,4': 1, 
    '2,5': 2, 
    '3,2': 1, 
    '3,4': 5, 
    '3,5': 12, 
    '4,5': 10, 
    '4,6': 1, 
    '5,6': 2
}

Cost = {
    '1,2': 10, 
    '1,3': 3, 
    '2,4': 1, 
    '2,5': 3, 
    '3,2': 2, 
    '3,4': 7, 
    '3,5': 3, 
    '4,5': 1, 
    '4,6': 7, 
    '5,6': 2
}

def ucs(start, goal):
    """
    Uniform cost search with energy constraint.

    Return the shortest path, distance travelled and energy consumed.
    """
    # Initialization
    pq = [(0, 0, start)]            # Min-heap priority queue (dist, cost, node)
    predecessors = {start: None}    # Dict of predecessors {node: predecessor}
    distances = {start: 0}          # Dict of distance from start to node
    costs = {start: 0}              # Dict of cost from start to node

    while pq:
        # Dequeue
        dist, cost, node = heapq.heappop(pq)

        # Return solution when goal is reached
        if node == goal:
            path = [node]
            while node != start:
                node = predecessors[node]
                path.append(node)
            return path[::-1], dist, cost

        for neighbor in G[node]:
            # Calculate new distance and cost based on current node
            new_dist = dist + Dist[','.join([node, neighbor])]
            new_cost = cost + Cost[','.join([node, neighbor])]
            if new_cost > BUDGET:
                continue
            # Return infinity as value if key not in dict (to avoid KeyError)
            # so new distance and cost will always be lower for first time visited nodes
            if new_dist < distances.get(neighbor, float('inf')) or new_cost < costs.get(neighbor, float('inf')):
                # If new distance is shorter, update distances dict
                if new_dist < distances.get(neighbor, float('inf')):
                    distances[neighbor] = new_dist
                # If new cost is lower, update costs dict
                if new_cost < costs.get(neighbor, float('inf')):
                    costs[neighbor] = new_cost
                # Assign current node as predecessor
                predecessors[neighbor] = node
                # Enqueue
                entry = (new_dist, new_cost, neighbor)
                heapq.heappush(pq, entry)

if __name__ == '__main__':
    path, distance, cost = ucs(START, END) or NO_PATH
    print('Shortest path: {}.'.format('->'.join(path)))
    print('Shortest distance: {}.'.format(str(distance)))
    print('Total energy cost: {}.'.format(str(cost)))

It seems the problem is when I'm updating the predecessors dictionary in the for loop. Since I've allowed the nodes to be visited more than once in order to obtain the solution, the predecessor of a node will keep getting updated. And if I update the predecessor only in if new_dist < distances.get(neighbor, float('inf')):, the printed path will always be the shortest path by unmodified UCS (i.e., not even considering the energy budget constraint). It'll work in this example graph but will fail when using large graphs like the one from 9th DIMACS implementation challenge.

Is there any approach that I can take to correctly print out the shortest feasible path without modifying the original code too much in this problem?

pkl
  • 1
  • 2
  • Instead of setting the predecessor for each node, have you tried storing the current visited path for each node? – Abhinav Mathur Mar 05 '22 at 07:49
  • @AbhinavMathur I could try it but would there even be any difference when updating the predecessor of a node, though? Like in the example graph, I'm at Node 4 expanding to Node 6 but realized the energy cost exceeds the budget so expands to Node 5 instead. Node 5 then expands to Node 6 and goal is reached. But given my original code, the path for Node 5 would be [1,2,4] but the actual correct path is [1,2]. Now my question is how do I know which old predecessors to "go back" to when I realize the current shortest path is not feasible (i.e., exceeds budget) anymore? – pkl Mar 05 '22 at 08:39
  • Generally speaking, you can usually store the path in 2 ways. a) Store the predecessor for each node, and get path by starting from the end node and going backwards, or b) store the path from current node to end node. The second approach might be better imo, since you can update the path only when a valid solution is found – Abhinav Mathur Mar 05 '22 at 09:24
  • @AbhinavMathur Sorry, just to clarify: by method 'b', do you mean something like {'1': [2,5,6], '2': [5,6], '5': [6]} ? – pkl Mar 05 '22 at 09:35
  • Yes, since that would allow you to make updates after verifying the solution path – Abhinav Mathur Mar 05 '22 at 09:48
  • Thank you, I'll try out this approach! – pkl Mar 05 '22 at 09:52

0 Answers0