feat: 2109 lab1

This commit is contained in:
Yadunand Prem 2024-02-03 12:41:27 +08:00
parent 113700eac3
commit 1b606271ef
No known key found for this signature in database
3 changed files with 576 additions and 0 deletions

View File

@ -0,0 +1,272 @@
# Task 1.1
# State representation: (Left_Missionaries, Left_Cannibals, Right_Missionaries, Right_Cannibals, Direction)
# This could be done with only Right_Missionaries and Right_Cannibals, and derive the Left_Missionaries and
# Left_Cannibals, but that would make the code slightly more difficult to write.
# possible_actions = [(2, 0), (1, 0), (1, 1), (0, 1), (0, 2)], where the left number in each tuple represents the
# number of missionaries on the boat and the right number represents the number of cannibals on the boat. The boat
# can only go in the corresponding direction looking at the previous state.
# Task 1.2
# Initial State: All the cannibals and missionaries are on the left side of the river.
# Goal State: All the cannibals and missionaries are on the right side of the river.
# Task 1.3
# The number of missionaries must always be greater than or equal to the number of cannibals on both sides of the river.
# The people on either side cannot be lesser than 0
# The boat can only carry 2 people at a time
# The boat can only go in the opposite direction of the previous state
# Task 1.4
# I used BFS to solve this problem. Using DFS, we might get a solution, but it might not be the most optimal, as it
# doesn't give the shortest path. Since this is effectively a graph with uniform weight, using BFS will give us a shortest path to the goal.
# Task 1.5
#
# Task 1.6
def mnc_tree_search(m, c):
'''
Solution should be the action taken from the root node (initial state) to
the leaf node (goal state) in the search tree.
Parameters
----------
m: no. of missionaries
c: no. of cannibals
Returns
----------
Returns the solution to the problem as a tuple of steps. Each step is a tuple of two numbers x and y, indicating the number of missionaries and cannibals on the boat respectively as the boat moves from one side of the river to another. If there is no solution, return False.
'''
from collections import deque
# the state holds ((L_missionary, L_cannibal), (R_missionary, R_cannibal), nextBoatDirection, current_path)
initial_state = ((m, c), (0, 0), 'right', ())
possible_actions = [(2, 0), (1, 0), (1, 1), (0, 1), (0, 2)] # total of 2 people on the boat
def is_valid_state(state) -> bool:
if state[0][0] < 0 or state[0][1] < 0 or state[1][0] < 0 or state[1][1] < 0:
return False
if state[0][0] == 0 or state[1][0] == 0:
return True
return state[0][0] >= state[0][1] and state[1][0] >= state[1][1]
def is_goal_state(state) -> bool:
return state[1][0] == m and state[1][1] == c
def transition(state, action):
if state[2] == 'right':
return ((state[0][0] - action[0], state[0][1] - action[1]),
(state[1][0] + action[0], state[1][1] + action[1]),
'left', state[3] + (action, ))
else:
return ((state[0][0] + action[0], state[0][1] + action[1]),
(state[1][0] - action[0], state[1][1] - action[1]),
'right', state[3] + (action, ))
frontier = deque()
frontier.append(initial_state)
while frontier:
state = frontier.popleft()
for action in possible_actions:
next_state = transition(state, action)
if not is_valid_state(next_state):
continue
if is_goal_state(next_state):
return next_state[3]
frontier.append(next_state)
return False
# Test cases for Task 1.6
def test_16():
expected = ((2, 0), (1, 0), (1, 1))
assert(mnc_tree_search(2,1) == expected)
expected = ((1, 1), (1, 0), (2, 0), (1, 0), (1, 1))
print(mnc_tree_search(2,2))
assert(mnc_tree_search(2,2) == expected)
expected = ((1, 1), (1, 0), (0, 2), (0, 1), (2, 0), (1, 1), (2, 0), (0, 1), (0, 2), (1, 0), (1, 1))
assert(mnc_tree_search(3,3) == expected)
# test_16()
# Task 1.7
def mnc_graph_search(m, c):
'''
Graph search requires to deal with the redundant path: cycle or loopy path.
Modify the above implemented tree search algorithm to accelerate your AI.
Parameters
----------
m: no. of missionaries
c: no. of cannibals
Returns
----------
Returns the solution to the problem as a tuple of steps. Each step is a tuple of two numbers x and y, indicating the number of missionaries and cannibals on the boat respectively as the boat moves from one side of the river to another. If there is no solution, return False.
'''
from collections import deque
# the state holds ((L_missionary, L_cannibal), (R_missionary, R_cannibal), nextBoatDirection, current_path)
initial_state = ((m, c), (0, 0), 'right', ())
possible_actions = [(2, 0), (1, 0), (1, 1), (0, 1), (0, 2)] # total of 2 people on the boat
def is_valid_state(state) -> bool:
if state[0][0] < 0 or state[0][1] < 0 or state[1][0] < 0 or state[1][1] < 0:
return False
if state[0][0] == 0 or state[1][0] == 0:
return True
return state[0][0] >= state[0][1] and state[1][0] >= state[1][1]
def is_goal_state(state) -> bool:
return state[1][0] == m and state[1][1] == c
def transition(state, action):
if state[2] == 'right':
return ((state[0][0] - action[0], state[0][1] - action[1]),
(state[1][0] + action[0], state[1][1] + action[1]),
'left', state[3] + (action, ))
else:
return ((state[0][0] + action[0], state[0][1] + action[1]),
(state[1][0] - action[0], state[1][1] - action[1]),
'right', state[3] + (action, ))
visited = set()
frontier = deque()
frontier.append(initial_state)
while frontier:
state = frontier.popleft()
if (state[0], state[1], state[2]) in visited:
continue
visited.add((state[0], state[1], state[2]))
for action in possible_actions:
next_state = transition(state, action)
if not is_valid_state(next_state):
continue
if is_goal_state(next_state):
return next_state[3]
frontier.append(next_state)
return False
# Test cases for Task 1.7
def test_17():
expected = ((2, 0), (1, 0), (1, 1))
assert(mnc_graph_search(2,1) == expected)
expected = ((1, 1), (1, 0), (2, 0), (1, 0), (1, 1))
assert(mnc_graph_search(2,2) == expected)
expected = ((1, 1), (1, 0), (0, 2), (0, 1), (2, 0), (1, 1), (2, 0), (0, 1), (0, 2), (1, 0), (1, 1))
assert(mnc_graph_search(3,3) == expected)
assert(mnc_graph_search(4, 4) == False)
# test_17()
# Task 1.8
# For the 4 Missionaries and 4 Cannibals case, the tree search does not terminate. Whereas the graph search does
# terminate and return if no valid solutions can be found. In this situation, the graph search is much faster than
# the tree search. The graph search should run a lot faster than the tree search also in normal situations where the
# tree search could make unnecessary visits to states it has been to before.
# 2.1 State representation for the Pitcher Problem
# State representation: (P1, P2, P3) where P1, P2, P3 are the amount of water in pitcher 1, 2, 3 respectively.
# Actions: Fill P_i, Empty P_i, P_i=>P_j where i, j are the pitcher numbers
# Possible actions: Fill P_i
# Empty P_i
# P_1=>P_2, P_1=>P_3, P_2=>P_1, P_2=>P_3, P_3=>P_1, P_3=>P_2
# 2.2 Initial and Goal State for the Pitcher Problem
# Initial State: (0, 0, 0)
# Goal State: (a, 0, 0) | (0, a, 0) | (0, 0, a) where a, b, c are the amount of water we want to measure
# Task 2.3
def pitcher_search(p1,p2,p3,a):
'''
Solution should be the action taken from the root node (initial state) to
the leaf node (goal state) in the search tree.
Parameters
----------
p1: capacity of pitcher 1
p2: capacity of pitcher 2
p3: capacity of pitcher 3
a: amount of water we want to measure
Returns
----------
Returns the solution to the problem as a tuple of steps. Each step is a string: "Fill Pi", "Empty Pi", "Pi=>Pj".
If there is no solution, return False.
'''
# Initial state is represented as ((P1, P2, P3), current_path)
capacities = (p1, p2, p3)
initial_state = ((0, 0, 0), ())
actions = (('FILL', 1), ('FILL', 2), ('FILL', 3),
('EMPTY', 1), ('EMPTY', 2), ('EMPTY', 3),
('POUR', 1, 2), ('POUR', 1, 3), ('POUR', 2, 1), ('POUR', 2, 3), ('POUR', 3, 1), ('POUR', 3, 2))
from collections import deque
queue = deque()
queue.append(initial_state)
visited = set()
while queue:
current_state, steps = queue.popleft()
# goal finding
for idx, state in enumerate(current_state):
if state == a:
return steps
if current_state in visited:
continue
print(current_state, steps)
visited.add(current_state)
for action in actions:
next_state = list(current_state)
if action[0] == 'POUR':
idx1 = action[1] - 1
idx2 = action[2] - 1
if current_state[idx1] == 0 or current_state[idx2] == capacities[idx2]:
continue
# If water in current pitcher is more than the space left in the other pitcher
if current_state[idx1] >= (capacities[idx2] - current_state[idx2]):
next_state[idx1] -= (capacities[idx2] - current_state[idx2])
next_state[idx2] = capacities[idx2]
else:
next_state[idx2] += next_state[idx1]
next_state[idx1] = 0
queue.append((tuple(next_state), steps + (f'P{action[1]}=>P{action[2]}', )))
if action[0] == 'FILL':
idx = action[1] - 1
if next_state[idx] == capacities[idx]:
continue
next_state[idx] = capacities[idx]
queue.append((tuple(next_state), steps + (f'Fill P{action[1]}', )))
continue
if action[0] == 'EMPTY':
idx = action[1] - 1
if next_state[idx] == 0:
continue
next_state[idx] = 0
queue.append((tuple(next_state), steps +(f'Empty P{action[1]}', ) ))
continue
return False
# Test cases for Task 2.3
def test_23():
expected = ('Fill P2', 'P2=>P1')
assert(pitcher_search(2,3,4,1) == expected)
expected = ('Fill P3', 'P3=>P1', 'Empty P1', 'P3=>P1')
assert(pitcher_search(1,4,9,7) == expected)
assert(pitcher_search(2,3,7,8) == False)
test_23()

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

