Newer
Older
# ______________________________________________________________________________
# Graphs and Graph Problems
class Graph:
"""A graph connects nodes (vertices) by edges (links). Each edge can also
have a length associated with it. The constructor call is something like:
this makes a graph with 3 nodes, A, B, and C, with an edge of length 1 from
A to B, and an edge of length 2 from A to C. You can also do:
g = Graph({'A': {'B': 1, 'C': 2}, directed=False)
This makes an undirected graph, so inverse links are also added. The graph
stays undirected; if you add more links with g.connect('B', 'C', 3), then
inverse link is also added. You can use g.nodes() to get a list of nodes,
g.get('A') to get a dict of links out of A, and g.get('A', 'B') to get the
length of the link from A to B. 'Lengths' can actually be any object at
all, and nodes can be any hashable object."""
def __init__(self, graph_dict=None, directed=True):
self.graph_dict = graph_dict or {}
self.directed = directed
def make_undirected(self):
"""Make a digraph into an undirected graph by adding symmetric edges."""
for a in list(self.graph_dict.keys()):
for (b, dist) in self.graph_dict[a].items():
def connect(self, A, B, distance=1):
"""Add a link from A and B of given distance, and also add the inverse
link if the graph is undirected."""
self.connect1(A, B, distance)
if not self.directed:
self.connect1(B, A, distance)
def connect1(self, A, B, distance):
"""Add a link from A to B of given distance, in one direction only."""
self.graph_dict.setdefault(A, {})[B] = distance
def get(self, a, b=None):
"""Return a link distance or a dict of {node: distance} entries.
.get(a,b) returns the distance or None;
.get(a) returns a dict of {node: distance} entries, possibly {}."""
links = self.graph_dict.setdefault(a, {})
if b is None:
return links
else:
return links.get(b)
def nodes(self):
s1 = set([k for k in self.graph_dict.keys()])
s2 = set([k2 for v in self.graph_dict.values() for k2, v2 in v.items()])
nodes = s1.union(s2)
return list(nodes)
def UndirectedGraph(graph_dict=None):
"""Build a Graph where every edge (including future ones) goes both ways."""
Donato Meoli
a validé
return Graph(graph_dict=graph_dict, directed=False)
def RandomGraph(nodes=list(range(10)), min_links=2, width=400, height=300,
"""Construct a random graph, with the specified nodes, and random links.
The nodes are laid out randomly on a (width x height) rectangle.
Then each node is connected to the min_links nearest neighbors.
Because inverse links are added, some nodes will have more connections.
The distance between nodes is the hypotenuse times curvature(),
where curvature() defaults to a random number between 1.1 and 1.5."""
g = UndirectedGraph()
g.locations = {}
for node in nodes:
g.locations[node] = (random.randrange(width), random.randrange(height))
# Build roads from each city to at least min_links nearest neighbors.
for i in range(min_links):
for node in nodes:
if len(g.get(node)) < min_links:
here = g.locations[node]
def distance_to_node(n):
Donato Meoli
a validé
return np.inf
return distance(g.locations[n], here)
Donato Meoli
a validé
neighbor = min(nodes, key=distance_to_node)
d = distance(g.locations[neighbor], here) * curvature()
return g
Surya Teja Cheedella
a validé
""" [Figure 3.2]
Simplified road map of Romania
"""
romania_map = UndirectedGraph(dict(
Arad=dict(Zerind=75, Sibiu=140, Timisoara=118),
Bucharest=dict(Urziceni=85, Pitesti=101, Giurgiu=90, Fagaras=211),
Craiova=dict(Drobeta=120, Rimnicu=146, Pitesti=138),
Drobeta=dict(Mehadia=75),
Eforie=dict(Hirsova=86),
Fagaras=dict(Sibiu=99),
Hirsova=dict(Urziceni=98),
Iasi=dict(Vaslui=92, Neamt=87),
Lugoj=dict(Timisoara=111, Mehadia=70),
Oradea=dict(Zerind=71, Sibiu=151),
Pitesti=dict(Rimnicu=97),
Rimnicu=dict(Sibiu=80),
Urziceni=dict(Vaslui=142)))
Arad=(91, 492), Bucharest=(400, 327), Craiova=(253, 288),
Drobeta=(165, 299), Eforie=(562, 293), Fagaras=(305, 449),
Giurgiu=(375, 270), Hirsova=(534, 350), Iasi=(473, 506),
Lugoj=(165, 379), Mehadia=(168, 339), Neamt=(406, 537),
Oradea=(131, 571), Pitesti=(320, 368), Rimnicu=(233, 410),
Sibiu=(207, 457), Timisoara=(94, 410), Urziceni=(456, 350),
Vaslui=(509, 444), Zerind=(108, 531))
Surya Teja Cheedella
a validé
""" [Figure 4.9]
* "State of the left room" "State of the right room" "Room in which the agent
is present"
1 - DDL Dirty Dirty Left
2 - DDR Dirty Dirty Right
3 - DCL Dirty Clean Left
4 - DCR Dirty Clean Right
5 - CDL Clean Dirty Left
6 - CDR Clean Dirty Right
7 - CCL Clean Clean Left
8 - CCR Clean Clean Right
State_1=dict(Suck=['State_7', 'State_5'], Right=['State_2']),
State_2=dict(Suck=['State_8', 'State_4'], Left=['State_2']),
State_3=dict(Suck=['State_7'], Right=['State_4']),
State_4=dict(Suck=['State_4', 'State_2'], Left=['State_3']),
State_5=dict(Suck=['State_5', 'State_1'], Right=['State_6']),
State_6=dict(Suck=['State_8'], Left=['State_5']),
State_7=dict(Suck=['State_7', 'State_3'], Right=['State_8']),
State_8=dict(Suck=['State_8', 'State_6'], Left=['State_7'])
Donato Meoli
a validé
))
Surya Teja Cheedella
a validé
""" [Figure 4.23]
One-dimensional state space Graph
"""
one_dim_state_space = Graph(dict(
State_1=dict(Right='State_2'),
State_2=dict(Right='State_3', Left='State_1'),
State_3=dict(Right='State_4', Left='State_2'),
State_4=dict(Right='State_5', Left='State_3'),
State_5=dict(Right='State_6', Left='State_4'),
State_6=dict(Left='State_5')
Donato Meoli
a validé
))
one_dim_state_space.least_costs = dict(
State_1=8,
State_2=9,
State_3=2,
State_4=2,
State_5=4,
State_6=3)
Surya Teja Cheedella
a validé
""" [Figure 6.1]
Principal states and territories of Australia
"""
T=dict(),
SA=dict(WA=1, NT=1, Q=1, NSW=1, V=1),
NT=dict(WA=1, Q=1),
NSW=dict(Q=1, V=1)))
australia_map.locations = dict(WA=(120, 24), NT=(135, 20), SA=(135, 30),
Q=(145, 20), NSW=(145, 32), T=(145, 42),
V=(145, 37))
class GraphProblem(Problem):
"""The problem of searching a graph from one node to another."""
def __init__(self, initial, goal, graph):
super().__init__(initial, goal)
self.graph = graph
"""The actions at a graph node are just its neighbors."""
"""The result of going to a neighbor is just that neighbor."""
def path_cost(self, cost_so_far, A, action, B):
Donato Meoli
a validé
return cost_so_far + (self.graph.get(A, B) or np.inf)
def find_min_edge(self):
"""Find minimum value of edges."""
Donato Meoli
a validé
m = np.inf
for d in self.graph.graph_dict.values():
local_min = min(d.values())
m = min(m, local_min)
return m
def h(self, node):
"""h function is straight-line distance from a node's state to goal."""
locs = getattr(self.graph, 'locations', None)
if locs:
if type(node) is str:
return int(distance(locs[node], locs[self.goal]))
return int(distance(locs[node.state], locs[self.goal]))
else:
Donato Meoli
a validé
return np.inf
A version of GraphProblem where an action can lead to
nondeterministic output i.e. multiple possible states.
Define the graph as dict(A = dict(Action = [[<Result 1>, <Result 2>, ...], <cost>], ...), ...)
A the dictionary format is different, make sure the graph is created as a directed graph.
def result(self, state, action):
return self.graph.get(state, action)
# ______________________________________________________________________________
class NQueensProblem(Problem):
"""The problem of placing N queens on an NxN board with none attacking
each other. A state is represented as an N-element array, where
a value of r in the c-th entry means there is a queen at column c,
row r, and a value of -1 means that the c-th column has not been
filled in yet. We fill in columns left to right.
>>> depth_first_tree_search(NQueensProblem(8))
def __init__(self, N):
super().__init__(tuple([-1] * N))
self.N = N
"""In the leftmost empty column, try all non-conflicting rows."""
if not self.conflicted(state, row, col)]
"""Place the next queen at the given row."""
col = state.index(-1)
new = list(state[:])
def conflicted(self, state, row, col):
"""Would placing a queen at (row, col) conflict with anything?"""
withal
a validé
return any(self.conflict(row, col, state[c], c)
for c in range(col))
def conflict(self, row1, col1, row2, col2):
"""Would putting two queens in (row1, col1) and (row2, col2) conflict?"""
return (row1 == row2 or # same row
col1 == col2 or # same column
row1 - col1 == row2 - col2 or # same \ diagonal
Donato Meoli
a validé
row1 + col1 == row2 + col2) # same / diagonal
def goal_test(self, state):
"""Check if all columns filled, no conflicts."""
return False
withal
a validé
return not any(self.conflicted(state, state[col], col)
for col in range(len(state)))
def h(self, node):
"""Return number of conflicting queens for a given node"""
num_conflicts = 0
for (r1, c1) in enumerate(node.state):
for (r2, c2) in enumerate(node.state):
if (r1, c1) != (r2, c2):
num_conflicts += self.conflict(r1, c1, r2, c2)
return num_conflicts
Donato Meoli
a validé
# ______________________________________________________________________________
# Inverse Boggle: Search for a high-scoring Boggle board. A good domain for
# iterative-repair and related search techniques, as suggested by Justin Boyan.
ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
cubes16 = ['FORIXB', 'MOQABJ', 'GURILW', 'SETUPL',
'CMPDAE', 'ACITAO', 'SLCRAE', 'ROMASH',
'NODESW', 'HEFIYE', 'ONUDTK', 'TEVIGN',
'ANEDVZ', 'PINESH', 'ABILYT', 'GKYLEU']
def random_boggle(n=4):
"""Return a random Boggle board of size n x n.
We represent a board as a linear list of letters."""
cubes = [cubes16[i % 16] for i in range(n * n)]
random.shuffle(cubes)
Donato Meoli
a validé
# The best 5x5 board found by Boyan, with our word list this board scores
# 2274 words, for a score of 9837
boyan_best = list('RSTCSDEIAEGNLRPEATESMSSID')
def print_boggle(board):
for i in range(n2):
if i % n == 0 and i > 0:
print()
if board[i] == 'Q':
print('Qu', end=' ')
else:
print(str(board[i]) + ' ', end=' ')
"""Return a list of lists, where the i-th element is the list of indexes
for the neighbors of square i."""
if cache.get(n2):
return cache.get(n2)
n = exact_sqrt(n2)
neighbors = [None] * n2
for i in range(n2):
neighbors[i] = []
on_top = i < n
on_bottom = i >= n2 - n
on_left = i % n == 0
Donato Meoli
a validé
on_right = (i + 1) % n == 0
if not on_top:
neighbors[i].append(i - n)
if not on_left:
neighbors[i].append(i - n - 1)
if not on_right:
neighbors[i].append(i - n + 1)
if not on_bottom:
if not on_left:
neighbors[i].append(i + n - 1)
if not on_right:
neighbors[i].append(i + n + 1)
if not on_left:
neighbors[i].append(i - 1)
if not on_right:
neighbors[i].append(i + 1)
cache[n2] = neighbors
return neighbors
def exact_sqrt(n2):
"""If n2 is a perfect square, return its square root, else raise error."""
Donato Meoli
a validé
n = int(np.sqrt(n2))
assert n * n == n2
return n
Donato Meoli
a validé
# _____________________________________________________________________________
class Wordlist:
"""This class holds a list of words. You can use (word in wordlist)
to check if a word is in the list, or wordlist.lookup(prefix)
to see if prefix starts any of the words in the list."""
def __init__(self, file, min_len=3):
lines = file.read().upper().split()
self.words = [word for word in lines if len(word) >= min_len]
self.words.sort()
self.bounds = {}
for c in ALPHABET:
c2 = chr(ord(c) + 1)
self.bounds[c] = (bisect.bisect(self.words, c),
bisect.bisect(self.words, c2))
def lookup(self, prefix, lo=0, hi=None):
"""See if prefix is in dictionary, as a full word or as a prefix.
Return two values: the first is the lowest i such that
words[i].startswith(prefix), or is None; the second is
True iff prefix itself is in the Wordlist."""
words = self.words
i = bisect.bisect_left(words, prefix, lo, hi)
if i < len(words) and words[i].startswith(prefix):
return i, (words[i] == prefix)
return None, False
return self.lookup(word)[1]
return len(self.words)
Donato Meoli
a validé
# _____________________________________________________________________________
class BoggleFinder:
"""A class that allows you to find all the words in a Boggle board."""
wordlist = None # A class variable, holding a wordlist
def __init__(self, board=None):
if BoggleFinder.wordlist is None:
BoggleFinder.wordlist = Wordlist(open_data("EN-text/wordlist.txt"))
self.found = {}
if board:
self.set_board(board)
def set_board(self, board=None):
"""Set the board, and find all the words in it."""
if board is None:
board = random_boggle()
self.board = board
self.neighbors = boggle_neighbors(len(board))
self.found = {}
for i in range(len(board)):
lo, hi = self.wordlist.bounds[board[i]]
self.find(lo, hi, i, [], '')
return self
def find(self, lo, hi, i, visited, prefix):
"""Looking in square i, find the words that continue the prefix,
considering the entries in self.wordlist.words[lo:hi], and not
revisiting the squares in visited."""
return
wordpos, is_word = self.wordlist.lookup(prefix, lo, hi)
if wordpos is not None:
self.found[prefix] = True
visited.append(i)
c = self.board[i]
prefix += c
self.find(wordpos, hi, j, visited, prefix)
visited.pop()
scores = [0, 0, 0, 0, 1, 2, 3, 5] + [11] * 100
def score(self):
"""The total score for the words found, according to the rules."""
return sum([self.scores[len(w)] for w in self.words()])
def __len__(self):
return len(self.found)
Donato Meoli
a validé
# _____________________________________________________________________________
def boggle_hill_climbing(board=None, ntimes=100, verbose=True):
"""Solve inverse Boggle by hill-climbing: find a high-scoring board by
starting with a random one and changing it."""
finder = BoggleFinder()
if board is None:
board = random_boggle()
best = len(finder.set_board(board))
for _ in range(ntimes):
i, oldc = mutate_boggle(board)
new = len(finder.set_board(board))
if new > best:
best = new
print_boggle(board)
return board, best
def mutate_boggle(board):
i = random.randrange(len(board))
oldc = board[i]
# random.choice(boyan_best)
board[i] = random.choice(random.choice(cubes16))
return i, oldc
Donato Meoli
a validé
# ______________________________________________________________________________
# Code to compare searchers on various problems.
class InstrumentedProblem(Problem):
"""Delegates to a problem, and keeps statistics."""
self.problem = problem
self.succs = self.goal_tests = self.states = 0
self.found = None
def actions(self, state):
self.succs += 1
return self.problem.actions(state)
def result(self, state, action):
self.states += 1
return self.problem.result(state, action)
def goal_test(self, state):
self.goal_tests += 1
result = self.problem.goal_test(state)
self.found = state
return result
def path_cost(self, c, state1, action, state2):
return self.problem.path_cost(c, state1, action, state2)
def value(self, state):
return self.problem.value(state)
def __getattr__(self, attr):
def __repr__(self):
return '<{:4d}/{:4d}/{:4d}/{}>'.format(self.succs, self.goal_tests,
def compare_searchers(problems, header,
searchers=[breadth_first_tree_search,
breadth_first_graph_search,
iterative_deepening_search,
depth_limited_search,
recursive_best_first_search]):
def do(searcher, problem):
p = InstrumentedProblem(problem)
searcher(p)
return p
Donato Meoli
a validé
table = [[name(s)] + [do(s, p) for p in problems] for s in searchers]
print_table(table, header)
def compare_graph_searchers():
compare_searchers(problems=[GraphProblem('Arad', 'Bucharest', romania_map),
GraphProblem('Oradea', 'Neamt', romania_map),
GraphProblem('Q', 'WA', australia_map)],
header=['Searcher', 'romania_map(Arad, Bucharest)',
'romania_map(Oradea, Neamt)', 'australia_map'])