Fundemantal of AI - Project

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

Project 2

Artificial Intelligence

Fall 2023

[Solutions to this assignment must be submitted via CANVAS prior to midnight on the due date. These
dates and times vary depending on the milestone to be submitted. Submissions up to one day late will be
penalized 10% and a further 10% will be applied for the next day late. Submissions will not be accepted if
more than two days later than the due date.]

This project may be undertaken in pairs or individually. If working in a pair, state the names of the two
people undertaking the project and the contributions that each has made. Only ONE submission should
be made per group.

You will make use of the Networkx environment that you should be familiar with from Project 1.

Purpose: To gain a thorough understanding of the working of an agent that maximizes flow through a
network. Maximizing network flow ensures that the network’s resources are fully utilized according to
their capacities, and this reduces time to move commodities through the network. Numerous real-world
applications benefit from maximizing flow such as maximizing data throughput in a computer network,
maximizing the flow of oil through a network of pipelines, etc. Figures 1 A and B below provide a
concrete example of a network flow problem. The source node is S, and the sink (destination) node is T.

V 1/3
2/6
1/5 T
4/8
3/3 Flow into T before optimization = 1+4+4=9
S
W 2/9 4/5
2/2
4/5
U Z
2/3

V 1/3 Flow into T after optimization with hill


6/6
climbing = 1+7+5=13
5/5 T Note: optimal flow = 1+8+5=14
3/3 7/8

S
W 3/9 5/5
2/2
4/5
U Z
2/3

Figures 1 A and B

Each link has a capacity (e.g., maximum number of barrels of oil that can flow per hour) and an actual
flow. The actual flow in the above figure for each edge is indicated in red while the capacity is given in
black. Figure 1 A represents a sub optimal flow as we can see that the flow across most of the edges do
not utilize their full capacity. The solution in Figure 1 B utilizes the full capacity of the network and is
thus an optimal solution.

One constraint that must be enforced is the law of conservation: the sum of the outflows from a node
cannot exceed the sum of inflows into that same node. Thus, for example, the sum of the inflows into
node w must be equal to the sum of the outflows from node w.

In practice, on a large network it may take too long to obtain the optimal solution and therefore we will
use an extended version of the Hill Climbing (HC) algorithm (see Tutorial 3 for details) to construct a
solution. We saw in the lectures that hill climbing can return high quality solutions that avoid getting
stuck in local minima and return solutions that are as close to the global (optimal) solution as possible.
The solution that you generate must also satisfy the conservation law across all nodes in the network.

Hill Climbing requires an initial solution to start off with and this solution can be generated randomly
(refer Tutorial 4 for code for this purpose). Of course, you need to ensure that your start solution is
valid: all links must obey the law of conservation and all edge values must not exceed their assigned
capacities. Edge capacities can be assigned at random to be an integer between 1 and 10.

Once an initial solution is in place, HC needs to generate in each iteration a neighbor of the solution that
existed in the previous iteration until no uphill moves are possible (i.e., when the value of the heuristic
function is 0 for all child nodes of the current node).

Jot down any questions/doubts that you may have and feel free to ask me questions in class or in
person. Together with your partner, work out a strategy before you start coding the solution in Python.
Given the limited timeframe for the project, some simplifications have been assumed.

Environment Description: The environment is a network that is specified in the form of a graph
structure as represented in Figures 1 A and B (simply a sample, not the entire network). The graph that
you will use to represent the network will consist of 30 nodes, with two different average connectivity
values (see below).

Your task in this project is to implement the following requirements. The project has two milestones,
with Part A representing design of algorithms and Part B consisting of algorithm implementation and
experimentation. The milestones have different deadlines as shown below.

Part A
Produce pseudo code versions of algorithms needed to construct:

a) A heuristic function suitable for a hill climbing solution for the network resource problem.
b) A successor function – a successor function specifies how a node is expanded into its child
nodes. Your successor function should contain code that (1) generates paths and (2)
removes redundant paths.
c) The hill climbing algorithm itself takes as parameters: source and sink nodes and calls the
two functions mentioned in a) and b) above.

Due at midnight on Wednesday 11 October (35


marks)

Part B
Using the pseudo code that you produced in Part A above to implement a Python program to maximize
the flow. For all of the requirements below use 0 and 29 for source and sink nodes respectively. Your
Python program should meet the following requirements:
R1: Visualize your solution for the graph given in Figure 1 B, showing the capacities and the actual flow
along each edge that is returned by executing your HC algorithm. This is an important test case for your
code and will require you to hard code your graph G. Run your HC code for the graph in Figure 1 A and
present its answer to the total flow into the sink node T. Verify that the value returned is the same value
given in Figure 1 B. (30 marks)

