Skip to content
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

Add MST Heuristic for TSP: Problem Generator and Efficient Algorithm #910

Open
wants to merge 2 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
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()
```