304
cs2109s/labs/ps1/ps1.ipynb Normal file
View File

@ -0,0 +1,304 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Problem Set 1: Uninformed Search\n",
"\n",
"**Release Date:** 23 January 2024\n",
"\n",
"**Due Date:** 23:59, 3 Febuary 2024"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Overview\n",
"\n",
"In class, we discussed a number of different uninformed search algorithms. In this problem set, we get some hands-on practice by implementing them to solve some simple puzzles. In particular, we investigate the Missionaries and Cannibals problem and the Pitcher Filling problem.\n",
"\n",
"Required Files:\n",
"\n",
"* ps1.py\n",
"\n",
"**Honour Code**: Note that plagiarism will not be condoned! You may discuss with your classmates and check the internet for references, but you MUST NOT submit code/report that is copied directly from other sources!\n",
"\n",
"**IMPORTANT**: While it is possible to write and run Python code directly in Jupyter notebook, we recommend that you do this Problem Set with an IDE using the `.py` file provided. An IDE will make debugging significantly easier."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Missionaries and Cannibals\n",
"\n",
"The Missionaries and Cannibals (MnC) problem is a classic river-crossing logic puzzle. It is not difficult to solve this problem by hand when we have 3 Missionaries and 3 Cannibals. However, the solution becomes less obvious when we generalize the problem to *m* Missionaries and *c* Cannibals where *m* and *c* are some positive integers.\n",
"\n",
"The Missionaries and Cannibals problem is usually stated as follows: *m* missionaries and *c* cannibals are on the right side of a river, along with a boat that can hold at most 2 people. Your goal is to find a way to get everyone to the other side, without ever leaving a group of missionaries in one place outnumbered by the cannibals in that location (or the missionaries will get eaten!). You can try it here: https://javalab.org/en/boat_puzzle_en/ to test your understanding of the problem. \n",
"\n",
"Some important points to note when solving this question:\n",
"\n",
"* If the number of missionaries is zero, it does not matter how many cannibals there are even though technically they outnumber the number of missionaries. \n",
"* You need to have at least 1 person in the boat to row the boat from one side of the river to the other. \n",
"* At all times, both sides of the river AND the boat must have either 0 missionaries or at least as many missionaries as cannibals."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Task 1.1 - State Representation\n",
"\n",
"Propose a state representation for this problem if we want to formulate it as a search problem and define the corresponding actions.\n",
"\n",
"### Task 1.2 - Initial & Goal States\n",
"\n",
"What are the initial and goal states for the problem under your proposed representation?\n",
"\n",
"### Task 1.3 - Representation Invariant\n",
"\n",
"What is the invariant that your representation needs to satisfy? Explain how you will use this invariant when you implement search? \n",
"\n",
"### Task 1.4 - Which Search Algorithm Should We Pick?\n",
"\n",
"If we want to implement Tree Search to solve this problem, which **Tree Search** should we implement: BFS, DFS or Depth-Limited Search? Explain. You may make minor modifications to these algorithms if necessary to suit the problem.\n",
"\n",
"### Task 1.5 - Completeness and Optimality\n",
"\n",
"Derive a loose upper bound on the maximum number of valid states a game of $m$ missionaries and $c$ cannibals. Express your answer in terms of $m$ and $c$, for example, $(m+c)^{m+c}$.\n",
"\n",
"Using the above upper bound, how can you ensure that Tree Search **will always terminate**? In your proposed formulation, is your Tree Search (i) complete and/or (ii) optimal? Explain.\n",
"\n",
"### Task 1.6 - Implement Tree Search\n",
"\n",
"Implement the function `mnc_tree_search` with **Tree** Search using your proposed representation described in Tasks 1.1, 1.2 and 1.3. \n",
"\n",
"`mnc_tree_search` takes two integers $m$ and $c$, which are the numbers of missionaries and cannibals on one side of the river initially, and returns the solution to the problem as a tuple of steps. Each step is a tuple of two numbers $m$ and $c$, indicating the number of missionaries and cannibals on the boat respectively as the boat moves from one side of the river to another. The odd steps are for the boat moving from right to left and the even steps are for the boat moving from left to right, assuming that we are counting from step zero in the tuple. Note that $1 <= c + m <= 2$, i.e. you need to have at least one person in the boat to row the boat from one side of the river to the other.\n",
"\n",
"Note that your solution needs to be optimal.\n",
"\n",
"\n",
"#### Worked Example\n",
"\n",
"Note that there are a **range** of possible solutions. This example is just one of them. Let us consider the case of 2 missionaries and 1 cannibal. One possible solution is as follows:\n",
"- **Start:** 2 missionaries, 1 cannibal and the boat are on the LHS of the river\n",
"- **Step 1:** 2 missionaries row the boat to the RHS of the river\n",
" \n",
" 1 cannibal is on the LHS of the river. 2 missionaries and boat are on the RHS of the river\n",
" \n",
" **Note:** 1 cannibal and 0 missionaries on the LHS of the river is a valid state (0 missionaries is a valid state)\n",
"- **Step 2:** 1 missionary rows the boat to the LHS of the river\n",
" \n",
" **Note:** 1 cannibal and 1 missionary on the LHS of the river is a valid state (no. of missionaries >= no. of cannibals)\n",
"- **Step 3:** 1 cannibal and 1 missionary row the boat to the RHS of the river\n",
"- **End:** 2 missionaries and 1 cannibal are on the RHS of the river\n",
"\n",
"Hence, the solution is `((2, 0), (1, 0), (1, 1))`.\n",
"\n",
"Hint: You may find data structures helpful for your implementation."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def mnc_tree_search(m, c): \n",
" '''\n",
" Solution should be the action taken from the root node (initial state) to \n",
" the leaf node (goal state) in the search tree.\n",
"\n",
" Parameters\n",
" ---------- \n",
" m: no. of missionaries\n",
" c: no. of cannibals\n",
" \n",
" Returns\n",
" ---------- \n",
" Returns the solution to the problem as a tuple of steps. Each step is a tuple of two numbers x and y, indicating the number of missionaries and cannibals on the boat respectively as the boat moves from one side of the river to another. If there is no solution, return False.\n",
" '''\n",
" # TODO: add your solution here and remove `raise NotImplementedError`\n",
" raise NotImplementedError\n",
"\n",
"\n",
"# Test cases \n",
"print(mnc_tree_search(2,1)) # ((2, 0), (1, 0), (1, 1))\n",
"\n",
"print(mnc_tree_search(2,2)) # ((1, 1), (1, 0), (2, 0), (1, 0), (1, 1))\n",
"\n",
"print(mnc_tree_search(3,3)) # ((1, 1), (1, 0), (0, 2), (0, 1), (2, 0), (1, 1), (2, 0), (0, 1), (0, 2), (1, 0), (1, 1))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Task 1.7 - Implement Graph Search\n",
"\n",
"Next, implement the function `mnc_graph_search` with **Graph** Search using your proposed representation described in 1.1, 1.2 and 1.3. `mnc_graph_search` takes two integers $m$ and $c$, which are the numbers of missionaries and cannibals on one side of the river initially, and returns the to the problem as a tuple of steps following the format in Task 1.5."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def mnc_graph_search(m,c):\n",
" '''\n",
" Graph search requires to deal with the redundant path: cycle or loopy path.\n",
" Modify the above implemented tree search algorithm to accelerate your AI.\n",
"\n",
" Parameters\n",
" ---------- \n",
" m: no. of missionaries\n",
" c: no. of cannibals\n",
" \n",
" Returns\n",
" ---------- \n",
" Returns the solution to the problem as a tuple of steps. Each step is a tuple of two numbers x and y, indicating the number of missionaries and cannibals on the boat respectively as the boat moves from one side of the river to another. If there is no solution, return False.\n",
" '''\n",
" # TODO: add your solution here and remove `raise NotImplementedError`\n",
" raise NotImplementedError\n",
"\n",
"\n",
"# Test cases \n",
"print(mnc_graph_search(2,1)) # ((2, 0), (1, 0), (1, 1))\n",
"\n",
"print(mnc_graph_search(2,2)) # ((1, 1), (1, 0), (2, 0), (1, 0), (1, 1))\n",
"\n",
"print(mnc_graph_search(3,3)) # ((1, 1), (1, 0), (0, 2), (0, 1), (2, 0), (1, 1), (2, 0), (0, 1), (0, 2), (1, 0), (1, 1))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"### Task 1.8 - Tree vs Graph Search\n",
"\n",
"Evaluate the difference in performance between Tree Search and Graph Search by timing how long each piece of code takes to run for some configurations and report your results. \n",
"\n",
"Is Graph Search (i) complete and/or (ii) optimal in your proposed formulation? Explain."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Pitcher Filling Problem\n",
"\n",
"You currently have 3 pitchers $P_1$, $P_2$ and $P_3$ with capacities $c_1$, $c_2$ and $c_3$ respectively, such that $0 < c_1 < c_2 < c_3$ where each capacity is a positive integer. You also have access to a well (unlimited supply of water). Your job is to determine a sequence of steps to measure out an amount of water $a$ in any of the pitchers, where $0<a \\leq c_3, a \\in \\mathbb{Z}_{+}$. \n",
"\n",
"<p align=\"center\">\n",
"<img src=\"imgs/pitchers.jpg\">\n",
"</p>\n",
"\n",
"In each step, you can do one of three things:\n",
"* You can fill a pitcher $P_i$ to the brim; or\n",
"* You can empty a pitcher $P_i$; or\n",
"* You can pour water from pitcher $P_i$ to pitcher $P_j$.\n",
"\n",
"In the 3rd case, we will try to pour enough water from $P_i$ to $P_j$ to fill $P_j$\n",
"**but not more**. For example, if $P_j$ is already full, then nothing happens. If the total amount of water in $P_i$ and $P_j$ is less than or equal to $c_j$, then all the water gets poured into $P_j$ and $P_i$ is emptied.\n",
"\n",
"### Task 2.1 - State Representation for Pitcher Problem\n",
"\n",
"Propose a state representation for this problem if we want to formulate it as a search problem and define the corresponding actions.\n",
"\n",
"### Task 2.2 - Initial & Goal States\n",
"\n",
"What are the initial and goal states for the problem under your proposed representation in Task 2.1?\n",
"\n",
"### Task 2.3 - Implement Pitcher Search\n",
"\n",
"Implement the function `pitcher_search` that can will solve the problem **efficiently** (i.e. be mindful of your time and space complexity) using your representation in Tasks 2.1 and 2.2. `pitcher_search` takes in 4 integers integers $p_1$, $p_2$, $p_3$ and $a$, where $p_i$ are the capacities of the $i$-th pitcher and $a$ is the target amount of water, and return the answer as a tuple of steps. Each step is one of three strings:\n",
"1. \"Fill $P_i$\"\n",
"2. \"$P_i$ => $P_j$\"\n",
"3. \"Empty $P_i$\"\n",
"where $i$ is the label of the pitcher. If there is no solution, return `False`.\n",
"\n",
"Your solution here needs to be optimal as well.\n",
"\n",
"#### Worked Example\n",
"\n",
"Note that there are a **range** of possible solutions. This example is just one of them. Let us consider the case where $P_1 = 2$, $P_2 = 3$, $P_3 = 7$. Assume we want to measure $a = 6$. One possible solution is as follows:\n",
"- **Start:** All 3 pitchers are empty\n",
"- **Step 1:** Fill $P_2$\n",
"- Now $P_2$ has 3 litres and $P_1$, $P_3$ have 0 litres\n",
"- **Step 2:** Pour $P_2$ into $P_3$\n",
"- Now $P_3$ has 3 litres and $P_1$, $P_2$ have 0 litres\n",
"- **Step 3:** Fill $P_2$\n",
"- Now $P_2$ has 3 litres, $P_3$ has 3 litres, and $P_1$ has 0 litres\n",
"- **Step 4:** Pour $P_2$ into $P_3$\n",
"- Now $P_3$ has 6 litres and $P_1$, $P_2$ have 0 litres\n",
"\n",
"Hence, the solution is `('Fill P2', 'P2=>P3', 'Fill P2', 'P2=>P3')`."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def pitcher_search(p1, p2, p3, a):\n",
" '''\n",
" Solution should be the action taken from the root node (initial state) to \n",
" the leaf node (goal state) in the search tree.\n",
"\n",
" Parameters\n",
" ---------- \n",
" p1: capacity of pitcher 1\n",
" p2: capacity of pitcher 2\n",
" p3: capacity of pitcher 3\n",
" a: amount of water we want to measure\n",
" \n",
" Returns\n",
" ---------- \n",
" Returns the solution to the problem as a tuple of steps. Each step is a string: \"Fill Pi\", \"Empty Pi\", \"Pi=>Pj\". \n",
" If there is no solution, return False.\n",
" '''\n",
" # TODO: add your solution here and remove `raise NotImplementedError`\n",
" raise NotImplementedError\n",
"\n",
"\n",
"# Test cases\n",
"print(pitcher_search(2,3,4,1)) # ('Fill P2', 'P2=>P1')\n",
"\n",
"print(pitcher_search(1,4,9,7)) # ('Fill P3', 'P3=>P1', 'Empty P1', 'P3=>P1')\n",
"\n",
"print(pitcher_search(2,3,7,8)) # False"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3.10.11 64-bit (microsoft store)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.11"
},
"vscode": {
"interpreter": {
"hash": "5bd82fe7d6960a8d360fa7f139d752acec6855dc3eced6eff166183e4ea63ebb"
}
}
},
"nbformat": 4,
"nbformat_minor": 2
}