R2: Run your HC code on a graph G containing 30 nodes and an average connectivity of 3 (connectivity
parameter of 0.1). Work out the total flow value tf into node T and compare it with the total flow value
tf_net returned by the NetworkX version of the hill climbing algorithm. Present both values, tf and
tf_net. (10 marks)

R3: This part has 3 sub parts:


a. Run your HC code with 30 different random graphs G (having 30 nodes and an average
connectivity of 3). Work out the average value of the total flow into sink node T. Present the
total flow tf_net_avg(3). (10 marks)
b. Now re-run your HC code on 30 different random graphs, each of size 30 nodes once again, but
this time with an average connectivity of 2. Compute and present the average flow tf_net_av(2).
(10 marks)
c. Do you notice a significant difference between tf_net_avg(3) and tf_net_avg(2)? If so, why do
you think this occurs? (5 marks)

Due at midnight on Wednesday 25 October

Notes:

1. Produce ONE pdf document that contains pseudo code, actual Python code and the answers to the
questions. Do NOT bury answers to questions as comments in your code. If you are unsure how to
produce an original pdf from your Google Co-lab notebook, refer to this tutorial on Youtube: Bing
Videos. Do NOT simply use the print option as this will result in an image. If you submit an image
version of a pdf it will not be graded.
2. If you finish Part A before its deadline, start working on Part B immediately afterwards.

End of project specification


PROJECT 2 – PART A:
PSEUDOCODES

1. DEFINE HEURISTIC FUNCTION

FUNCTION compute-heuristic(path, G) RETUNS min_increase in flow (to be added to the path)

min_increase  ∞
FOR each edge(u, v) in path:
unused_capacity  capacity.egde(u, v) – flow.edge(u, v)
IF unsued_capacity < min_increase:
min_increase = unused_capacity

2. DEFINE SUCCESSORS FUNCTION

FUNCTION generate-successors(source, sink, G) RETURNS a list of valid_paths

simple_paths  LIST(GET_ALL_SIMPLE_PATHS_EDGES(G, source, sink))


valid_paths  empty list [ ]
FOR each path in simple_paths:
heuristic_value  compute-heuristic(path, G)
IF heuristic_value > 0:
APPEND path TO valid_paths

3. DEFINE HILL-CLIMBING ALGORITHM


FUNCTION hill-climbing(source, sink, G) RETURNS total_flow into the sink
WHILE true do:
successors  generate-successors(source, sink, G)
IF successors.IS_EMPTY( ):
BREAK
heuristic_values  empty list [ ]
probabilities  empty list [ ]
FOR each s in successors:
h  compute-heuristic(s, G)
APPEND h TO heuristic_values
total_heuristic  SUM(heuristic_values)
FOR each h in heuristic_values:
h_probability  (h/total_heuristic)
APPEND h_probability TO probabilities
selected_path  RANDOM_CHOICE_WITH_PROBABILITY(successors,
probabilities)
increase_value  compute-heuristic(selected_path, G)
FOR each edge(u, v) in selected_path do:
increase_flow(G, u, v, increase_value)

total_flow  SUM_OF_FLOW_INTO_SINK(G, sink)


RETURN total_flow
October 27, 2023

PROJECT 2 - PART B
STUDENT NAME: SAMIR TARDA

Source Code:
https://colab.research.google.com/drive/1QZx-ix7yf-ZwE7hggSrbaHtgHdBsczJt?usp=sharing

#Part B - R1

[ ]: from networkx.algorithms.shortest_paths.weighted import␣


↪single_source_bellman_ford_path

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

# 1. GRAPH INITIALIZATION
source = "S"
sink = "T"
G = nx.DiGraph()
G.add_edges_from([
("S", "V", {'flow': 2}), ("S", "V",{'capacity': 6}),
("S", "W", {'flow': 3}), ("S", "W",{'capacity': 3}),
("S","U", {'flow': 4}), ("S","U",{'capacity': 5}),
("V","W", {'flow': 1}), ("V","W",{'capacity': 5}),
("V","T", {'flow': 1}), ("V","T",{'capacity': 3}),
("W","Z", {'flow': 2}), ("W","Z",{'capacity': 9}),
("W","T", {'flow': 4}), ("W","T",{'capacity': 8}),
("U","W", {'flow': 2}), ("U","W",{'capacity': 2}),
("U","Z", {'flow': 2}), ("U","Z",{'capacity': 3}),
("Z","T", {'flow': 4}), ("Z","T",{'capacity': 5})
])

