You are on page 1of 13

Kuwait University

College Engineering and Petroleum


Computer Engineering Department

CpE-300: DES.&ANAL.OF ALGORITHMS


Semester: Fall 2023- 2024
Section No. 02A

Project Report
Student Names: Student IDs:
Lulwah Alfadhli 2181150182
Amal Alenezi 2182160072

Instructor Name: Dr.Mehemet Karataa


TA Name: Eng. Tareek Al-Melhem
Introduction

The concept of graphs, as abstract structures made up of nodes and edges, has its roots deeply
embedded in the annals of mathematics and computer science. From modeling the seven
bridges of Königsberg in the 18th century to representing intricate data structures in today's
digital age, graphs have been instrumental in shaping numerous theories and applications.

Connected Graph: At its core, a connected graph is a representation of unity and cohesion.
Every pair of vertices in this graph type is connected in some manner, ensuring no vertex
remains in isolation. Historically, understanding and ensuring connectivity in networks,
whether they be of cities, roads, or computers, has been of paramount importance. For example,
ensuring that every house in a city gets electricity would require an understanding of a
connected graph.

Weighted Graph: As we transition from simple connections to understanding the quality or cost
of these connections, weighted graphs come into the picture. Here, every edge has an associated
weight, which could represent distances, costs, or any measurable metric. This kind of graph
mirrors real-life scenarios perfectly. For instance, when planning road trips, we don't just
consider the availability of a road (connection) but also the distance or time it takes (weight)
to traverse it.

Traveling Salesman Problem (TSP): This classic conundrum, often visualized as a traveling
salesman trying to minimize his travel costs, has been a topic of intrigue for over two centuries.
The problem is seemingly simple: Find the shortest possible route that visits each city once and
returns to the origin. However, the potential solutions grow exponentially with the addition of
each city, making it a computationally challenging task. TSP isn't just a theoretical puzzle; it's
reflective of real-world optimization problems in logistics, transportation, and even DNA
sequencing.

Our focus in this report is a variant of the traditional TSP. In this adaptation, while the objective
remains to minimize travel, the salesman has the liberty to bypass certain paths or connections,
provided he visits every city at least once. This added flexibility makes the problem both
intriguing and potentially more amenable to heuristic solutions.

Algorithm Description and Time Complexity

Algorithm Description:
The code is structured to solve a variation of the Traveling Salesman Problem, wherein each
city needs to be visited at least once, but not every edge (route between two cities) needs to be
traversed and returning to the first city.

Initialization:

• Starts from the first city.


• Proceeds to its closest neighbor based on distance.
Traversal Mechanism:

• For the current city, its unvisited neighbors are identified.


• If no unvisited neighbors are found, the algorithm backtracks to the previously visited
city.
• The next move is decided based on the shortest distance to an unvisited neighboring
city. If a shorter route via backtracking is identified, the algorithm uses it.
Backtracking:

• Before committing to a direction, the algorithm considers the benefit of moving back
to the starting city.
• It also uses backtracking to revisit previous cities when encountering a dead-end, i.e., a
city where all its neighbors are already visited.
Completion:

• Once every city has been visited, the algorithm prints the path taken and its total
distance.
• The route and total distance are derived from the Visited Nodes and Path Cost data.
Execution Control:

• The `execute` function coordinates the above processes, starting the journey and
continually deciding the next city to visit until the tour is complete.
In essence, the code uses a nearest-neighbor heuristic, complemented by a backtracking
strategy, to find a path through all cities. However, the resulting path might not be the most
optimal.

Time Complexity Analysis:

Initialization:

• Getting distances and finding the closest neighbor: O(n)


During Traversal (`process_remaining_nodes` function):

• Checking unvisited neighbors for the current city: O(n)


• Deciding the shortest path to an unvisited city: O(n)
• In the worst-case scenario, if the algorithm hits a city where all neighbors have been
visited and needs to backtrack, this could require looking back through all visited cities,
leading to another: O(n)
• The operations involved in updating the path history (in the `previous_path` dictionary)
can also take up to: O(n)
Combining the above steps, each call to `process_remaining_nodes` can take up to O(n)
operations. Since this function might need to be called for every city in the graph, its overall
complexity becomes O(n^2).

