feat: 2109s ps1
This commit is contained in:
parent
c46a9ee24e
commit
01ffa3b6aa
295
cs2109s/labs/ps2/cube.py
Normal file
295
cs2109s/labs/ps2/cube.py
Normal 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
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
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
442
cs2109s/labs/ps2/ps2.py
Normal 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
94
cs2109s/labs/ps2/utils.py
Normal 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.
Loading…
Reference in New Issue
Block a user