# 2. GRAPH VISUALIZATION
links = [(u, v) for (u, v, d) in G.edges(data=True)]
pos = nx.nx_pydot.graphviz_layout(G)
nx.draw_networkx_nodes(G, pos, node_size=1200, node_color='lightblue',␣
↪linewidths=0.05) # draw nodes

nx.draw_networkx_edges(G, pos, edgelist=links, width=4, arrows=True,␣


↪arrowsize=20) # draw edges with arrows
nx.draw_networkx_labels(G, pos, font_size=20, font_family="sans-serif")
edge_data = nx.get_edge_attributes(G, 'flow')

1
capacity_data = nx.get_edge_attributes(G, 'capacity')
edge_labels = {(u, v): f"{edge_data[(u, v)]}/{capacity_data[(u, v)]}" for u, v␣
↪in edge_data}

nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels)

plt.show()

# 3. Defining the HEURISTIC FUNCTION


# The heuristic is essentially the minimum unused capacity along the path.
def compute_heuristic(path, G):

min_increase = float('inf') # Initialize min_increase to infinity


for u, v in path:
unused_capacity = G[u][v]['capacity'] - G[u][v]['flow']
if unused_capacity < min_increase:
min_increase = unused_capacity
return min_increase

# 4. Defining the SUCCESSORS FUNCTION


# - Generating a list of valid paths from a source to a sink in graph G.
# - A path is valid if its heuristic value is greater than 0.
def generate_successors(source, sink, G):

simple_paths = list(nx.all_simple_edge_paths(G, source=source, target=sink))


valid_paths = []

for path in simple_paths:


heuristic_value = compute_heuristic(path, G)
if heuristic_value > 0:
valid_paths.append(path)

return valid_paths

# 4.1 Testing the generate_successors function


generate_successors(source, sink, G)

# 5.1 Updating the flow along each edge and calculating the total flow of all␣
↪edges

def increase_flow(G, u, v, value):


G[u][v]['flow'] += value #Increase the flow of edge (u, v) by the given␣
↪value.

# 5.2 Calculating total flow into the sink


def sum_of_flow_into_sink(G, sink):
return sum(G[u][sink]['flow'] for u in G.predecessors(sink))

# 6. Defining HILL-CLIMBINGH ALGORITHM

2
# - to maximize the flow from the source to the sink in graph G.
def hill_climbing(source, sink, G):

while True:
successors = generate_successors(source, sink, G)
if not successors:
break

heuristic_values = [compute_heuristic(s, G) for s in successors]


total_heuristic = sum(heuristic_values)
if total_heuristic == 0:
break

probabilities = [h/total_heuristic for h in heuristic_values]

# Randomly select a path based on the probabilities


selected_path = random.choices(successors, weights=probabilities,␣
↪k=1)[0]

increase_value = compute_heuristic(selected_path, G)

for u, v in selected_path:
increase_flow(G, u, v, increase_value)

return sum_of_flow_into_sink(G, sink)

# Testing the hill_climbing function


hill_climbing(source, sink, G)
total_flow_value = hill_climbing(source, sink, G)
print(f"\n Total Flow Value from S to T: {total_flow_value}")

<ipython-input-1-3e8b6b54804c>:25: DeprecationWarning:
nx.nx_pydot.graphviz_layout depends on the pydot package, which hasknown issues
and is not actively maintained. Consider usingnx.nx_agraph.graphviz_layout
instead.

See https://github.com/networkx/networkx/issues/5723
pos = nx.nx_pydot.graphviz_layout(G)

3
Total Flow Value from S to T: 14

[ ]: # Visualize the graph with optimal flows


links = [(u, v) for (u, v, d) in G.edges(data=True)]
pos = nx.nx_pydot.graphviz_layout(G)
nx.draw_networkx_nodes(G, pos, node_size=1200, node_color='lightblue',␣
↪linewidths=0.05) # draw nodes

nx.draw_networkx_edges(G, pos, edgelist=links, width=4, arrows=True,␣


↪arrowsize=20) # draw edges with arrows
nx.draw_networkx_labels(G, pos, font_size=20, font_family="sans-serif")
edge_data = nx.get_edge_attributes(G, 'flow')
capacity_data = nx.get_edge_attributes(G, 'capacity')
edge_labels = {(u, v): f"{edge_data[(u, v)]}/{capacity_data[(u, v)]}" for u, v␣
↪in edge_data}

nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels)

plt.show()

<ipython-input-2-976b31985a69>:3: DeprecationWarning:
nx.nx_pydot.graphviz_layout depends on the pydot package, which hasknown issues
and is not actively maintained. Consider usingnx.nx_agraph.graphviz_layout
instead.

4
See https://github.com/networkx/networkx/issues/5723
pos = nx.nx_pydot.graphviz_layout(G)

#Part B - R2
[1]: import networkx as nx
import random

# 1. New graph initialization and structure adjustment


N = 30 # Define number of nodes
source = 0 # Define your source
sink = N - 1 # Define your sink

G = nx.gnp_random_graph(N, 0.1, directed=True)

# 2. Initializing connections between nodes and adjusting edges


# 2.1 Ensuring every node has at least one incoming and one outgoing edge.
for u in G.nodes():
in_list = G.in_edges(u)
out_list = G.out_edges(u)
if len(in_list) == 0 and u > 0:
G.add_edge(u - 1, u)
if len(out_list) == 0 and u < N - 1:
G.add_edge(u, u + 1)

5
# 2.2 Removing any edges that could create a bidirectional connection between␣
↪two nodes.

# This simplification can help avoid complexities in network flow␣


↪algorithms.

for u in G.nodes():
adj_list = G.adj[u]
for nbr, datdict in adj_list.copy().items():
print(u,nbr)
if (u > nbr) and G.has_edge(nbr, u):
G.remove_edge(u, nbr)
print (G.edges())

0 1
0 11
0 14
0 16
0 18
1 5
1 7
1 10
2 4
2 10
2 11
2 15
2 17
3 2
3 5
3 17
3 21
3 26
4 5
5 6
5 11
6 2
6 10
6 14
6 26
7 12
7 13
8 27
9 3
9 6
10 12
10 16
10 21
10 22

6
10 26
11 1
11 3
11 8
11 17
12 8
12 21
12 29
13 5
13 18
13 21
13 29
14 15
14 24
15 0
15 1
15 7
15 16
16 3
16 11
16 19
17 13
18 4
18 15
18 17
18 19
18 26
19 3
19 12
19 22
19 29
19 20
20 25
21 0
21 2
21 16
22 3
22 9
22 12
22 16
23 14
23 25
24 2
24 3
24 13
25 26
26 8
26 18

7
27 10
27 12
27 18
27 23
27 28
28 2
28 12
29 4
29 8
29 10
29 24
[(0, 1), (0, 11), (0, 14), (0, 16), (0, 18), (1, 5), (1, 7), (1, 10), (2, 4),
(2, 10), (2, 11), (2, 15), (2, 17), (3, 2), (3, 5), (3, 17), (3, 21), (3, 26),
(4, 5), (5, 6), (5, 11), (6, 2), (6, 10), (6, 14), (6, 26), (7, 12), (7, 13),
(8, 27), (9, 3), (9, 6), (10, 12), (10, 16), (10, 21), (10, 22), (10, 26), (11,
1), (11, 3), (11, 8), (11, 17), (12, 8), (12, 21), (12, 29), (13, 5), (13, 18),
(13, 21), (13, 29), (14, 15), (14, 24), (15, 0), (15, 1), (15, 7), (15, 16),
(16, 3), (16, 11), (16, 19), (17, 13), (18, 4), (18, 15), (18, 17), (18, 19),
(18, 26), (19, 3), (19, 12), (19, 22), (19, 29), (19, 20), (20, 25), (21, 0),
(21, 2), (21, 16), (22, 3), (22, 9), (22, 12), (22, 16), (23, 14), (23, 25),
(24, 2), (24, 3), (24, 13), (25, 26), (26, 8), (27, 10), (27, 12), (27, 18),
(27, 23), (27, 28), (28, 2), (28, 12), (29, 4), (29, 8), (29, 10), (29, 24)]

[2]: # 2.3 Checking for bidirectional edges


nx.is_weakly_connected(G)

[2]: True

[3]: # 2.4 Assigning a 'flow' and a 'capacity' for each node in the graph
for u, v in G.edges():
G[u][v]['flow'] = random.randint(0, 5)
G[u][v]['capacity'] = G[u][v]['flow'] + random.randint(1, 10)

[4]: # 3. Defining the HEURISTIC FUNCTION


# The heuristic is essentially the minimum unused capacity along the path.
def compute_heuristic(path, G):

min_increase = float('inf') # Initialize min_increase to infinity


