Phase2 Report1 Updated

You might also like

Download as pdf or txt
Download as pdf or txt
You are on page 1of 17

Kuwait University

College Engineering and Petroleum


Computer Engineering Department

CpE <No.>: <Course Name>


Semester: <Fall, spring, summer>
Section No. <No.>

Homework <No.>

Student Name: <Name>


Student Id: <Id No.>
Instructor Name: <Name>
TA Name: <Name>

Date: <Date>

©Computer Engineering Department, Kuwait University, 2023


Introduction
Graphs have emerged as one of the quintessential structures in the realm of computer science and
mathematics, providing a compact way to represent relational data. Among the vast
classifications of graphs, we focus on connected and weighted graphs, especially in the context
of the TSP.

Connected Graph: A graph is termed connected if there's a path between every pair of vertices,
ensuring no vertex remains isolated. In real-world scenarios, this represents interconnectivity, be
it in a network of computers, cities, or social relations. The primary importance of such graphs
lies in ensuring accessibility between nodes, making it a critical factor in numerous algorithms,
including our TSP variant.

Weighted Graph: Unlike simple graphs where edges only represent connections, in a weighted
graph, edges bear specific weights. These weights can be equated to distances, cost, time, or any
quantitative measure defining the relationship between two vertices. In the domain of problems
like routing, shortest path, and particularly TSP, weights signify the cost or distance between
nodes, making the graph a precise representation of real-world problems where decisions are
based on quantitative measures.

Traveling Salesman Problem (TSP): One of the most studied optimization problems in
computational theory, TSP presents a scenario where a salesman is tasked with finding the
shortest route that lets him visit each city precisely once and then return to his starting point. The
essence of this problem is to minimize the total distance (or cost) traveled. As simple as it
sounds, the TSP is notorious for its complexity, belonging to the class of NP-hard problems. This
means that as the number of cities increases, the problem's solution space grows exponentially,
making it computationally challenging to find the exact optimal solution within a reasonable
time frame.

In this report, we examine a modified version of the TSP. The crux of our modification lies in the
stipulation that each city must be visited at least once, but it's permissible to forgo some streets
or connections and finishing at the city he starts from. This nuance introduces an intriguing layer
of complexity, as the backward path may not be the same as the forward path.

Algorithm and Time Complexity:

©Computer Engineering Department, Kuwait University, 2023


Our approach to the modified TSP involves using a nearest-neighbor heuristic complemented by
a backtracking strategy to traverse all cities.

Algorithm:

first_vertex:

1. Start by marking the first city as visited.


2. Find the closest neighboring city from the start city and mark it as visited.
rest_of_vertices:

1. For the current city, check its unvisited neighboring cities.


2. If a city has no unvisited neighbors and there are other unvisited cities remaining,
backtrack to a previously visited city.
3. Compare the minimal distance among the unvisited neighbors to the distance from the
previous city to any unvisited city. The salesman either moves to the closest unvisited
city or backtracks based on this comparison.
4. before moving to a new city, calculate the shorter path to return to the start city and save
the backward path data.
5. Continue this until all cities are visited at least once.
6. When all cities are visited at least once:
i. Add the backward path and cost from the last vertex to the start city to the
forward path and cost.
plot_graph:

• Plot the graph


Main:

1. Calls the first_vertex function to begin the traversal and keeps processing the remaining
cities until all are visited.
2. Print the order of visited cities and total cost of the traveled path.
3. Tracks and prints the algorithm execution time.
4. Calls plot_graph function to plot the Graph
In summary, this is a heuristic-based algorithm to tackle a variant of the Traveling Salesman
Problem (TSP) where every city needs to be visited at least once without necessarily traversing

©Computer Engineering Department, Kuwait University, 2023


every street and finishing at the city he starts from. The algorithm is driven by the closest
neighboring city heuristic and integrates a custom backtracking mechanism.

The time complexity of different parts of the algorithm:

first_vertex Function:

• Extracting the weights and finding the minimum: O(n)


• Updating various lists and dictionaries: O(1)
rest_of_vertices Function:

• Finding connected vertices and new connected vertices(unvisited): O(n).


• Finding the minimum weight among new connected vertices: O(n).
• Searching for the parent vertex in the visited_vertices list: O(n).
• In the worst-case, backtracking might require checking parent vertex multiple times,
adding another: O(n).
• Updating the backward dictionary can, in the worst case, take: O(n).
Summing these up, each iteration of rest_of_vertices function can be O(n). In the worst case, the
function may have to process each vertex in the graph, leading to O(n^2) for the entirety of the
function's operations.

