Skip to content

Instantly share code, notes, and snippets.

@n0phx
Last active December 10, 2015 18:38
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save n0phx/4476005 to your computer and use it in GitHub Desktop.
Save n0phx/4476005 to your computer and use it in GitHub Desktop.
edX MIT 6.00x Problem Set 10 solution / approach 2
# 6.00 Problem Set 10
# Graph optimization
#
# A set of data structures to represent graphs
#
class Node(object):
def __init__(self, name):
self.name = str(name)
def getName(self):
return self.name
def __str__(self):
return self.name
def __repr__(self):
return self.name
def __eq__(self, other):
return self.name == other.name
def __ne__(self, other):
return not self.__eq__(other)
class SmartNode(Node):
def __hash__(self):
return int(self.name)
class Edge(object):
def __init__(self, src, dest):
self.src = src
self.dest = dest
def getSource(self):
return self.src
def getDestination(self):
return self.dest
def __str__(self):
return '%s->%s' % (str(self.src), str(self.dest))
class WeightedEdge(Edge):
def __init__(self, src, dest, total_distance, distance_outdoors):
super(WeightedEdge, self).__init__(src, dest)
self.total_distance = total_distance
self.distance_outdoors = distance_outdoors
class Path(object):
def __init__(self, nodes_visited, total_distance, distance_outdoors):
self._nodes_visited = list(nodes_visited)
self.total_distance = total_distance
self.distance_outdoors = distance_outdoors
def is_node_visited(self, node):
return node in self._nodes_visited
def is_end_reached(self, end):
return self._nodes_visited[-1].getName() == end
def get_node_list(self):
return [str(node) for node in self._nodes_visited]
def get_current_position(self):
return self._nodes_visited[-1]
def add_edge(self, edge):
self.total_distance += edge.total_distance
self.distance_outdoors += edge.distance_outdoors
self._nodes_visited.append(edge.getDestination())
def clone(self):
return Path(self._nodes_visited,
self.total_distance,
self.distance_outdoors)
def __str__(self):
return ' => '.join(str(node) for node in self._nodes_visited)
class Digraph(object):
"""
A directed graph
"""
def __init__(self):
self.nodes = set([])
self.edges = {}
def addNode(self, node):
if node in self.nodes:
raise ValueError('Duplicate node')
else:
self.nodes.add(node)
self.edges[node] = []
def addEdge(self, edge):
src = edge.getSource()
dest = edge.getDestination()
if not(src in self.nodes and dest in self.nodes):
raise ValueError('Node not in graph')
self.edges[src].append(dest)
def childrenOf(self, node):
return self.edges[node]
def hasNode(self, node):
return node in self.nodes
def __str__(self):
res = ''
for k in self.edges:
for d in self.edges[str(k)]:
res = '%s%s->%s\n' % (res, str(k), str(d))
return res[:-1]
class SmartDigraph(Digraph):
def addEdge(self, edge):
src = edge.getSource()
dest = edge.getDestination()
if not(src in self.nodes and dest in self.nodes):
raise ValueError('Node not in graph')
self.edges[src].append(edge)
# 6.00 Problem Set 10
# Graph optimization
# Finding shortest paths through MIT buildings
#
# Name:
# Collaborators:
#
import time
from graph import SmartNode, WeightedEdge, Path, SmartDigraph
#
# Problem 2: Building up the Campus Map
#
# Write a couple of sentences describing how you will model the
# problem as a graph
#
def load_map(mapFilename):
"""
Parses the map file and constructs a directed graph
Parameters:
mapFilename : name of the map file
Assumes:
Each entry in the map file consists of the following four positive
integers, separated by a blank space:
From To TotalDistance DistanceOutdoors
e.g.
32 76 54 23
This entry would become an edge from 32 to 76.
Returns:
a directed graph representing the map
"""
print "Loading map from file..."
campus_graph = SmartDigraph()
with open(mapFilename, 'r') as map_file:
for line in map_file.readlines():
(start_loc,
end_loc,
total_distance,
distance_outdoors) = line.split()
start_node = SmartNode(start_loc)
end_node = SmartNode(end_loc)
if not campus_graph.hasNode(start_node):
campus_graph.addNode(start_node)
if not campus_graph.hasNode(end_node):
campus_graph.addNode(end_node)
edge = WeightedEdge(start_node,
end_node,
int(total_distance),
int(distance_outdoors))
campus_graph.addEdge(edge)
return campus_graph
#
# Problem 3: Finding the Shortest Path using Brute Force Search
#
# State the optimization problem as a function to minimize
# and the constraints
#
def timeit(func):
def _timeit(*args, **kwargs):
start = time.time()
try:
result = func(*args, **kwargs)
except ValueError:
raise
else:
return result
finally:
duration = time.time() - start
print '{0} ran for: {1:0.5f} secs.'.format(func.__name__, duration)
return _timeit
@timeit
def bruteForceSearch(digraph, start, end, maxTotalDist, maxDistOutdoors):
"""
Finds the shortest path from start to end using brute-force approach.
The total distance travelled on the path must not exceed maxTotalDist, and
the distance spent outdoor on this path must not exceed maxDisOutdoors.
Parameters:
digraph: instance of class Digraph or its subclass
start, end: start & end building numbers (strings)
maxTotalDist : maximum total distance on a path (integer)
maxDistOutdoors: maximum distance spent outdoors on a path (integer)
Assumes:
start and end are numbers for existing buildings in graph
Returns:
The shortest-path from start to end, represented by
a list of building numbers (in strings), [n_1, n_2, ..., n_k],
where there exists an edge from n_i to n_(i+1) in digraph,
for all 1 <= i < k.
If there exists no path that satisfies maxTotalDist and
maxDistOutdoors constraints, then raises a ValueError.
"""
stack = [Path([SmartNode(start)], 0, 0)]
valid_paths = []
while stack:
path = stack.pop(-1)
for edge in digraph.childrenOf(path.get_current_position()):
if not path.is_node_visited(edge.getDestination()):
new_path = path.clone()
new_path.add_edge(edge)
# "...consider first finding all valid paths that satisfy
# the max distance outdoors constraint,..."
if new_path.distance_outdoors <= maxDistOutdoors:
if new_path.is_end_reached(end):
valid_paths.append(new_path)
else:
stack.append(new_path)
# "... and then going through those paths and returning the shortest,
# rather than trying to fulfill both constraints at once..."
valid_paths = filter(lambda path: path.total_distance <= maxTotalDist,
valid_paths)
# min will raise a ValueError if an empty sequence is passed to it
shortest = min(valid_paths, key=lambda x: x.total_distance)
return shortest.get_node_list()
#
# Problem 4: Finding the Shorest Path using Optimized Search Method
#
@timeit
def directedDFS(digraph, start, end, maxTotalDist, maxDistOutdoors):
"""
Finds the shortest path from start to end using directed depth-first.
search approach. The total distance travelled on the path must not
exceed maxTotalDist, and the distance spent outdoor on this path must
not exceed maxDisOutdoors.
Parameters:
digraph: instance of class Digraph or its subclass
start, end: start & end building numbers (strings)
maxTotalDist : maximum total distance on a path (integer)
maxDistOutdoors: maximum distance spent outdoors on a path (integer)
Assumes:
start and end are numbers for existing buildings in graph
Returns:
The shortest-path from start to end, represented by
a list of building numbers (in strings), [n_1, n_2, ..., n_k],
where there exists an edge from n_i to n_(i+1) in digraph,
for all 1 <= i < k.
If there exists no path that satisfies maxTotalDist and
maxDistOutdoors constraints, then raises a ValueError.
"""
stack = [Path([SmartNode(start)], 0, 0)]
while stack:
path = stack.pop(-1)
for edge in digraph.childrenOf(path.get_current_position()):
if not path.is_node_visited(edge.getDestination()):
new_path = path.clone()
new_path.add_edge(edge)
if (new_path.distance_outdoors <= maxDistOutdoors and
new_path.total_distance <= maxTotalDist):
if new_path.is_end_reached(end):
return new_path.get_node_list()
else:
stack.append(new_path)
raise ValueError()
# Uncomment below when ready to test
if __name__ == '__main__':
# Test cases
digraph = load_map("mit_map.txt")
LARGE_DIST = 1000000
# Test case 1
print "---------------"
print "Test case 1:"
print "Find the shortest-path from Building 32 to 56"
expectedPath1 = ['32', '56']
brutePath1 = bruteForceSearch(digraph, '32', '56', LARGE_DIST, LARGE_DIST)
dfsPath1 = directedDFS(digraph, '32', '56', LARGE_DIST, LARGE_DIST)
print "Expected: ", expectedPath1
print "Brute-force: ", brutePath1
print "DFS: ", dfsPath1
# Test case 2
print "---------------"
print "Test case 2:"
print "Find the shortest-path from Building 32 to 56 without going out"
expectedPath2 = ['32', '36', '26', '16', '56']
brutePath2 = bruteForceSearch(digraph, '32', '56', LARGE_DIST, 0)
dfsPath2 = directedDFS(digraph, '32', '56', LARGE_DIST, 0)
print "Expected: ", expectedPath2
print "Brute-force: ", brutePath2
print "DFS: ", dfsPath2
# Test case 3
print "---------------"
print "Test case 3:"
print "Find the shortest-path from Building 2 to 9"
expectedPath3 = ['2', '3', '7', '9']
brutePath3 = bruteForceSearch(digraph, '2', '9', LARGE_DIST, LARGE_DIST)
dfsPath3 = directedDFS(digraph, '2', '9', LARGE_DIST, LARGE_DIST)
print "Expected: ", expectedPath3
print "Brute-force: ", brutePath3
print "DFS: ", dfsPath3
# Test case 4
print "---------------"
print "Test case 4:"
print "Find the shortest-path from Building 2 to 9 without going outdoors"
expectedPath4 = ['2', '4', '10', '13', '9']
brutePath4 = bruteForceSearch(digraph, '2', '9', LARGE_DIST, 0)
dfsPath4 = directedDFS(digraph, '2', '9', LARGE_DIST, 0)
print "Expected: ", expectedPath4
print "Brute-force: ", brutePath4
print "DFS: ", dfsPath4
# Test case 5
print "---------------"
print "Test case 5:"
print "Find the shortest-path from Building 1 to 32"
expectedPath5 = ['1', '4', '12', '32']
brutePath5 = bruteForceSearch(digraph, '1', '32', LARGE_DIST, LARGE_DIST)
dfsPath5 = directedDFS(digraph, '1', '32', LARGE_DIST, LARGE_DIST)
print "Expected: ", expectedPath5
print "Brute-force: ", brutePath5
print "DFS: ", dfsPath5
# Test case 6
print "---------------"
print "Test case 6:"
print "Find the shortest-path from Building 1 to 32 without going outdoors"
expectedPath6 = ['1', '3', '10', '4', '12', '24', '34', '36', '32']
brutePath6 = bruteForceSearch(digraph, '1', '32', LARGE_DIST, 0)
dfsPath6 = directedDFS(digraph, '1', '32', LARGE_DIST, 0)
print "Expected: ", expectedPath6
print "Brute-force: ", brutePath6
print "DFS: ", dfsPath6
# Test case 7
print "---------------"
print "Test case 7:"
print "Find the shortest-path from Building 8 to 50 without going outdoors"
bruteRaisedErr = 'No'
dfsRaisedErr = 'No'
try:
bruteForceSearch(digraph, '8', '50', LARGE_DIST, 0)
except ValueError:
bruteRaisedErr = 'Yes'
try:
directedDFS(digraph, '8', '50', LARGE_DIST, 0)
except ValueError:
dfsRaisedErr = 'Yes'
print "Expected: No such path! Should throw a value error."
print "Did brute force search raise an error?", bruteRaisedErr
print "Did DFS search raise an error?", dfsRaisedErr
# Test case 8
print "---------------"
print "Test case 8:"
print "Find the shortest-path from Building 10 to 32 without walking"
print "more than 100 meters in total"
bruteRaisedErr = 'No'
dfsRaisedErr = 'No'
try:
bruteForceSearch(digraph, '10', '32', 100, LARGE_DIST)
except ValueError:
bruteRaisedErr = 'Yes'
try:
directedDFS(digraph, '10', '32', 100, LARGE_DIST)
except ValueError:
dfsRaisedErr = 'Yes'
print "Expected: No such path! Should throw a value error."
print "Did brute force search raise an error?", bruteRaisedErr
print "Did DFS search raise an error?", dfsRaisedErr
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment