Skip to content

Add Detailed Solutions for Exercise 3.25 in the Search Exercises Section #918

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion markdown/3-Solving-Problems-By-Searching/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
| 22 | Unanswered | [`Question`](exercises/ex_22/question.md)|
| 23 | Unanswered | [`Question`](exercises/ex_23/question.md)|
| 24 | Unanswered | [`Question`](exercises/ex_24/question.md)|
| 25 | Unanswered | [`Question`](exercises/ex_25/question.md)|
| 25 | answered | [`Question`](exercises/ex_25/question.md)|
| 26 | Unanswered | [`Question`](exercises/ex_26/question.md)|
| 27 | Unanswered | [`Question`](exercises/ex_27/question.md)|
| 28 | Unanswered | [`Question`](exercises/ex_28/question.md)|
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
layout: exercise
title: Answers to Exercise 3.25
permalink: /search-exercises/ex_25/answers/
breadcrumb: 3-Solving-Problems-By-Searching
canonical_id: ch3ex25_ans
---

{% include mathjax_support %}

## Answers

### 1. Breadth-First Search as a Special Case of Uniform-Cost Search

Breadth-first search (BFS) can be seen as a special case of uniform-cost search (UCS) when all edge costs are equal. In UCS, the algorithm expands the least-cost node, but if all costs are the same, it effectively behaves like BFS, expanding nodes in the order they were added.

- **Proof**: In BFS, each level of the search tree is expanded one level at a time, where each level corresponds to nodes that are equally distant from the root node. In UCS, nodes are expanded based on cumulative cost, and if all edges have equal cost, the cumulative cost is directly proportional to the depth of the node. Hence, UCS will expand nodes in the same order as BFS when edge costs are uniform.

### 2. Depth-First Search as a Special Case of Best-First Tree Search

Depth-first search (DFS) can be interpreted as a special case of best-first tree search if the evaluation function used always prefers deeper nodes.

- **Proof**: In DFS, nodes are expanded by going as deep as possible down one branch before backtracking. This behavior can be mimicked in a best-first tree search by using an evaluation function that prioritizes nodes with greater depth (negative depth as priority). This ensures that the search always proceeds to the deepest unexplored node, effectively replicating the behavior of DFS.

### 3. Uniform-Cost Search as a Special Case of A* Search

Uniform-cost search (UCS) can be viewed as a special case of A* search where the heuristic function \( h(n) = 0 \) for all nodes \( n \).

- **Proof**: A* search algorithm expands nodes based on the evaluation function \( f(n) = g(n) + h(n) \), where \( g(n) \) is the cost from the start node to node \( n \), and \( h(n) \) is the heuristic estimate from \( n \) to the goal. When \( h(n) = 0 \), the evaluation function simplifies to \( f(n) = g(n) \), which is exactly the same as the cost criterion used in UCS. Thus, A* with a zero heuristic behaves identically to UCS.

These proofs demonstrate the relationship between the different search algorithms and show how specific conditions in one algorithm can replicate the behavior of another.
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
---
layout: exercise
title: Answers to Exercise 3.38
permalink: /search-exercises/ex_38/answers/
breadcrumb: 3-Solving-Problems-By-Searching
canonical_id: ch3ex38_ans
---

{% include mathjax_support %}

## Answers

### 1. Deriving the MST Heuristic from a Relaxed Version of the TSP

The MST heuristic for the TSP is derived from a relaxed version of the problem where instead of finding a tour (a path that visits all cities and returns to the start), we only find a minimum-spanning tree (MST) that connects all cities.

To derive the MST heuristic:
- **Relaxation**: Consider the problem where we need to connect all cities but not necessarily return to the starting city. This relaxed version is equivalent to finding an MST of the graph formed by the cities and their pairwise distances.
- **Heuristic Derivation**: The cost of the MST provides a lower bound on the cost of the optimal TSP tour. This is because any valid tour must span all the cities and hence must include at least the edges of the MST, which is the minimum sum of edges connecting all cities. Thus, the MST cost is used as an estimate or heuristic for the TSP.

### 2. Showing that the MST Heuristic Dominates Straight-Line Distance

To show that the MST heuristic dominates the straight-line distance heuristic:
- **Straight-Line Distance Heuristic**: This heuristic estimates the tour cost by assuming a straight-line path between cities. It is typically less accurate because it ignores the actual network of paths and only considers the direct Euclidean distance.
- **Comparison**: The MST heuristic dominates the straight-line distance heuristic because:
- The MST heuristic is guaranteed to be at least as large as the straight-line distance between cities (considering any optimal tour will involve distances that are at least as large as the MST).
- The MST heuristic provides a lower bound on the tour cost, while the straight-line distance might underestimate the cost significantly by ignoring the network of paths that would be used in a true tour.

### 3. Problem Generator for Random TSP Instances

To generate instances of the TSP where cities are represented by random points in the unit square:

```python
import numpy as np

def generate_random_tsp(num_cities):
"""
Generates a TSP instance with cities as random points in the unit square.

Parameters:
num_cities (int): Number of cities to generate.

Returns:
np.array: A matrix where element [i, j] represents the distance between city i and city j.
"""
# Generate random points in the unit square
points = np.random.rand(num_cities, 2)

# Compute the distance matrix
distances = np.sqrt(np.sum((points[:, np.newaxis] - points)**2, axis=2))

return distances
```

### 4. Efficient Algorithm for Constructing the MST

One efficient algorithm for constructing the MST is Kruskal's Algorithm. This algorithm can be used as follows:

1. Sort: Sort all edges of the graph in ascending order of their weights.
2. Union-Find: Use a union-find data structure to add edges to the MST, ensuring no cycles are formed.
3. Construct MST: Add edges to the MST in increasing order of their weight until the MST spans all vertices.

##### Implementation in Python:
```python
import numpy as np

def kruskal_mst(distances):
"""
Computes the MST of a graph using Kruskal's algorithm.

Parameters:
distances (np.array): The distance matrix representing the graph.

Returns:
float: Total weight of the MST.
"""
num_cities = len(distances)
edges = [(distances[i, j], i, j) for i in range(num_cities) for j in range(i + 1, num_cities)]
edges.sort()

parent = list(range(num_cities))
rank = [0] * num_cities

def find(u):
if parent[u] != u:
parent[u] = find(parent[u])
return parent[u]

def union(u, v):
root_u = find(u)
root_v = find(v)
if root_u != root_v:
if rank[root_u] > rank[root_v]:
parent[root_v] = root_u
elif rank[root_u] < rank[root_v]:
parent[root_u] = root_v
else:
parent[root_v] = root_u
rank[root_u] += 1

mst_weight = 0
for weight, u, v in edges:
if find(u) != find(v):
union(u, v)
mst_weight += weight

return mst_weight
```

## Conclusion

- The MST heuristic provides a useful approximation for the TSP by estimating the lower bound of the tour cost.
- It dominates the straight-line distance heuristic due to its consideration of network paths.
- The provided problem generator and MST algorithm can be used to generate TSP instances and solve them efficiently.
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
---
Name: Udit Kumar Nayak
Email: [email protected]
---
## Intro
1. The state of the problem are number of missionaries and cannibals on each side of the river
This is why i made a structure that takes these data into account. (not sure about the implementation
of the actions though, because i have to filter it two times)