for u, v in path:
unused_capacity = G[u][v]['capacity'] - G[u][v]['flow']
if unused_capacity < min_increase:
min_increase = unused_capacity
return min_increase

# 4. Defining the SUCCESSORS FUNCTION


# - Generating a list of valid paths from a source to a sink in graph G.
# - A path is valid if its heuristic value is greater than 0.

8
def generate_successors(source, sink, G):

simple_paths = list(nx.all_simple_edge_paths(G, source=source, target=sink))


valid_paths = []

for path in simple_paths:


heuristic_value = compute_heuristic(path, G)
if heuristic_value > 0:
valid_paths.append(path)

return valid_paths

# 5.1 Updating the flow along each edge and calculating the total flow of all␣
↪edges

def increase_flow(G, u, v, value):


G[u][v]['flow'] += value #Increase the flow of edge (u, v) by the given␣
↪value.

# 5.2 Calculating total flow into the sink


def sum_of_flow_into_sink(G, sink):
return sum(G[u][sink]['flow'] for u in G.predecessors(sink))

# 6. Defining HILL-CLIMBINGH ALGORITHM


# - to maximize the flow from the source to the sink in graph G.
def hill_climbing(source, sink, G):

while True:
successors = generate_successors(source, sink, G)
if not successors:
break

heuristic_values = [compute_heuristic(s, G) for s in successors]


total_heuristic = sum(heuristic_values)
if total_heuristic == 0:
break

probabilities = [h/total_heuristic for h in heuristic_values]

# Randomly select a path based on the probabilities


selected_path = random.choices(successors, weights=probabilities,␣
↪k=1)[0]

increase_value = compute_heuristic(selected_path, G)

for u, v in selected_path:
increase_flow(G, u, v, increase_value)

9
return sum_of_flow_into_sink(G, sink)

# Executing hill-climbing algorithm


total_flow_value = hill_climbing(source, sink, G)
print(f"\n1. Total Flow from Source (S) to Sink (T): {total_flow_value}")

# Calculating the NetworkX's max flow


total_flow_net = nx.maximum_flow_value(G, source, sink, capacity='capacity')
print(f"2. Net Total Flow from Source (S) to Sink (T): {total_flow_net}")

1. Total Flow from Source (S) to Sink (T): 20


2. Net Total Flow from Source (S) to Sink (T): 20
#Part B - R3.a
[5]: import networkx as nx
import random

# 1. New graph initialization and structure adjustment


num_graphs = 30 # Define number of Graphs
N = 30 # Define number of nodes per garph
source = 0 # Define your source
sink = N - 1 # Define your sink
total_flow_values = [] # Initialize sum total flow of all 30 graphs

def initialize_graph(N):
G = nx.gnp_random_graph(N, 0.1, directed=True)

# 2. Initialize connections between nodes and adjusting edge


# 2.1 Ensure every node has at least one incoming and one outgoing edge.
for u in G.nodes():
in_list = G.in_edges(u)
out_list = G.out_edges(u)
if len(in_list) == 0 and u > 0:
G.add_edge(u - 1, u)
if len(out_list) == 0 and u < N - 1:
G.add_edge(u, u + 1)

# 2.2 Remove any edges that could create a bidirectional connection between␣
↪two nodes.
# This simplification can help avoid complexities in network flow␣
↪algorithms.

for u in G.nodes():
adj_list = G.adj[u]
for nbr, datdict in adj_list.copy().items():
if (u > nbr) and G.has_edge(nbr, u):
G.remove_edge(u, nbr)

10
# 2.3 Assigning a 'flow' and a 'capacity' for each node in the graph
for u, v in G.edges():
G[u][v]['flow'] = random.randint(0, 5)
G[u][v]['capacity'] = G[u][v]['flow'] + random.randint(1, 10)

return G

[6]: for _ in range(num_graphs):


# For each graph in num_graphs:
# 1. Initialize a new random graph
G = initialize_graph(N)

# 2. Compute the total flow using the hill-climbing algorithm


total_flow_value3 = hill_climbing(source, sink, G)
total_flow_values.append(total_flow_value3)

[7]: print(total_flow_values)

[18, 15, 8, 28, 3, 10, 25, 2, 14, 18, 10, 6, 6, 7, 37, 8, 4, 18, 22, 13, 22, 10,
15, 18, 9, 8, 14, 8, 26, 15]

[8]: # Calculate the average total flow value


tf_net_avg3 = sum(total_flow_values) / num_graphs

# Print the average total flow value