Main Execution (`execute` function):

• The main loop in `execute` runs until all cities are visited. In the worst-case, it iterates
n times (for n cities).
• For each of those iterations, it calls the `process_remaining_nodes` function, which has
a complexity of O(n^2).
Multiplying these complexities together, the dominant complexity for the main execution
becomes O(n^3).

In summary, the worst-case time complexity of the entire algorithm is O(n^3), where n
represents the number of cities in the graph. This indicates that the algorithm's performance
could degrade significantly as the number of cities increases.

Data Structures and Function Algorithms

Data Structures

Data Structure graph:

• a dictionary where each key is a string (node) and value is a list of dictionaries
• each dictionary contains 'dest' as destination node and 'dist' as distance to the destination
Data Structure nodes:

• a list of keys from graph


Data Structure total_nodes:
• length of nodes
Data Structure remaining_nodes:

• a copy of nodes
Data Structure visited_nodes:

• an empty list
Data Structure path_cost:

• an empty list
Data Structure is_done:

• an integer with value 0


Data Structure initial_node:

• the first element of nodes


Data Structure final_node:

• an empty string
Data Structure previous_path:

• an empty dictionary
Algorithm initialize:

Pre: None

Post: Returns next node to visit, initial node and minimum distance

1. append initial_node to visited_nodes


2. decrement total_nodes by 1
3. remove initial_node from remaining_nodes
4. calculate distances for all edges of initial_node
5. find min_dist as minimum of distances
6. find next_node as the destination of the edge with min_dist
7. append min_dist to path_cost
8. append next_node to visited_nodes
9. decrement total_nodes by 1
10. remove next_node from remaining_nodes
11. update previous_path for next_node with initial_node, min_dist, and
total_previous_cost as min_dist
12. return next_node, initial_node, min_dist
Algorithm process_remaining_nodes (curr_node, prev_node, prev_cost):

Pre: Current node, previous node, and previous cost

Post: Returns current node, previous node, and previous cost after processing

1. list neighbors of curr_node


2. list unvisited_neighbors from neighbors that are not in visited_nodes(followed by the
logic to determine the next node based on distances, backtracking, and updating
visited_nodes, path _cost, previous_path, and other data structures)
3. If there are no unvisited_neighbors and total_nodes is not zero:
a. Set current node as previous node if they are the same
b. Update curr_node and prev_node based on visited_nodes
c. return curr_node, prev_node, prev_cost
4. Compute distances for unvisited neighbors of curr_node
5. Set min_dist as the minimum of these distances, or infinity if there are none
6. If min_dist > prev_cost, compute alternative distances and costs from prev_node to
unvisited nodes
7. If the alternative path is shorter, update curr_node, prev_node, and prev_cost and
return them
8. If not, update the path based on the shortest distance from curr_node to unvisited
neighbors
9. Handle Update previous_path with the nodes and costs leading up to the current node
10. backtracking by comparing the distance to the initial_node with the pr evious path's
cost
11. If there are no remaining nodes:
a. Update final_node
b. Append the previous nodes and costs to visited_nodes and path_cost
c. Print the shortest path and its cost
d. Set is_done to 1
e. Return curr_node, prev_node, prev_cost
Algorithm execute:
Pre: None

Post: Executes the algorithm to find the shortest path and prints the result

1. Initialize node_count as total_nodes


2. Start a timer
3. Initialize curr_node, prev_node, and prev_cost using the initialize function
4. While the algorithm is not done:
a. Update curr_node, prev_node, and prev_cost using the process_remaining_nodes
function
5. Stop the timer
6. Print the execution time
Source Code
import time
import random

class Graph:
def __init__(self):
self.adjacency_list = {}

def add_vertex(self, key):


if key not in self.adjacency_list:
self.adjacency_list[key] = []