Main Function:

• The while loop in the main function will run until all cities are visited. In the worst case,
this can be O(n) iterations. In each iteration, it invokes the rest_of_vertices function,
which has O(n^2) complexity. Therefore, the loop in the main function has a worst-case
time complexity of O(n^3).
So, the overall worst-case time complexity of the algorithm is O(n^3), where n is the number of
vertices (cities) in the graph.

Design of Data Structure and Algorithm of Each Function:

Data Structures:
1. adjacency_list: A dictionary where key is a vertex and value is a list of dictionaries with
'to_vertex' and 'weight' representing the connected vertices and their weights.
2. vertices: A list containing the keys of adjacency_list.

©Computer Engineering Department, Kuwait University, 2023


3. num_vertices: An integer representing the number of vertices.
4. new_vertices: A list of vertices yet to be visited.
5. visited_vertices: A list of already visited vertices.
6. cost: A list of costs associated with visiting each vertex.
7. process_indicator: An integer to indicate if the process is completed.
8. start_vertex: The starting vertex for the salesman.
9. last_vertex: The last visited vertex.
10. backwoard: A dictionary to keep track of the backtracking details for each vertex.
Function first_vertex():
Pre: Graph Global Data
Post: Initializes the starting vertex and visits the closest connected vertex
1. Add start_vertex to visited_vertices.
2. Decrement num_vertices.
3. Remove start_vertex from new_vertices.
4. For each connected vertex of start_vertex, add its weight to weights list.
5. Find the minimum weight from weights list as min_weight.
6. Determine the next_vertex based on min_weight.
7. Add min_weight to cost.
8. Add next_vertex to visited_vertices.
9. Decrement num_vertices.
10. Remove next_vertex from new_vertices.
11. Update backwoard dictionary for next_vertex.
12. Return next_vertex, parent_vertex, and parent_cost.
End Function
Function rest_of_vertices(next_vertex, parent_vertex, parent_cost):
Pre: The next vertex to visit and its parent vertex and cost and Graph Global Data
Post: Determines the next vertex to visit based on the current state
1. Get the connected vertices for next_vertex.
2. Filter out visited vertices from connected vertices.
3. If there are no new connected vertices and num_vertices is not zero, backtrack to
parent_vertex.

©Computer Engineering Department, Kuwait University, 2023


4. For each connected vertex of next_vertex that is in new_connected_vertices, add its
weight to weights list.
5. Find the minimum weight from weights list as min_weight.
6. If min_weight is greater than parent_cost, check for alternative paths from parent_vertex.
7. If the alternative path is shorter, backtrack to parent_vertex.
8. Otherwise, determine the next vertex based on min_weight.
9. Update backwoard dictionary for the new vertex.
10. If no vertices are left to visit, update the last_vertex and add the backtracking details to
visited_vertices and cost.
11. Set process_indicator to 1.
12. Return next_vertex, parent_vertex, and parent_cost.
End Function
Function plot_graph():
Pre: Graph Global Data
Post: Plots the graph based on the adjacency_list.

1. Initialize an empty Graph, G.


2. For each vertex in adjacency_list:
a. For i from 0 to length(adjacency_list[vertex]) - 1:
i. Set node as vertex.
ii. Set neighbor as adjacency_list[vertex][i]['to_vertex'].
iii. Set weight as adjacency_list[vertex][i]['weight'].
iv. Add an edge to G between node and neighbor with the specified weight.
b. End For.
3. End For.
4. Generate a random layout for G and set it as pos.
5. Draw G on the screen using pos, with specified node and edge properties.
6. Extract edge attributes of G with the 'weight' key and set it as edge_labels.
7. Display edge labels on G using pos and edge_labels with specified font properties.
8. Show the graph plot on the screen.
End Function
Function main():

©Computer Engineering Department, Kuwait University, 2023


Pre: Graph Global Data
Post: Executes the algorithm to find the minimum cost tour
1. Start a timer.
2. Initialize next_vertex, parent_vertex, and parent_cost using the first_vertex function.
3. While process_indicator is not 1:
a. Update next_vertex, parent_vertex, and parent_cost using the rest_of_vertices
function.
4. Print the shortest path and its cost.
5. Stop the timer and print the total execution time.
6. Plot the Graph using plot_graph() Function.
End Function

Source Code:

import networkx as nx
import matplotlib.pyplot as plt
import time
import random

class Vertex:
def __init__(self, key):
self.key = key
self.arc = None

class Arc:
def __init__(self, destination, weight):
self.destination = destination
self.weight = weight
self.nextArc = None

class Graph:
def __init__(self):
self.vertices = []

def find_vertex(self, key):


for vertex in self.vertices:
if vertex.key == key:
return vertex
return None

def arc_exists(self, fromKey, toKey):


fromPtr = self.find_vertex(fromKey)
arc = fromPtr.arc
while arc:
if arc.destination.key == toKey:
return True
arc = arc.nextArc

©Computer Engineering Department, Kuwait University, 2023


return False

def insert_vertex(self, key):


if not self.find_vertex(key):
self.vertices.append(Vertex(key))

def insert_arc(self, fromKey, toKey, weight):


if self.arc_exists(fromKey, toKey):
return

fromPtr = self.find_vertex(fromKey)
toPtr = self.find_vertex(toKey)
if not fromPtr or not toPtr:
print("Vertex not found!")
return

# Insert arc in both directions for undirected graph


newArc1 = Arc(toPtr, weight)
if not fromPtr.arc:
fromPtr.arc = newArc1
else:
arc = fromPtr.arc
while arc.nextArc:
arc = arc.nextArc
arc.nextArc = newArc1

newArc2 = Arc(fromPtr, weight)


if not toPtr.arc:
toPtr.arc = newArc2
else:
arc = toPtr.arc
while arc.nextArc:
arc = arc.nextArc
arc.nextArc = newArc2

def print_graph(self):
adjancy_list = {}
for vertex in self.vertices:
adjancy_list[vertex.key]=[]
print(f"{vertex.key} ->", end=" ")
arc = vertex.arc
while arc:
adjancy_list[vertex.key].append({'to_vertex':
arc.destination.key, 'weight': arc.weight})
print(f"({arc.destination.key}, {arc.weight})", end=" -> ")
arc = arc.nextArc
print("None")
return adjancy_list

def phase1_main():
g = Graph()

v = int(input("Enter number of vertices: "))


while True:
e = int(input("Enter number of edges: "))
if v-1 <= e <= v*(v-1)/2:
break
else:

©Computer Engineering Department, Kuwait University, 2023


print(f"Ensure v-1 <= e <= v(v-1)/2 for v = {v}")
start_time = time.time()
# Insert vertices
for i in range(v):
g.insert_vertex(str(i+1))

# Ensure the graph is connected


for i in range(1, v):
weight = random.randint(1, 20)
g.insert_arc(str(i), str(i+1), weight)
e -= 1

# Add remaining edges


while e > 0:
fromKey = str(random.randint(1, v))
toKey = str(random.randint(1, v))
if fromKey != toKey and not g.arc_exists(fromKey, toKey):
weight = random.randint(1, 20)
g.insert_arc(fromKey, toKey, weight)
e -= 1

print("\nConstructed graph (Adjacency List):")


x= g.print_graph()

end_time = time.time()
print(f"\nRunning time of phase_1 algorithm: {end_time - start_time:.6f}
seconds")
return x

example1={'1': [{'to_vertex': '2', 'weight': 2}, {'to_vertex': '3', 'weight':


15}, {'to_vertex': '4', 'weight': 5}],
'2': [{'to_vertex': '1', 'weight': 2}, {'to_vertex': '4', 'weight': 1}],
'3': [{'to_vertex': '1', 'weight': 15}, {'to_vertex': '4', 'weight': 1}],
'4': [{'to_vertex': '1', 'weight': 5}, {'to_vertex': '2', 'weight': 1},
{'to_vertex': '3', 'weight': 1}]}

example2={'1': [{'to_vertex': '2', 'weight': 2}, {'to_vertex': '4', 'weight':


10}],
'2': [{'to_vertex': '1', 'weight': 2}, {'to_vertex': '3', 'weight':
2},{'to_vertex': '5', 'weight': 4}],
'3': [{'to_vertex': '2', 'weight': 2}, {'to_vertex': '4', 'weight':
50},{'to_vertex': '5', 'weight': 40}],
'4': [{'to_vertex': '1', 'weight': 10}, {'to_vertex': '3', 'weight': 50}],
'5': [{'to_vertex': '2', 'weight': 4}, {'to_vertex': '3', 'weight': 40}]}

adjacency_list = phase1_main()
vertices = list(adjacency_list.keys())
num_vertices = len(vertices)
new_vertices = vertices
visited_vertices = []
cost = []
process_indicator =0
start_vertex = vertices[0]
last_vertex = ''
backward = {}

©Computer Engineering Department, Kuwait University, 2023


def first_vertex():
global num_vertices, new_vertices, visited_vertices, cost, backward

visited_vertices.append(start_vertex)
num_vertices -=1
backward [start_vertex] = {'back_vertices':[], 'back_cost':[0],
'total_back_cost': 0}
new_vertices.remove(start_vertex)
weights = []
min_weight=0
for i in adjacency_list[start_vertex]:
weights.append(i['weight'])

if len(weights) == 1 :
min_weight = weights[0]
else:
min_weight = min(weights)

next_vertex = ''
for i in adjacency_list[start_vertex]:
if i['weight'] == min_weight:
next_vertex = i['to_vertex']

cost.append(min_weight)
visited_vertices.append(next_vertex)
num_vertices -=1
new_vertices.remove(next_vertex)
parent_vertex = start_vertex
parent_cost = min_weight
backward [next_vertex] = {'back_vertices':[parent_vertex],
'back_cost':[parent_cost], 'total_back_cost': parent_cost}
return next_vertex,parent_vertex,parent_cost

def rest_of_vertices(next_vertex,parent_vertex,parent_cost):
global num_vertices, new_vertices, visited_vertices, cost,
process_indicator, last_vertex, backward

connected_vertices = []
for i in adjacency_list[next_vertex]:
connected_vertices.append(i['to_vertex'])
new_connected_vertices = []
for i in connected_vertices:
if i not in visited_vertices:
new_connected_vertices.append(i)

if not new_connected_vertices and num_vertices != 0:

if next_vertex == parent_vertex:
for i in range(0, len(visited_vertices)):
if visited_vertices[i] == next_vertex:
parent_vertex = visited_vertices[i-1]
parent_cost = cost[i-1]
break

©Computer Engineering Department, Kuwait University, 2023


next_vertex = parent_vertex
visited_vertices.append(parent_vertex)
cost.append(parent_cost)
return next_vertex,parent_vertex,parent_cost

weights = []
min_weight=0
for i in adjacency_list[next_vertex]:
if i['to_vertex'] in new_connected_vertices:
weights.append(i['weight'])

if len(weights) == 0:
...
elif len(weights) == 1:
min_weight = weights[0]

else:
min_weight = min(weights)

weights2=[]
min_weight2=0
cost_second = 0
if min_weight > parent_cost:
for i in adjacency_list[parent_vertex]:
if i['to_vertex'] not in visited_vertices:
weights2.append(i['weight'])

if len(weights2) == 0:
...
elif len(weights2) == 1:
min_weight2 = weights2[0]
cost_second = min_weight2 + parent_cost
else:
min_weight2 = min(weights2)
cost_second = min_weight2 + parent_cost

if cost_second > 0 and cost_second < min_weight:


next_vertex = parent_vertex
visited_vertices.append(parent_vertex)
cost.append(parent_cost)
return next_vertex,parent_vertex,parent_cost

if min_weight <= parent_cost or cost_second == 0 or cost_second >=


min_weight:
for i in adjacency_list[next_vertex]:
if i['to_vertex'] in new_connected_vertices and i['weight'] ==
min_weight:
parent_vertex = next_vertex
next_vertex = i['to_vertex']
visited_vertices.append(next_vertex)
cost.append(min_weight)
num_vertices -=1
new_vertices.remove(next_vertex)
parent_cost = min_weight
temp_cost = float('inf')
for j in adjacency_list[next_vertex]:
if j['to_vertex'] == start_vertex:

©Computer Engineering Department, Kuwait University, 2023


temp_cost= j['weight']
if temp_cost <= backward
[parent_vertex]['total_back_cost'] + parent_cost:
backward [next_vertex] =
{'back_vertices':[start_vertex], 'back_cost':[temp_cost], 'total_back_cost':
temp_cost}
break
else:
break

if temp_cost > 0:
backward[next_vertex] = {'back_vertices':[parent_vertex],
'back_cost':[parent_cost], 'total_back_cost': parent_cost}
for i in range(0,len(backward
[parent_vertex]['back_vertices'])):

backward[next_vertex]['back_vertices'].append(backward
[parent_vertex]['back_vertices'][i])
backward[next_vertex]['back_cost'].append(backward
[parent_vertex]['back_cost'][i])
backward [next_vertex]['total_back_cost'] = backward
[parent_vertex]['total_back_cost'] + parent_cost

if num_vertices == 0 :
last_vertex = next_vertex
for i in range(0,len(backward
[last_vertex]['back_vertices'])):
visited_vertices.append(backward
[last_vertex]['back_vertices'][i])
cost.append(backward [last_vertex]['back_cost'][i])

process_indicator = 1
return next_vertex,parent_vertex,parent_cost

return next_vertex,parent_vertex,parent_cost

def plot_graph():
G = nx.Graph()
for vertex in adjacency_list:
for i in range(0, len(adjacency_list[vertex])):

node = vertex
neighbor = adjacency_list[vertex][i]['to_vertex']
weight = adjacency_list[vertex][i]['weight']
G.add_edge(node, neighbor, weight=weight)

pos= nx.circular_layout(G)
nx.draw(G, pos, with_labels=True, node_color='lightblue', node_size=500,
font_size=12, edge_color='gray', width=1)

edge_labels = nx.get_edge_attributes(G, 'weight')


nx.draw_networkx_edge_labels(G, pos,
edge_labels=edge_labels,label_pos=0.2, font_size=9,font_color='red')

©Computer Engineering Department, Kuwait University, 2023


plt.show()

def main():
v = num_vertices
start_time = time.time()
next_vertex,parent_vertex,parent_cost= first_vertex()

while True:
next_vertex,parent_vertex,parent_cost =
rest_of_vertices(next_vertex,parent_vertex,parent_cost)
if process_indicator == 1:
cost_as_string = [str(num) for num in cost]
print(f"\nShortest path = {', '.join(visited_vertices)}")
print(f"Path Cost = {' + '.join(cost_as_string)} = {sum(cost)}")
break
end_time = time.time()
print(f"\nAlgorithm Running time for a Graph of Size {v} is: ({end_time -
start_time:.6f}) seconds")
plot_graph()