print(f"Average Total Flow (tf_net_avg3) over {num_graphs} random graphs:␣
↪{tf_net_avg3}")

Average Total Flow (tf_net_avg3) over 30 random graphs: 13.9


#Part B - R3.b
[9]: import networkx as nx
import random

# 1. New graph initialization and structure adjustment


num_graphs = 30 # Define number of Graphs
N = 30 # Define number of nodes per garph
source = 0 # Define your source
sink = N - 1 # Define your sink
total_flow_values = [] # Initialize sum total flow of all 30 graphs

def initialize_graph(N):
G = nx.gnp_random_graph(N, 0.067, directed=True)

# 2. Initialize connections between nodes and adjusting edge


# 2.1 Ensure every node has at least one incoming and one outgoing edge.

11
for u in G.nodes():
in_list = G.in_edges(u)
out_list = G.out_edges(u)
if len(in_list) == 0 and u > 0:
G.add_edge(u - 1, u)
if len(out_list) == 0 and u < N - 1:
G.add_edge(u, u + 1)

# 2.2 Remove any edges that could create a bidirectional connection between␣
↪two nodes.
# This simplification can help avoid complexities in network flow␣
↪algorithms.

for u in G.nodes():
adj_list = G.adj[u]
for nbr, datdict in adj_list.copy().items():
if (u > nbr) and G.has_edge(nbr, u):
G.remove_edge(u, nbr)

# 2.3 Assigning a 'flow' and a 'capacity' for each node in the graph
for u, v in G.edges():
G[u][v]['flow'] = random.randint(0, 5)
G[u][v]['capacity'] = G[u][v]['flow'] + random.randint(1, 10)

return G

[10]: for _ in range(num_graphs):


# For each graph in num_graphs:
# 1. Initialize a new random graph
G = initialize_graph(N)

# 2. Compute the total flow using the hill-climbing algorithm


total_flow_value2 = hill_climbing(source, sink, G)
total_flow_values.append(total_flow_value2)

[11]: # Calculate the average total flow value


tf_net_avg3 = sum(total_flow_values) / num_graphs

# Print the average total flow value


print(f"Average Total Flow (tf_net_avg2) over {num_graphs} random graphs:␣
↪{tf_net_avg3}")

Average Total Flow (tf_net_avg2) over 30 random graphs: 10.566666666666666

#Part B - R3.c
Yes, there’s a noticeable difference. T his d ifference is no t su rprising, an d it ca n be at tributed to
the following reasons: 1. Graph Connectivity: A graph with a higher average connectivity

12
(like R3.a) typically has more edges than one with a lower average connectivity (like R3.b). More
edges mean there are potentially more paths from the source to the sink, leading to a greater
chance of finding paths with higher capacities and flows. Imagine we have two cities; each one
represents a “graph”. One city where almost every street (edge) is connected to numerous others
vs. the second city with fewer connected roads. The many different paths in this highly accessible
network of the first city makes it a lot easier to travel from a source location to a given destination
(sink). 2. Redundant Paths: as mentioned earlier, with a higher average connectivity, there’s a
greater chance of having multiple paths between any two nodes. This redundancy in paths provides
flexibility and multiple options for the hill-climbing algorithm. It allows for better distribution,
optimization, and utilization of the available capacities in the graph, leading to an enhanced flow
from the source to the sink. For example, in the context of the hill-climbing algorithm, multiple
paths between the source and the sink provide alternative routes for routing the flow. If one path
becomes saturated (i.e., reaches its capacity), the algorithm can look for another path that still
has unused capacity. This increases the chances of finding paths where additional flow can be
pushed through. 3. Bottleneck Reduction: In network flow problems, the maximum flow is
often determined by the “bottleneck”, typically an edge (or a set of edges) that has the smallest
capacity along a path from the source to the sink. With more edges and paths available (due
to higher connectivity), the impact of these bottlenecks can be minimized, as flow can be routed
through alternate paths.
References
1. Mahmood, A., Javaid, N., Razzaq, S., Ilahi, M., Khan, Z.A., Qasim, U. (2021). Optimal Route
Selection with Load Balancing in IoT-Based Vehicular Networks Using water Flow-Based Technique.
IEEE Access, 9, 5568-5583.
2. Nandi, S., Nandi, S.K., Yadav, A., Singh, S.P., Kumar, A., Rodrigues, J.J. (2022). Telco-Cloud-
Empowered 6G: A Graph-Theory-Based Connectivity Model Using SDN. IEEE Network, 36(2),
184-192.

13

You might also like