G2 Phase2
G2 Phase2
G2 Phase2
Project Report
Student Names: Student IDs:
Lulwah Alfadhli 2181150182
Amal Alenezi 2182160072
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:
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:
• 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.
Initialization:
• 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
• 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 copy of nodes
Data Structure visited_nodes:
• an empty list
Data Structure path_cost:
• an empty list
Data Structure is_done:
• 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
Post: Returns current node, previous node, and previous cost after processing
Post: Executes the algorithm to find the shortest path and prints the result
class Graph:
def __init__(self):
self.adjacency_list = {}
def display_graph(self):
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)
# 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])
def execute():
node_count = total_nodes
start = time.time()
curr_node, prev_node, prev_cost = initialize()
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
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.
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.