feat: 2109s ps1

This commit is contained in:
Yadunand Prem 2024-02-08 00:40:04 +08:00
parent c46a9ee24e
commit 01ffa3b6aa
No known key found for this signature in database
6 changed files with 1686 additions and 0 deletions

295
cs2109s/labs/ps2/cube.py Normal file
View File

@ -0,0 +1,295 @@
"""
A Rubik's Cube like this
0 1 2
|-----|-----|-----|
0 | R | R | R |
|-----|-----|-----|
1 | G | G | G |
|-----|-----|-----|
2 | B | B | B |
|-----|-----|-----|
"""
import copy
import json
from ast import literal_eval
from typing import Dict, Iterable, List, Optional, Tuple, Union
Action = List[Union[int, str]]
class State:
r"""State class describes the setting of the Cube
Args:
shape (List[int]): describe the number of rows and columns of the cube
layout (Iterable[int]): describe the layout of the cube. The length of
layout should be equal to the product of shape.
Example:
State([2,3],[0, 1, 2, 3, 4, 5]) represents the state of
label: 0 1 2
0 | 0 | 1 | 2 |
1 | 3 | 4 | 5 |
Methods:
left(label): move the @label row left
returns the copy of new state (State)
right(label): move the @label row right
returns the copy of new state (State)
up(label): move the @label col up
returns the copy of new state (State)
down(label): move the @label col down
returns the copy of new state (State)
"""
def __init__(self, shape: Tuple[int,int], layout: Iterable[int]):
if len(layout) != shape[0]*shape[1]:
raise ValueError("layout does not match the shape")
self.__shape = list(shape)
self.__layout = layout
def __eq__(self, state: "State"):
if isinstance(state, State):
same_shape = state.shape[0] == self.__shape[0] and \
state.shape[1] == self.__shape[1]
same_layout = all([x==y for x,y in zip(self.__layout,state.layout)])
return same_shape and same_layout
else:
return False
def __hash__(self) -> int:
return hash(tuple(self.__layout))
def __repr__(self) -> str:
return str({'shape': self.__shape, 'layout': self.__layout})
def __str__(self):
# Header
row_str = f"{' '*5} "
for col in range(self.shape[1]):
row_str += f"{col:^5d} "
cube_str = row_str + '\n'
cube_str += f"{' '*5}+{'-----+'*self.shape[1]}\n"
# Content
for row in range(self.shape[0]):
row_str = f"{row:^5d}|"
for col in range(self.shape[1]):
row_str += f"{str(self.layout[row*self.shape[1]+col]):^5s}|"
cube_str += row_str + '\n'
cube_str += f"{' '*5}+{'-----+'*self.shape[1]}\n"
return cube_str
@property
def shape(self):
return copy.deepcopy(self.__shape)
@property
def layout(self):
return copy.deepcopy(self.__layout)
def left(self, label):
layout = self.layout
rows, cols = self.shape
head = layout[label * cols]
for i in range(cols-1):
layout[label * cols + i] = layout[label * cols + i + 1]
layout[(label+1) * cols - 1] = head
return State(self.shape,layout)
def right(self, label):
layout = self.layout
rows, cols = self.shape
tail = layout[(label + 1) * cols - 1]
for i in range(cols - 1, 0, -1):
layout[label * cols + i] = layout[label * cols + i - 1]
layout[label * cols] = tail
return State(self.shape,layout)
def up(self, label):
layout = self.layout
rows, cols = self.shape
head = layout[label]
for i in range(rows-1):
layout[label + cols * i] = layout[label + cols * (i + 1)]
layout[label + cols * (rows - 1)] = head
return State(self.shape,layout)
def down(self, label):
layout = self.layout
rows, cols = self.shape
tail = layout[label + cols * (rows - 1)]
for i in range(rows - 1, 0, -1):
layout[label + cols * i] = layout[label + cols * (i - 1)]
layout[label] = tail
return State(self.shape,layout)
class Cube:
r"""Cube problem class
Args:
input_file (Optional[str]): the absolute path of the Cube json file
initial (Optional[State]): the initial state of the Cube
goal (Union[State, Iterable[State]]): the goal
state(s) of the cube.
"""
def __init__(
self,
input_file: Optional[str] = None,
input_dict: Optional[Dict] = None,
initial: Optional[State] = None,
goal:Optional[State] = None
):
if input_file:
with open(input_file, 'r') as f:
data = json.load(f)
state_dict = literal_eval(data['initial'])
self.__initial = State(state_dict['shape'],state_dict['layout'])
state_dict = literal_eval(data['goal'])
self.__goal = State(state_dict['shape'],state_dict['layout'])
elif input_dict:
state_dict = input_dict['initial']
self.__initial = State(state_dict['shape'],state_dict['layout'])
state_dict = input_dict['goal']
self.__goal = State(state_dict['shape'],state_dict['layout'])
elif all([initial, goal]):
self.__initial = initial
self.__goal = goal
else:
raise ValueError
self.__actions = self._get_actions(*self.__initial.shape)
def __repr__(self) -> str:
return repr({'initial':repr(self.__initial), 'goal':repr(self.__goal)})
def __str__(self) -> str:
return f"initial:\n{str(self.__initial)}\ngoal:\n{str(self.__goal)}"
def _get_actions(self, rows:int, cols:int):
actions = []
for i in range(rows):
actions.append([i,"left"])
actions.append([i,"right"])
for i in range(cols):
actions.append([i,"up"])
actions.append([i,"down"])
return actions
# Observable Environment
@property
def initial(self):
return copy.deepcopy(self.__initial)
@property
def goal(self):
return copy.deepcopy(self.__goal)
def actions(self, state: State):
r"""Return the actions that can be executed in the given state.
Args:
state (State): the state to be checked for actions.
Returns:
A list of actions can be executed at the provided state.
"""
return copy.deepcopy(self.__actions)
# Transition Model (Deterministic)
def result(self, source: State, action):
r"""Return the state that results from executing the given
action in the given state. The action must be one of
self.actions(state).
Args:
source (State): the state to excute the action
action: the action can be executed
Returns:
the state after taking action from source
"""
assert list(action) in self.actions(source), \
f"{action} is illegal action at {source}"
label, act = action
if act == 'left':
result = source.left(label)
elif act == 'right':
result = source.right(label)
elif act == 'down':
result = source.down(label)
elif act == 'up':
result = source.up(label)
return result
def path_cost(self, c: float, state1: State, action,
state2: State) -> float:
r"""Return the cost of a solution path that arrives at state2 from
state1 via action, assuming cost c to get up to state1.
.. math::
c + action cost
Args:
c (float): the cost of getting state1 from the initial state
state1 (State): the State before executing action
action: the action taken at state1
state2 (State): the State after executing action
Returns:
the path cost of reaching state2
"""
if self.result(state1, action) == state2:
return c + 1
# Goal Test
def goal_test(self, state: State) -> bool:
r"""Return True if the state is a goal. The default method compares the
state to self.goal or checks for state in self.goal if it is a
list, as specified in the constructor.
Args:
state (State): the state to be checked
Return:
True if the give state is the goal state, otherwise False
"""
if isinstance(self.__goal, list):
return any(state == x for x in self.__goal)
else:
return state == self.__goal
# Solution Check
def verify_solution(self, solution, _print=False):
r"""Verify whether the given solution can reach goal state
Args:
solution (List): the list of actions is supposed to reach
goal state
Returns:
(True, cost) if the solution can reach the goal state,
(False, cost) if the solution fails to reach the goal state.
Notes:
cost == 0 means that there exists an illegal action in the solution
"""
curr = self.__initial
cost = 0
for action in solution:
if _print:
print(curr, action)
if list(action) not in self.actions(curr):
return False, 0
next = self.result(curr, action)
cost = self.path_cost(cost, curr, action, next)
curr = next
return self.goal_test(curr), cost