2. Solve1 is a custom graph-search BFS, i found that the general implementation was present
at [this](https://github.com/aimacode/aima-python/blob/master/search.py) repo, but it was to late
so i just used solve2 to see a DFS.

3. Probably because people tend to be frustrated by hard problems and make the same error
over and over again

## Implementation
### Program solution
```python
# search is the module here
# https://github.com/aimacode/aima-python/blob/master/search.py
from search import Problem, Node, depth_first_graph_search
from queue import Queue

SHIP_SIZE = 2

class RiverSide():
"""
Structure representing the people on one side of the river
"""

def __init__(self, missionary=0, cannibals=0, tuple=None):
"""
tuple(missionaries, cannibals) on one riverside
"""
if tuple == None:
# security check
if missionary < 0 or cannibals < 0:
raise ValueError("Coudn't have negative missionaries or cannibals")

self.missionaries = missionary
self.cannibals = cannibals
else:
# security check
if tuple[0] < 0 or tuple[1] < 0:
raise ValueError("Coudn't have negative missionaries or cannibals")

self.missionaries = tuple[0]
self.cannibals = tuple[1]
self.shipSize = SHIP_SIZE

def isValid(self):
"""
Check if current state is valid
"""
# check if one or more missionaire is overwhelmed by a cannibal
if self.cannibals > self.missionaries and self.missionaries > 0:
return False

return True

def set(self, tuple):
self.missionaries = tuple[0]
self.cannibals = tuple[1]

def action(self):
"""
Returns possible ship transportations, by ship size.
"""
# Tuples of (missionary, cannibals)
actions = list()
for i in range(self.missionaries + 1):
for j in range(self.cannibals + 1):
if i + j != 0 and i + j <= self.shipSize:
actions.append((i,j))

return actions

def __eq__(self, other):
if self.missionaries == other.missionaries and self.cannibals == other.cannibals:
return True
else:
return False

def __sub__(self, other):
missionaries = self.missionaries - other.missionaries
cannibals = self.cannibals - other.cannibals
if missionaries < 0 or cannibals < 0:
raise ValueError("Coudn't have negative missionaries or cannibals")
return RiverSide(missionaries, cannibals)

def __add__(self, other):
missionaries = self.missionaries + other.missionaries
cannibals = self.cannibals + other.cannibals
return RiverSide(missionaries, cannibals)

def __str__(self):
return f"Riverside with {self.missionaries} missionaries and {self.cannibals} cannibals"

def __repr__(self):
return f"{self.missionaries} {self.cannibals}"

def __hash__(self):
return hash((self.missionaries, self.cannibals))

class Mc(Problem):
def __init__(self, initial):
self.state = (RiverSide(initial, 0), RiverSide(0, initial))
# i needed this to make solve2 work, compatibility stuff
self.initial = self.state
self.goalState = (RiverSide(0, initial), RiverSide(initial, 0))

def actions(self, state):
"""
The second member of the tuple is direction, 0 is from left to right, 1 is from right
to left, the first is the possible ship setup by missionaries and cannibals.
Data like this:
((missionaries, cannibals), 0)
"""
act = list()
for sideAction in state[0].action():
act.append(((sideAction), 0))

for sideAction in state[1].action():
act.append(((sideAction), 1))

return act

def result(self, state, action):
sideAction, code = action
sideAction = RiverSide(tuple=sideAction)
leftRiver, rightRiver = state

if code == 0:
state = (leftRiver - sideAction, rightRiver + sideAction)
else:
state = (leftRiver + sideAction, rightRiver - sideAction)

# check if these results are valid:
# print(state, f"and valid check is { state[0].isValid() and state[1].isValid()}")
if state[0].isValid() and state[1].isValid():
return state
else:
return (leftRiver, rightRiver)

def goal_test(self, state):
if state == self.goalState:
return True

return False

def solve(self):
"""
solving using BFS
"""
# initial parameters
frontier = Queue()
explored = set()

currentNode = Node(self.state)
frontier.put(currentNode)
explored.add(currentNode)

while True:
if frontier.empty():
print("no solution")
return False
currentNode = frontier.get()
if self.goal_test(currentNode.state):
print("found solution")
print(currentNode.solution())
return True
explored.add(currentNode)

for node in currentNode.expand(self):
if node not in explored:
frontier.put(node)

def solve2(self):
solNode = depth_first_graph_search(self)
if solNode != None:
print(solNode.solution())

def main():
# REGION TEST
# print("hello, these are some tests")
# print(RiverSide(0,0) == RiverSide(0,1))

# # test __init__ clas
# a = RiverSide(1,1)
# b = RiverSide(tuple=(1,2))

# # test string rapresentation
# print(a)
# print(b)

# # addition and subtraction
# print(a + RiverSide(2, 3))
# print(a - RiverSide(0, 1))
# ENDREGION

# testing problem and solving it
probbi = Mc(3)
probbi.solve2()

if __name__ == "__main__":
main()
```