main()

Output for 3 Examples:

Example1:

©Computer Engineering Department, Kuwait University, 2023


Example2:

Example3:

©Computer Engineering Department, Kuwait University, 2023


Running Time for Varying Graph Sizes:

Graph Size Running Time


5 0.000000000
10 0.000000000
20 0.000997782
30 0.000997066
40 0.001995087
50 0.002991915
60 0.004986763
70 0.007893801
80 0.011967897
90 0.014960289
100 0.019946337

Conclusion:

©Computer Engineering Department, Kuwait University, 2023


The modified Traveling Salesman Problem (TSP) presents an intriguing challenge, melding the
traditional constraints of the TSP with the flexibility of bypassing specific paths and returning to
the start point. This elasticity offers a broader solution space and potential avenues for
optimizing the route, making it a fertile ground for algorithmic exploration.

Results Explanation:

Upon analyzing the provided algorithm, it is evident that while it aims to find an approximate
solution to our modified TSP, its computational demands increase substantially with the size of
the graph. As demonstrated by the plotted chart, there's a noticeable upward trend in the
execution time as the graph size enlarges, hinting at the algorithm's non-linear time complexity.
This observation aligns well with our analytical conclusion, which places the algorithm in a high
polynomial time complexity bracket.

It's worth noting that the world of algorithmic challenges, particularly optimization problems like
TSP, often demands a trade-off between accuracy and computational efficiency. Exact
algorithms might offer optimal solutions but are computationally infeasible for larger datasets.
On the other hand, heuristic or approximation algorithms, like the one explored in this report,
provide near-optimal solutions in a reasonable time frame.

New Concepts Learned:

©Computer Engineering Department, Kuwait University, 2023


Through this project, we deepened our understanding of heuristic methods, backtracking, and
algorithmic time complexity.

Difficulties Faced:

Identifying the right heuristic and ensuring that all cities are visited were some challenges.
Handling edge cases, especially during backtracking, added complexity to the algorithm design.

©Computer Engineering Department, Kuwait University, 2023

You might also like