BIN
cs2109s/labs/ps2/graph.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

855
cs2109s/labs/ps2/ps2.ipynb Normal file

File diff suppressed because one or more lines are too long

442
cs2109s/labs/ps2/ps2.py Normal file
View File

@ -0,0 +1,442 @@
""" CS2109S Problem Set 2: Informed Search"""
import copy
import heapq
import math
import random
import time
from typing import List, Tuple
import cube
import utils
""" ADD HELPER FUNCTION HERE """
"""
We provide implementations for the Node and PriorityQueue classes in utils.py, but you can implement your own if you wish
"""
from utils import Node
from utils import PriorityQueue
#TODO Task 1.1: Implement your heuristic function, which takes in an instance of the Cube and
# the State class and returns the estimated cost of reaching the goal state from the state given.
def heuristic_func(problem: cube.Cube, curr_state: cube.State) -> float:
r"""
Computes the heuristic value of a state
Args:
problem (cube.Cube): the problem to compute
state (cube.State): the state to be evaluated
Returns:
h_n (float): the heuristic value
"""
h_n = 0.0
goals = problem.goal
""" YOUR CODE HERE """
for idx, val in enumerate(curr_state.layout):
if val != goals.layout[idx]:
h_n += 1
""" END YOUR CODE HERE """
h_n /= max(curr_state.shape)
return h_n
# Test
def wrap_test(func):
def inner(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
return f'FAILED, error: {type(e).__name__}, reason: {str(e)}'
return inner
# Test case for Task 1.1
@wrap_test
def test_heuristic(case):
input_dict = case['input_dict']
answer = case['answer']
problem = cube.Cube(input_dict = input_dict)
assert heuristic_func(problem, problem.goal) == 0, "Heuristic is not 0 at the goal state"
assert heuristic_func(problem, problem.initial) <= answer['cost'], "Heuristic is not admissible"
return "PASSED"
if __name__ == '__main__':
cube1 = {'input_dict': {"initial": {'shape': [3, 3], 'layout': ['N', 'U',
'S', 'N','U', 'S', 'N', 'U', 'S']}, 'goal': {'shape': [3, 3], 'layout':
['N', 'U', 'S', 'N', 'U', 'S', 'N', 'U', 'S']}}, 'answer': {'solution':
[], 'cost': 0}}
cube2 = {'input_dict': {"initial": {'shape': [3, 3], 'layout': ['S', 'O',
'C', 'S', 'O', 'C', 'S', 'O', 'C']}, 'goal': {'shape': [3, 3],
'layout': ['S', 'S', 'S', 'O', 'O', 'O', 'C', 'C', 'C']}}, 'answer':
{'solution': [[2, 'right'], [1, 'left'], [1, 'down'],
[2, 'up']], 'cost': 4}}
cube3 = {'input_dict': {"initial": {'shape': [3, 3], 'layout': ['N', 'U',
'S', 'N', 'U', 'S', 'N', 'U', 'S']}, 'goal': {'shape': [3, 3], 'layout':
['S', 'U', 'N', 'N', 'S', 'U', 'U', 'N', 'S']}}, 'answer': {'solution':
[[0, 'left'], [1, 'right'], [0, 'up'], [1, 'down']], 'cost': 4}}
cube4 = {'input_dict':{"initial": {'shape': [3, 4], 'layout': [1, 1, 9, 0,
2, 2, 0, 2, 9, 0, 1, 9]}, 'goal': {'shape': [3, 4], 'layout': [ 1, 0,
9, 2, 2, 1, 0, 9, 2, 1, 0, 9]}}, 'answer': {'solution': [[1, 'down'],
[3, 'up'], [2, 'left']], 'cost': 3}}
print('Task 1.1:')
print('cube1: ' + test_heuristic(cube1))
print('cube2: ' + test_heuristic(cube2))
print('cube3: ' + test_heuristic(cube3))
print('cube4: ' + test_heuristic(cube4))
print('\n')
#TODO Task 1.2: Implement A* search which takes in an instance of the Cube
# class and returns a list of actions [(2,'left'), ...] from the provided action set.
def astar_search(problem: cube.Cube):
r"""
A* Search finds the solution to reach the goal from the initial.
If no solution is found, return False.
Args:
problem (cube.Cube): Cube instance
Returns:
solution (List[Action]): the action sequence
"""
fail = True
solution = []
frontier = PriorityQueue()
visited = set()
""" YOUR CODE HERE """
initial_state = Node(None, (), problem.initial, 0, heuristic_func(problem, problem.initial))
frontier.push(initial_state.get_fn(), initial_state)
while frontier:
curr_node: Node = frontier.pop()
if problem.goal_test(curr_node.state):
fail = False
solution = list(curr_node.act)
break
if curr_node in visited:
continue
visited.add(curr_node)
for action in problem.actions(curr_node.state):
next_state = problem.result(curr_node.state, action)
next_node = Node(curr_node, curr_node.act + (action,), next_state, problem.path_cost(curr_node.g_n, curr_node.state, action, next_state), heuristic_func(problem, next_state))
frontier.push(next_node.get_fn(), next_node)
""" END YOUR CODE HERE """
if fail:
return False
return solution
@wrap_test
def test_astar(case):
input_dict = case['input_dict']
answer = case['answer']
problem = cube.Cube(input_dict = input_dict)
start = time.time()
solution = astar_search(problem)
print(f"Time lapsed: {time.time() - start}")
if solution is False:
assert answer['solution'] is False, "Solution is not False"
else:
correctness, cost = problem.verify_solution(solution, _print=False)
assert correctness, f"Fail to reach goal state with solution {solution}"
assert cost <= answer['cost'], f"Cost is not optimal."
return "PASSED"
if __name__ == '__main__':
cube1 = {'input_dict': {"initial": {'shape': [3, 3], 'layout': ['N', 'U',
'S', 'N','U', 'S', 'N', 'U', 'S']}, 'goal': {'shape': [3, 3], 'layout':
['N', 'U', 'S', 'N', 'U', 'S', 'N', 'U', 'S']}}, 'answer': {'solution':
[], 'cost': 0}}
cube2 = {'input_dict': {"initial": {'shape': [3, 3], 'layout': ['S', 'O',
'C', 'S', 'O', 'C', 'S', 'O', 'C']}, 'goal': {'shape': [3, 3],
'layout': ['S', 'S', 'S', 'O', 'O', 'O', 'C', 'C', 'C']}}, 'answer':
{'solution': [[2, 'right'], [1, 'left'], [1, 'down'],
[2, 'up']], 'cost': 4}}
cube3 = {'input_dict': {"initial": {'shape': [3, 3], 'layout': ['N', 'U',
'S', 'N', 'U', 'S', 'N', 'U', 'S']}, 'goal': {'shape': [3, 3], 'layout':
['S', 'U', 'N', 'N', 'S', 'U', 'U', 'N', 'S']}}, 'answer': {'solution':
[[0, 'left'], [1, 'right'], [0, 'up'], [1, 'down']], 'cost': 4}}
cube4 = {'input_dict':{"initial": {'shape': [3, 4], 'layout': [1, 1, 9, 0,
2, 2, 0, 2, 9, 0, 1, 9]}, 'goal': {'shape': [3, 4], 'layout': [ 1, 0,
9, 2, 2, 1, 0, 9, 2, 1, 0, 9]}}, 'answer': {'solution': [[1, 'down'],
[3, 'up'], [2, 'left']], 'cost': 3}}
print('Task 1.2:')
print('cube1: ' + test_astar(cube1))
print('cube2: ' + test_astar(cube2))
print('cube3: ' + test_astar(cube3))
print('cube4: ' + test_astar(cube4))
print('\n')
#TODO Task 1.3: Explain why the heuristic you designed for Task 1.1 is {consistent}
# and {admissible}.
#A heuristic is admissible if for every node, h(n) <= h*(n), where h*(n) is the true cost (in this case, the number of operations, left / right / up / down rotations on a column).
# In this case, my heuristic calculates the mismatches in position against the goal divided by the max(row, col). 
# For a 3x3 grid, assuming the worst case initial state, where everything is not in its correct position, where there is 9 mismatches. Then h(initial) = 9/3 = 3. And at this state, it would take a minimum of 3 moves to get to its original state. Thus, h(initial) <= h*(initial). At goal state, h(goal) = 0/3 = 0 <= h*(goal).
#TODO Task 2.1: Propose a state representation for this problem if we want to formulate it
# as a local search problem.
# State representation: List[int] The current route as a list of cities in the order of travel
#TODO Task 2.2: What are the initial and goal states under your proposed representation?
# Initial state: (List[int]) The current route as a list of cities in the order of travel
# Goal state: (List[int]) The shortest route, represented by a list of cities in the order to be traversed.
#TODO Task 2.3: Implement a reasonable transition function to generate new routes by applying
# minor "tweaks" to the current route. It should return a list of new routes to be used in
# the next iteration in the hill-climbing algorithm.
def transition(route: List[int]):
r"""
Generates new routes to be used in the next iteration in the hill-climbing algorithm.
Args:
route (List[int]): The current route as a list of cities in the order of travel
Returns:
new_routes (List[List[int]]): New routes to be considered
"""
new_routes = []
""" YOUR CODE HERE """
import copy
for i in range(len(route)):
for j in range(i+1, len(route)):
new_route = copy.deepcopy(route)
new_route[i], new_route[j] = new_route[j], new_route[i]
new_routes.append(new_route)
""" END YOUR CODE HERE """
return new_routes
# Test
@wrap_test
def test_transition(route: List[int]):
counter = 0
for new_route in transition(route):
counter += 1
assert sorted(new_route) == list(range(len(route))), "Invalid route"
print(f"Number of new routes: {counter}, with route length: {len(route)}")
return "PASSED"
if __name__ == '__main__':
print('Task 2.3:')
print(test_transition([1, 3, 2, 0]))
print(test_transition([7, 8, 6, 3, 5, 4, 9, 2, 0, 1]))
print('\n')
#TODO Task 2.4: Implement an evaluation function `evaluation_func(cities, distances, route)` that
# would be helpful in deciding on the "goodness" of a route, i.e. an optimal route should
# return a higher evaluation score than a suboptimal one.
def evaluation_func(cities: int, distances: List[Tuple[int]], route: List[int]) -> float:
r"""
Computes the evaluation score of a route
Args:
cities (int): The number of cities to be visited
distances (List[Tuple[int]]): The list of distances between every two cities
Each distance is represented as a tuple in the form of (c1, c2, d), where
c1 and c2 are the two cities and d is the distance between them.
The length of the list should be equal to cities * (cities - 1)/2.
route (List[int]): The current route as a list of cities in the order of travel
Returns:
h_n (float): the evaluation score
"""
h_n = 0.0
""" YOUR CODE HERE """
# distance from the first city to the last city
dist = {}
for d in distances:
dist[(d[0], d[1])] = d[2]
dist[(d[1], d[0])] = d[2]
for i in range(len(route)):
c1, c2 = route[i-1], route[i]
h_n += dist[(c1, c2)]
# last city to the first city
h_n = 1/h_n
""" END YOUR CODE HERE """
return h_n
if __name__ == '__main__':
cities = 4
distances = [(1, 0, 10), (0, 3, 22), (2, 1, 8), (2, 3, 30), (1, 3, 25), (0, 2, 15)]
route_1 = evaluation_func(cities, distances, [0, 1, 2, 3])
route_2 = evaluation_func(cities, distances, [2, 1, 3, 0])
route_3 = evaluation_func(cities, distances, [1, 3, 2, 0])
print(f"route_1: {route_1}, route_2: {route_2}, route_3: {route_3}")
print('Task 2.4:')
print(route_1 == route_2) # True
print(route_1 > route_3) # True
print('\n')
#TODO Task 2.5: Explain why your evaluation function is suitable for this problem.
# The evaluation function calculates the inverse of total distance travelled. Since we're trying to minimise the
# distance, we're trying to maximise the inverse of the distance. Thus, the evaluation function is suitable.
#TODO Task 2.6: Implement hill-climbing which takes in the number of cities and the list of
# distances, and returns the shortest route as a list of cities.
def hill_climbing(cities: int, distances: List[Tuple[int]]):
r"""
Hill climbing finds the solution to reach the goal from the initial.
Args:
cities (int): The number of cities to be visited
distances (List[Tuple[int]]): The list of distances between every two cities
Each distance is represented as a tuple in the form of (c1, c2, d), where
c1 and c2 are the two cities and d is the distance between them.
The length of the list should be equal to cities * (cities - 1)/2.
Returns:
route (List[int]): The shortest route, represented by a list of cities
in the order to be traversed.
"""
route = []
""" YOUR CODE HERE """
# I'm going to make an assumption that the graph is connected so that I don't have to find a path between every pair of cities.
# https://chat.openai.com/share/588631bb-6261-48cc-8490-e29401febad9 to explain hill climbing and hwo to randomize shuffle
initial_state = list(range(cities))
random.shuffle(initial_state)
curr_h_n = evaluation_func(cities, distances, initial_state)
while True:
states = transition(initial_state)
eval_states = [(evaluation_func(cities, distances, state), state) for state in states]
max_h_n, max_state = max(eval_states)
if curr_h_n >= max_h_n:
return initial_state
initial_state = max_state
curr_h_n = max_h_n
""" END YOUR CODE HERE """
return route
# Test
@wrap_test
def test_hill_climbing(cities: int, distances: List[Tuple[int]]):
start = time.time()
route = hill_climbing(cities, distances)
print(f"Time lapsed: {(time.time() - start)*1000}")
assert sorted(route) == list(range(cities)), "Invalid route"
return "PASSED"
if __name__ == '__main__':
cities_1 = 4
distances_1 = [(1, 0, 10), (0, 3, 22), (2, 1, 8), (2, 3, 30), (1, 3, 25), (0, 2, 15)]
cities_2 = 10
distances_2 = [(2, 7, 60), (1, 6, 20), (5, 4, 70), (9, 8, 90), (3, 7, 54), (2, 5, 61), (4, 1, 106), (0, 6, 51), (3, 1, 45), (0, 5, 86), (9, 2, 73), (8, 4, 14), (0, 1, 51), (9, 7, 22), (3, 2, 22), (8, 1, 120), (5, 7, 92), (5, 6, 60), (6, 2, 10), (8, 3, 78), (9, 6, 82), (0, 2, 41), (2, 8, 99), (7, 8, 71), (0, 9, 32), (4, 0, 73), (0, 3, 42), (9, 1, 80), (4, 2, 85), (5, 9, 113), (3, 6, 28), (5, 8, 81), (3, 9, 72), (9, 4, 81), (5, 3, 45), (7, 4, 60), (6, 8, 106), (0, 8, 85), (4, 6, 92), (7, 6, 70), (7, 0, 22), (7, 1, 73), (4, 3, 64), (5, 1, 80), (2, 1, 22)]
print('Task 2.6:')
print('cities_1: ' + test_hill_climbing(cities_1, distances_1))
print('cities_2: ' + test_hill_climbing(cities_2, distances_2))
print('\n')
#TODO Task 2.7: Implement hill_climbing_with_random_restarts by repeating hill climbing
# at different random locations.
def hill_climbing_with_random_restarts(cities: int, distances: List[Tuple[int]], repeats: int = 10):
r"""
Hill climbing with random restarts finds the solution to reach the goal from the initial.
Args:
cities (int): The number of cities to be visited
distances (List[Tuple[int]]): The list of distances between every two cities
Each distance is represented as a tuple in the form of (c1, c2, d), where
c1 and c2 are the two cities and d is the distance between them.
The length of the list should be equal to cities * (cities - 1)/2.
repeats (int): The number of times hill climbing to be repeated. The default
value is 10.
Returns:
route (List[int]): The shortest route, represented by a list of cities
in the order to be traversed.
"""
route = []
""" YOUR CODE HERE """
results = [hill_climbing(cities, distances) for _ in range(repeats)]
results = sorted(results, key=lambda x: evaluation_func(cities, distances, x))
route = results[-1]
""" END YOUR CODE HERE """
return route
# Test
@wrap_test
def test_random_restarts(cities: int, distances: List[Tuple[int]], repeats: int = 10):
start = time.time()
route = hill_climbing_with_random_restarts(cities, distances, repeats)
print(f"Time lapsed: {time.time() - start}")
assert sorted(route) == list(range(cities)), "Invalid route"
return "PASSED"
if __name__ == '__main__':
cities_1 = 4
distances_1 = [(1, 0, 10), (0, 3, 22), (2, 1, 8), (2, 3, 30), (1, 3, 25), (0, 2, 15)]
cities_2 = 10
distances_2 = [(2, 7, 60), (1, 6, 20), (5, 4, 70), (9, 8, 90), (3, 7, 54), (2, 5, 61),
(4, 1, 106), (0, 6, 51), (3, 1, 45), (0, 5, 86), (9, 2, 73), (8, 4, 14), (0, 1, 51),
(9, 7, 22), (3, 2, 22), (8, 1, 120), (5, 7, 92), (5, 6, 60), (6, 2, 10), (8, 3, 78),
(9, 6, 82), (0, 2, 41), (2, 8, 99), (7, 8, 71), (0, 9, 32), (4, 0, 73), (0, 3, 42),
(9, 1, 80), (4, 2, 85), (5, 9, 113), (3, 6, 28), (5, 8, 81), (3, 9, 72), (9, 4, 81),
(5, 3, 45), (7, 4, 60), (6, 8, 106), (0, 8, 85), (4, 6, 92), (7, 6, 70), (7, 0, 22),
(7, 1, 73), (4, 3, 64), (5, 1, 80), (2, 1, 22)]
print('Task 2.7:')
print('cities_1: ' + test_random_restarts(cities_1, distances_1))
print('cities_2: ' + test_random_restarts(cities_2, distances_2, 20))
#TODO Task 2.8: Compared to previous search algorithms you have seen (uninformed search, A*),
# why do you think local search is more suitable for this problem?

94
cs2109s/labs/ps2/utils.py Normal file
View File

@ -0,0 +1,94 @@
import heapq
class Node:
r"""Node class for search tree
Args:
parent (Node): the parent node of this node in the tree
act (Action): the action taken from parent to reach this node
state (State): the state of this node
g_n (float): the path cost of reaching this state
h_n (float): the heuristic value of this state
"""
def __init__(
self,
parent: "Node",
act,
state,
g_n: float = 0.0,
h_n: float = 0.0):
self.parent = parent # where am I from
self.act = act # how to get here
self.state = state # who am I
self.g_n = g_n # what it costs to be here g(n)
self.h_n = h_n # heuristic function h(n)
def get_fn(self):
r"""
Returns the sum of heuristic and cost of the node
"""
return self.g_n + self.h_n
def __str__(self):
return str(self.state)
def __lt__(self, node):
"""Compare the path cost between states"""
return self.g_n < node.g_n
def __eq__(self, node):
"""Compare whether two nodes have the same state"""
return isinstance(node, Node) and self.state == node.state
def __hash__(self):
"""Node can be used as a KeyValue"""
return hash(self.state)
class PriorityQueue:
def __init__(self):
self.heap = []
def __contains__(self, node):
"""Decide whether the node (state) is in the queue"""
return any([item == node for _, item in self.heap])
def __delitem__(self, node):
"""Delete the an existing node in the queue"""
try:
del self.heap[[item == node for _, item in self.heap].index(True)]
except ValueError:
raise KeyError(str(node) + " is not in the queue")
heapq.heapify(self.heap) # O(n)
def __getitem__(self, node):
"""Return the priority of the given node in the queue"""
for value, item in self.heap:
if item == node:
return value
raise KeyError(str(node) + " is not in the queue")
def __len__(self):
return len(self.heap)
def __repr__(self):
string = '['
for priority, node in self.heap:
string += f"({priority}, {node}), "
string += ']'
return string
def push(self, priority, node):
"""Enqueue node with priority"""
heapq.heappush(self.heap, (priority, node))
def pop(self):
"""Dequeue node with highest priority (the minimum one)"""
if self.heap:
return heapq.heappop(self.heap)[1]
else:
raise Exception("Empty priority queue")
def get_priority(self, node):
return self.__getitem__(node)

Binary file not shown.