def add_edge(self, from_vertex, to_vertex, weight):


if from_vertex in self.adjacency_list and to_vertex in
self.adjacency_list:
# Check if the edge or its reverse already exists
if not self.edge_exists(from_vertex, to_vertex) and not
self.edge_exists(to_vertex, from_vertex):
self.adjacency_list[from_vertex].append({'dest': to_vertex,
'dist': weight})
self.adjacency_list[to_vertex].append({'dest': from_vertex,
'dist': weight}) # For undirected graph
else:
print("Vertex not found!")

def edge_exists(self, from_vertex, to_vertex):


for edge in self.adjacency_list[from_vertex]:
if edge['dest'] == to_vertex:
return True # Edge already exists
return False

def display_graph(self):

for vertex in self.adjacency_list:


connections = []
for edge in self.adjacency_list[vertex]:
connections.append(f"({edge['dest']}, {edge['dist']})")
print(f"\n{vertex} -> {' -> '.join(connections) if connections
else 'None'}")
def phase_1():
g = Graph()

num_vertices = int(input("\n\tEnter number of vertices: "))


while True:
num_edges = int(input("\tEnter number of edges: "))
if num_vertices - 1 <= num_edges <= num_vertices * (num_vertices -
1) // 2:
break
else:
print(f"Ensure v-1 <= e <= v(v-1)/2 for v = {num_vertices}")

for i in range(1, num_vertices + 1):


g.add_vertex(str(i))

for i in range(2, num_vertices + 1): # Ensure the graph is connected


g.add_edge(str(i-1), str(i), random.randint(1, 20))
num_edges -= 1

# Add remaining edges


while num_edges > 0:
from_vertex = str(random.randint(1, num_vertices))
to_vertex = str(random.randint(1, num_vertices))
if from_vertex != to_vertex:
if not g.edge_exists(from_vertex, to_vertex) and not
g.edge_exists(to_vertex, from_vertex):
g.add_edge(from_vertex, to_vertex, random.randint(1, 20))
num_edges -= 1
g.display_graph()
return g.adjacency_list

graph = phase_1()

nodes = list(graph.keys())
total_nodes = len(nodes)
remaining_nodes = nodes.copy()
visited_nodes = []
path_cost = []
is_done = 0
initial_node = nodes[0]
final_node = ''
previous_path = {}

def initialize():
global total_nodes, remaining_nodes, visited_nodes, path_cost,
previous_path

visited_nodes.append(initial_node)
total_nodes -= 1
previous_path[initial_node] = {'previous_nodes': [], 'previous_cost':
[0], 'total_previous_cost': 0}
remaining_nodes.remove(initial_node)

distances = [edge['dist'] for edge in graph[initial_node]]


min_dist = min(distances)
next_node = [edge['dest'] for edge in graph[initial_node] if
edge['dist'] == min_dist][0]
path_cost.append(min_dist)
visited_nodes.append(next_node)
total_nodes -= 1
remaining_nodes.remove(next_node)

previous_path[next_node] = {'previous_nodes': [initial_node],


'previous_cost': [min_dist], 'total_previous_cost': min_dist}
return next_node, initial_node, min_dist

def process_remaining_nodes(curr_node, prev_node, prev_cost):


global total_nodes, remaining_nodes, visited_nodes, path_cost, is_done,
final_node, previous_path

neighbors = [edge['dest'] for edge in graph[curr_node]]


unvisited_neighbors = [node for node in neighbors if node not in
visited_nodes]

if not unvisited_neighbors and total_nodes:


if curr_node == prev_node:
for i in range(0, len(visited_nodes)):
if visited_nodes[i] == curr_node:
prev_node = visited_nodes[i-1]
prev_cost = path_cost[i-1]
break
curr_node = prev_node
visited_nodes.append(prev_node)
path_cost.append(prev_cost)
return curr_node, prev_node, prev_cost

distances = [edge['dist'] for edge in graph[curr_node] if edge['dest']


in unvisited_neighbors]
min_dist = min(distances) if distances else float('inf')

if min_dist > prev_cost:


alt_distances = [edge['dist'] for edge in graph[prev_node] if
edge['dest'] not in visited_nodes]
alt_min_dist = min(alt_distances) if alt_distances else
float('inf')
alt_cost = alt_min_dist + prev_cost

if 0 < alt_cost < min_dist:


curr_node = prev_node
visited_nodes.append(prev_node)
path_cost.append(prev_cost)
return curr_node, prev_node, prev_cost

if min_dist <= prev_cost or not alt_distances or alt_cost >= min_dist:


for edge in graph[curr_node]:
if edge['dest'] in unvisited_neighbors and edge['dist'] ==
min_dist:
prev_node = curr_node
curr_node = edge['dest']
visited_nodes.append(curr_node)
path_cost.append(min_dist)
total_nodes -= 1
remaining_nodes.remove(curr_node)
prev_cost = min_dist

# Handle backtracking
temp_dist = next((edge['dist'] for edge in graph[curr_node]
if edge['dest'] == initial_node), float('inf'))
if temp_dist <=
previous_path[prev_node]['total_previous_cost'] + prev_cost:
previous_path[curr_node] = {'previous_nodes':
[initial_node], 'previous_cost': [temp_dist], 'total_previous_cost':
temp_dist}
else:
previous_path[curr_node] = {'previous_nodes':
[prev_node], 'previous_cost': [prev_cost], 'total_previous_cost':
prev_cost}
for idx in
range(len(previous_path[prev_node]['previous_nodes'])):

previous_path[curr_node]['previous_nodes'].append(previous_path[prev_node][
'previous_nodes'][idx])

previous_path[curr_node]['previous_cost'].append(previous_path[prev_node]['
previous_cost'][idx])
previous_path[curr_node]['total_previous_cost'] +=
previous_path[prev_node]['total_previous_cost']

if not total_nodes:
final_node = curr_node
for idx in
range(len(previous_path[final_node]['previous_nodes'])):

visited_nodes.append(previous_path[final_node]['previous_nodes'][idx])

path_cost.append(previous_path[final_node]['previous_cost'][idx])

print(f"\nShortest path = {', '.join(visited_nodes)}")


print(f"\nPath Cost = {' + '.join(map(str, path_cost))}
= {sum(path_cost)}")
is_done = 1
return curr_node, prev_node, prev_cost

return curr_node, prev_node, prev_cost

def execute():
node_count = total_nodes
start = time.time()
curr_node, prev_node, prev_cost = initialize()

while not is_done:


curr_node, prev_node, prev_cost =
process_remaining_nodes(curr_node, prev_node, prev_cost)

end = time.time()
print(f"\nRunning time for a Graph of Size {node_count} is: {end -
start:.9f} seconds")

execute()
Three Example Outputs
Algorithm Running Time

Graph Size (Number of Cities) Running Time


5 0.000000000
10 0.002987623
20 0.006982088
30 0.009972572
40 0.009763241
50 0.010945082
60 0.008975983
70 0.008977652
80 0.010969162
90 0.011968136

Conclusion

Insights: The algorithm, while solving the modified TSP, might not always offer the most
efficient route but guarantees visiting every city.

Our heuristic algorithm, provide near-optimal solutions in a reasonable time frame as shown
in the figure:
Graph size and Running Time
0.018

0.016

0.014
Running Time(sec)

0.012

0.01

0.008

0.006

0.004

0.002

0
0 20 40 60 80 100 120
Graph Size

Learning Curve: This project enlightened us about heuristic strategies, the importance of
backtracking, and complexities in algorithm design.

Challenges Encountered: Designing a comprehensive backtracking mechanism and ensuring


that the heuristic does not miss any city were prominent challenges.

In wrapping up, this exploration into the modified TSP underscores a broader narrative:
Computational problems, especially those rooted in real-world scenarios, demand a balance
between optimal solutions and practical execution. The ongoing challenge lies in continually
refining our algorithms to inch closer to this elusive equilibrium.

You might also like