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?