DM> Lab Manual
DM> Lab Manual
Name :
USN :
Section :
SJB INTITUTE OF TECHNOLOGY
Institution’s Vision
Institution’s Mission
Department of Mathematics
Department Vision
Is envisaging to become world’s foremost technical school fostering intellectual
excellence.
Department Mission
5. Modern tool usage: Create, select, and apply appropriate techniques, resources, and modern
engineering and IT tools including prediction and modelling to complex engineering activities with an
understanding of the limitations.
6. The engineer and society: Apply reasoning informed by the contextual knowledge to assess societal,
health, safety, legal and cultural issues and the consequent responsibilities relevant to the professional
engineering practice.
7. Environment and sustainability: Understand the impact of the professional engineering solutions in
societal and environmental contexts, and demonstrate the knowledge of, and need for sustainable
development.
8. Ethics: Apply ethical principles and commit to professional ethics and responsibilities and norms of
the engineering practice.
9. Individual and team work: Function effectively as an individual, and as a member or leader in
diverse teams, and in multidisciplinary settings.
10. Communication: Communicate effectively on complex engineering activities with the engineering
community and with society at large, such as, being able to comprehend and write effective reports and
design documentation, make effective presentations, and give and receive clear instructions.
11. Project management and finance: Demonstrate knowledge and understanding of the engineering
and management principles and apply these to one’s own work, as a member and leader in a team, to
manage projects and in multidisciplinary environments.
12. Life-long learning: Recognize the need for, and have the preparation and ability to engage in
independent and life-long learning in the broadest context of technological change
Do’s
It is mandatory for all the students to attend all practical classes &complete the experiments
as per syllabus.
Students should strictly follow the lab timings, dress code with Apron & ID cards.
Should maintain a neat observation book.
Study the theory and logic before executing the program.
Submit the completed lab records of executed programs and update the index book in every
lab session.
Should prepare for viva questions regularly. Handle the computer systems carefully.
Don’ts
Should not take Bags and Mobile phones into the Laboratory.
Correlation Matrix
CO/PO PO1 PO2 PO3 PO4 PO5 PO6 PO7 PO8 P09 PO10 PO11 PO12 PSO1 PSO2 PSO3
CO1 3 2 1 1 1
CO2 3 2 1 1 1
CO3 3 2 1 1 1
CO4 3 2 1 1 1
CO5 3 2 1 1 1
Note: Correlation levels: 1: Slight (Low) 2: Moderate (Medium) 3: Substantial (High), “-” No correlation
12 Using Menger’s theorem, find the minimum vertex cut between source and target.
Discrete mathematics underpins many computer science concepts, from algorithms to data
structures. Graph theory, a key branch, helps in solving problems related to networks, optimization,
and more.
In this manual, you'll find clear explanations of core topics such as set theory, combinatorics,
and graph algorithms. Each concept is illustrated with Python code with practical implementation.
The manual includes exercises to reinforce learning and real-world applications to show the
relevance of these concepts in computer science.
Software Requirements:
Jupyter Notebook: An interactive environment for writing and running Python code, ideal for
experiments and visualizations.
PyCharm: A robust Integrated Development Environment(IDE) for Python development with
advanced features for code management and debugging.
VS Code: A versatile editor with support for Python, including useful extensions for enhanced
development.
Anaconda: A distribution that includes Python, Jupyter Notebook, and commonly used scientific
libraries, simplifying package management and deployment.
Python 3.x: The programming language version required for compatibility with libraries and code
examples.
NumPy and Pandas: Libraries for numerical computing and data manipulation, often used in
conjunction with Jupyter Notebook.
Matplotlib and Seaborn: Libraries for data visualization, useful for graphical representation of results
and analysis.
Hardware Requirements:
Computer with Sufficient RAM: At least 8GB of RAM is recommended for smooth performance,
especially for handling large datasets and running complex algorithms.
Processor: A multi-core processor (e.g., Intel i5/i7 or Advanced Micro Devices (AMD) Ryzen) for
efficient code execution and handling multiple tasks.
Storage Space: Adequate disk space for installing software, libraries, and storing data files, ideally
with an SSD for faster performance.
2. Each lab will be evaluated for 15 marks and finally average will be
taken for 15 marks.
3. Viva-voce questions shall be asked in labs and attendance also can be
considered for everyday lab evaluation.
4. Tests shall be considered for 5 marks and final lab assessment is for 20
marks.
5. Student has to score minimum 8 marks out of 20 to pass l ab component.
1. Logical AND
The AND operator returns True if both operands are True.
OUTPUT:
A B A AND B
True True True
True False False
False True False
False False False
2. Logical OR
The OR operator returns True if at least one of the operands is True.
A B A OR B
True True True
True False True
False True True
False False False
3. Logical NOT
The NOT operator returns True if the operand is False and vice versa.
def logical_not(a):
return not a
OUTPUT:
A NOT A
True False
False True
OUTPUT:
A B A XOR B
True True False
True False True
False True True
False False False
Write a Python program that prints a truth table for the logical expression (A AND B) OR
(NOT C) AND (A XOR C).
Let's combine these connectives in a complex logical expression:
A person is eligible for a loan if they have a good credit score, a stable job, and no outstanding
debts.
# Example usage
credit_score = 650
has_stable_job = True
has_outstanding_debts = False
1.3 Exercise:
1. Write a Python program that prints a truth table for the following logical expression
i) (A and B) or (not C).
ii) (A or not B) and (not C).
iii) (A or B) and (C or not D) and not (A and C).
def is_tautology(expression):
# Generate all possible truth values for p and q
variables = {'p', 'q'}
truth_values = list(itertools.product([True, False], repeat=len(variables)))
# Example usage
expression = "(p or q) and (not p or q)" # Example proposition
result = is_tautology(expression)
print("The proposition is a tautology." if result else "The proposition is not a tautology.")
OUTPUT:
The proposition is not a tautology.
2. ¬ (p ∨ q) → (¬ p ∧ ¬ q).
def is_tautology():
# Generate all possible truth values for p and q
truth_values = list(product([True, False], repeat=2))
OUTPUT:
The proposition ¬(p ∨ q) → (¬p ∧ ¬q) is a tautology.
2.3 Exercise:
Answer: A proposition is a declarative statement that can either be True or False, but not
both. Propositions are used as the fundamental building blocks in propositional logic.
2. What is a tautology?
Answer: A tautology is a logical statement that is always True, regardless of the truth values
of the individual components in the expression. For example, P∨¬P (P or not P) is a tautology
because it is always True no matter what the truth value of P is.
Answer: To determine if a proposition is a tautology, you create a truth table that evaluates
the proposition for all possible combinations of truth values for the involved variables. If the
proposition is True for every possible combination, it is a tautology.
Answer: A truth table is a mathematical table used to compute the truth values of a logical
expression based on the truth values of its variables. It lists all possible combinations of input
values and the corresponding output of the logical expression.
Answer: Yes. The expression (P∧Q)⟹(P∨Q) is a tautology. It evaluates to True for all
possible truth values of P and Q, as shown by constructing a truth table.
Answer: Logical equivalence between two propositions occurs when both propositions have
the same truth value for every possible combination of truth values for the variables involved.
For example, P⟹Q is logically equivalent to ¬P∨Q.
8. How would you check for logical equivalence between two propositions?
Answer: To check for logical equivalence, you construct a truth table for both propositions. If
the output columns for both propositions are identical for every combination of truth values,
then the propositions are logically equivalent.
Answer:
Answer: Propositional logic is used in areas like digital circuit design, software engineering
(particularly in if-else conditions and decision-making algorithms), artificial intelligence,
formal verification of systems, and automated reasoning systems.
to establish the validity of the sum of the first n natural numbers using both the
basic step and an inductive step.
1. Basic Step: Verify that the statement holds for the initial value (often n=0 or n=1).
2. Inductive Step: Assume the statement holds for an arbitrary positive integer k (this is called
the inductive hypothesis), and then prove that it must also hold for k+1.
If both steps are successfully proven, then the statement is true for all integers greater than or equal to
the basic step.
3.2.1 Compute the sum of the first n odd numbers using the formula n2.
The function you provided correctly computes the sum of the first n odd numbers.
This is based on a well-known mathematical fact that the sum of the first n odd numbers is
equal to n2.
1.Mathematical Insight: The sum of the first n odd numbers (1, 3, 5, ..., 2n−1) can be
shown to be equal to n2. For example:
Sum of the first 1 odd number: 1=12
Sum of the first 2 odd numbers: 1+3=4=22
Sum of the first 3 odd numbers: 1+3+5=9=32 and so forth.
def sum_of_first_n_odds(n):
if n == 0:
return 0
else:
return n * n
# Example usage:
n=8
result = sum_of_first_n_odds(n)
print(f"The sum of the first {n} odd numbers is: {result}")
OUTPUT:
3.2.2 Compute the sum of the first n even numbers using the formula n*(n+1).
def sum_of_first_n_even(n):
if n == 0:
return 0
else:
return n*(n+1)
# Example usage:
n=3
result = sum_of_first_n_even(n)
print(f"The sum of the first {n} even numbers is: {result}")
OUTPUT:
3.2.3 Compute the sum of the squares of the first n odd integers
def main():
try:
# Input from the user
n = int(input("Enter the number of odd integers: "))
if n <= 0:
print("Please enter a positive integer.")
return
# Output result
print(f"The sum of the squares of the first {n} odd integers is: {total_sum}")
except ValueError:
print("Invalid input. Please enter a valid integer.")
if __name__ == "__main__":
main()
OUTPUT:
Enter the number of odd integers: 3
The sum of the squares of the first 3 odd integers is: 35
3.2.4 Compute the sum of the squares of the first n odd integers
def sum_of_first_n_natural_numbers(n):
return n * (n + 1) // 2
# Example usage
n = int(input("Enter the number of natural numbers: "))
print(f"The sum of the first {n} natural numbers is: {sum_of_first_n_natural_numbers(n)}")
OUTPUT:
Enter the number of natural numbers: 6
The sum of the first 6 natural numbers is: 21
3.2.5 Verify the sum of the first n natural numbers using both the basic step
and an inductive step.
def sum_of_natural_numbers(n):
""" Compute the sum of the first n natural numbers using the formula."""
return n * (n + 1) // 2
def verify_inductive_step(k):
"""
Verify the inductive step for the formula:
S_n = n * (n + 1) // 2
# Sum for k + 1
sum_k_plus_1 = sum_of_natural_numbers(k + 1)
def main():
# Basic step
n=1
print(f"Basic step: n = {n}")
print(f"Sum of the first {n} natural numbers: {sum_of_natural_numbers(n)}")
if __name__ == "__main__":
main()
OUTPUT:
Basic step: n = 1
Sum of the first 1 natural numbers: 1
3.3 Exercise:
1. Compute the sum of the squares of the first n even integers and display the result.
2. Write a program for the Fibonacci sequence can be defined recursively, and also verify that
the relationship F(n+2)=F(n+1)+F(n) holds using mathematical induction.
Answer: Mathematical induction is a method of proof used in mathematics to prove that a statement
is true for all natural numbers. It involves two steps: the basic step, where the statement is shown to
be true for the first value (usually n=1), and the induction step, where we assume the statement is true
for some integer k, and then prove it for k+1.
Answer: The base case in this proof is for n=1, where we show that the sum of the first odd number,
which is 1, is equal to 12.
Answer: The induction hypothesis is the assumption that the formula holds true for some arbitrary
integer k. That is, S(k)=k2 for the sum of the first k odd numbers.
Answer: The goal of the induction step is to prove that if the formula holds for n=k, it also holds for
n=k+1. This is done by showing that S(k+1)=(k+1)2 using the induction hypothesis.
Answer: Mathematical induction is valid because it leverages the idea of recursively building on a
basic step. By proving that the statement is true for a basic step and that the truth of the statement for
n=k implies the truth of the statement for n=k+1, we can conclude that the statement is true for all
natural numbers.
6. How does the sum of the first n odd numbers relate to squares?
Answer: The sum of the first n odd numbers equals n2. For example, the sum of the first three odd
numbers (1, 3, 5) is 9, which is 32.
7. Can you explain the time complexity of calculating permutations and combinations?
Answer: The time complexity of calculating permutations and combinations is primarily dominated
by the factorial operations, which grow rapidly. Computing factorials of large numbers can be
expensive, but Python's built-in math.factorial function is optimized for performance.
i. Permutations is the arrangement of items where the order matters. The number of permutations of
n items taken r at a time is given by:
𝑛!
P(n, r)=
(𝑛−𝑟)!
ii. Combinations refer to the selection of items where the order does not matter. The number of
combinations of n items taken r at a time is given by:
𝑛!
C(n, r)=
r!⋅(n−r)!
import math
# Example usage
n=6
r=3
print(f"Permutations of {n} items taken {r} at a time: {permutations(n, r)}")
OUTPUT:
Permutations of 6 items taken 3 at a time: 120
import math
# Example usage
n =6
OUTPUT:
Combinations of 6 items taken 3 at a time: 20
4.2.3 Write a Python program that calculates and displays both permutations
and combinations for a given number of items.
import math
def main():
n = int(input("Enter the total number of items (n): "))
r = int(input("Enter the number of items to choose or arrange (r): "))
if r > n:
print("r cannot be greater than n.")
return
if __name__ == "__main__":
main()
OUTPUT:
Enter the total number of items (n): 5
Enter the number of items to choose or arrange (r): 3
Permutations of 5 items taken 3 at a time: 60
Combinations of 5 items taken 3 at a time: 10
4.3 Exercise:
1. Calculate the number of possible passwords of length 4 using a set of 10 unique characters.
2. Determine the number of ways to sample a subset of data from a larger dataset. (Example: A dataset
of 50 records and want to choose a sample of 10, calculate the number of possible samples.)
3. Calculate the number of possible genetic variations or gene combinations.
4. Calculate the number of different possible configurations for a puzzle.
𝑛!
Answer: We calculate permutations in Python using the formula P(n, r)= . We can use Python's
(𝑛−𝑟)!
math.factorial function to compute the factorials and then apply the formula.
Answer: The math.factorial function computes the factorial of a given number. Factorials are used in
permutations and combinations calculations to determine the number of ways to arrange or choose
objects.
3. Can you provide an example of calculating permutations for 4 objects taken 2 at a time?
4!
Answer: Using the formula P(4, 2)= =12. In Python, we would call the permutations function
(4−2)!
as permutations(4, 2), which would return 12.
4. How would you handle cases where r>n in permutations and combinations?
Answer: If r>n, the number of permutations and combinations is zero because you cannot choose or
arrange more items than are available. In the permutations and combinations functions, you should
check if r>n and handle it appropriately, usually by returning zero or raising an exception.
Answer: Permutations consider the order of the items, meaning different arrangements of the same
items count as distinct permutations. Combinations do not consider order; different arrangements of
the same items count as one combination.
6. How do you use Python's itertools module for permutations and combinations?
7. Can you explain the time complexity of calculating permutations and combinations?
Answer: The time complexity of calculating permutations and combinations is primarily dominated
by the factorial operations, which grow rapidly. Computing factorials of large numbers can be
expensive, but Python's built-in math.factorial function is optimized for performance.
5.1 Objectives:
Use python
to apply the concept of one-to-one(injective), where a function maps distinct elements
of the domain to distinct elements in the codomain.
to apply the concept of onto(surjective), where a function covers the entire codomain
with its outputs
A function f:A→B is called one-to-one (or injective) if different elements in the domain A map
to different elements in the codomain B. In other words, if f(a1)=f(a2) implies a1=a2, then f is
injective.
Mathematically: f(a1)=f(a2) ⟹ a1=a2
A function f:A→B is called onto (or surjective) if every element in the codomain B has a
preimage in the domain A. In other words, for every b∈B, there is at least one a∈A such that
f(a)=b.
Mathematically: For every b∈B, there exists a∈A such that f(a)=b
# Define domain
domain = {1, 2, 3, 4, 5}
# Check injectivity
print("Function is injective:", is_injective(domain, example_function))
OUTPUT:
Function is injective: True
def f(x):
return 3 * x**4 + 1
def is_surjective(codomain):
""" Check if the function f(x) = 3x^4 + 1 is surjective for a given codomain.
:param codomain: A set of output values to test.
:return: True if the function is surjective for the given codomain, False otherwise. """
# Check if all values in the codomain are greater than or equal to 1
for y in codomain:
if y < 1:
return False
return True
OUTPUT:
Function f(x) = 3x^4 + 1 is surjective: True
𝒙 + 𝟐 𝒊𝒇 𝒙 ≤ 𝟎
5.2.3 Check the function 𝒈(𝒙) = { is one-to-one and/or onto.
𝟐𝒙 − 𝟏 𝒊𝒇 𝒙 > 𝟎
def g(x):
"""Piecewise function to test."""
if x <= 0:
return x + 2
else:
return 2 * x - 1
def compute_outputs(domain):
"""
Compute the outputs of the function g(x) for a given domain.
def is_injective(domain):
"""
Check if the function g(x) is injective (one-to-one).
def is_surjective(codomain):
"""
Check if the function g(x) is surjective for a given codomain.
def main():
# Define domain and codomain
domain = {-3, -1, 0, 1, 2} # Example domain values
codomain = {-1, 0, 1, 2, 3} # Example codomain values
# Check injectivity
injectivity_result = is_injective(domain)
print("Function g(x) is injective:", injectivity_result)
# Check surjectivity
surjectivity_result = is_surjective(codomain)
print("Function g(x) is surjective:", surjectivity_result)
if __name__ == "__main__":
main()
OUTPUT:
5.3 Exercise:
1. Check the function j(x)= 4x−7 is one-to-one or onto.
2. Verify if t(x)=log(x+1) is injective (one-to-one) over the domain [0,10].
1
3. Determine if 𝑓(𝑥) = is surjective (onto) for the codomain (0,∞).
𝑥+1
5. Can you provide an example of a function that is one-to-one but not onto?
Answer: Consider the function f(x)=x2 where the domain is {1,2,3} and the codomain is {1,4,9}. This
function is one-to-one but not onto if the codomain is set to {1,4} instead of {1,4,9}, as 9 is not mapped
to by any element in the domain.
6. Can you give an example of a function that is onto but not one-to-one?
Answer: Consider the function f(x)=mod(x,2) where the domain is {1,2,3} and the codomain is {0,1}.
This function is onto because every element in the codomain {0,1} is covered. However, it is not one-
to-one because both 1 and 3 map to the same value 1.
7. How would you modify the program to handle functions with larger domains and codomains?
Answer: The program can handle larger domains and codomains by using efficient data structures like
sets and dictionaries. The implementation will scale as long as the domain and codomain are correctly
represented and the function is defined over these sets.
10. How can you generalize the concept of injectivity and surjectivity to functions with more
complex structures?
Answer: For more complex structures, such as functions defined over multiple variables or functions
between different types of sets, the concepts of injectivity and surjectivity still apply. The general
principles are the same: injectivity requires unique mapping for each element in the domain, and
surjectivity requires complete coverage of the codomain. In higher dimensions or more complex
structures, these concepts are extended to ensure the function's properties meet the required criteria.
An equivalence relation is a type of relation on a set that satisfies three properties: reflexivity,
symmetry, and transitivity.
1. Reflexivity: Every element is related to itself. For a relation R on a set A, this means for every
a ∈A, the pair (a, a) must be in R.
2. Symmetry: If an element a is related to b, then b must also be related to a. For (a, b)∈R, the
pair (b, a) must also be in R.
3. Transitivity: If an element a is related to b, and b is related to c, then a must also be related to
c. For (a, b)∈R and (b, c)∈R, the pair (a, c) must also be in R.
def is_symmetric(R):
"""Check if the relation R is symmetric."""
for (a, b) in R:
if (b, a) not in R:
return False
return True
# Example usage
A = {1, 2, 3}
R = {(1, 1), (1, 2), (2, 1), (2, 2), (3, 3)}
if check_equivalence_relation(A, R):
print("The relation R is an equivalence relation.")
else:
print("The relation R is not an equivalence relation.")
OUTPUT:
The relation R is an equivalence relation.
6.2.2 Find the equivalence classes for relation R on set A. Given that A = {1, 2, 3}
R = {(1, 1), (1, 2), (2, 1), (2, 2), (3, 3)}.
for a in A:
if a not in visited:
# Find all elements related to a
class_members = {b for (x, b) in R if x == a}
class_members.update({x for (x, b) in R if b == a})
classes.append(class_members)
visited.update(class_members)
return classes
# Example usage
A = {1, 2, 3}
R = {(1, 1), (1, 2), (2, 1), (2, 2), (3, 3)}
equivalence_classes = find_equivalence_classes(A, R)
print("Equivalence classes:", equivalence_classes)
OUTPUT:
Equivalence classes: [{1, 2}, {3}]
def generate_relation_from_classes(classes):
"""Generate the relation R from equivalence classes."""
R = set()
for cls in classes:
for a in cls:
for b in cls:
R.add((a, b))
return R
# Example usage
classes = [{1, 2}, {3}]
R = generate_relation_from_classes(classes)
print("Generated relation R:", R)
OUTPUT:
Generated relation R: {(1, 2), (2, 1), (1, 1), (3, 3), (2, 2)}
6.2.4 Check if two people are equivalent if they have the same name and age.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
p1 = Person("RAMA", 45)
p2 = Person("RAMA", 45)
p3 = Person("LAKSHMANA", 30)
OUTPUT:
True
False
6.3 Exercise:
1. Given the set A={1,2,3} and the relation R={(1,1),(2,2),(1,2)}, check whether the given relation R
on set A is an equivalence relation or not.
2. Write a program on a group emails that are similar (or equivalent) to detect spam.
Answer: An equivalence relation on a set is a relation that satisfies three properties: reflexivity,
symmetry, and transitivity. These properties ensure that the relation partitions the set into equivalence
classes.
Answer: A relation is reflexive if every element in the set is related to itself. In terms of ordered pairs,
for every element x in the set, the pair (x,x) should be in the relation.
Answer: A relation is symmetric if for every pair (a,b) in the relation, the pair (b,a) must also be in the
relation. This means that if a is related to b, then b must be related to a.
Answer: A relation is transitive if whenever (a,b) and (b,c) are in the relation, the pair (a,c) must also
be in the relation. This ensures that the relation can be "chained" through intermediate elements.
Answer: An example of an equivalence relation is the relation of equality on any set. For any set A the
relation {(a,a)∣a∈A} is reflexive, symmetric, and transitive, making it an equivalence relation.
Answer: Equivalence classes are subsets of a set where each subset contains elements that are
equivalent to each other under a given equivalence relation. For example, if a is equivalent to b and b
is equivalent to c, then a, b, and c belong to the same equivalence class.
8. How would you modify the Python program to handle relations on larger sets or more complex
structures?
Answer: To handle larger sets or more complex structures, ensure that the relation and the set of
elements are represented efficiently. The program should be able to manage larger data structures, and
you might need to optimize the implementation for performance if working with very large sets.
Answer:
Reflexive: Ensures every element is related to itself, establishing a base point for
comparison.
Symmetric: Ensures that the relationship is mutual, reflecting a bidirectional
connection.
Transitive: Ensures that the relationship is consistent and can be extended through
intermediates, providing a coherent structure for equivalence classes.
10. How does the concept of equivalence relations apply in computer science?
Answer: In computer science, equivalence relations are used in various areas such as database design
(for defining normalization), algorithms (for partitioning and clustering), and formal languages (for
defining equivalence classes of strings). They help in structuring data and simplifying complex
problems.
Use python
The Fibonacci sequence F(n) is defined as: F(0)=0, F(1)=1, F(n)=F(n−1)+F(n−2) for n≥2.
1. Iterative Approach:
2. Recursive Approach:
a, b = 0, 1
print(f"Iteration 1: {a}")
print(f"Iteration 2: {b}")
for i in range(2, n):
a, b = b, a + b
print(f"Iteration {i + 1}: {b}")
# Example usage:
n = 10
print(f"Fibonacci number at position {n} (iterative): {fibonacci_iterative_with_steps(n)}")
OUTPUT:
Iteration 1: 0
Iteration 2: 1
Iteration 3: 1
Iteration 4: 2
Iteration 5: 3
Iteration 6: 5
Iteration 7: 8
Iteration 8: 13
Iteration 9: 21
Iteration 10: 34
Fibonacci number at position 10 (iterative): 34
def fibonacci_recursive(n):
"""Compute the nth Fibonacci number using a recursive approach."""
if n <= 0:
return "Invalid input, n should be a positive integer."
elif n == 1:
return 0
elif n == 2:
return 1
else:
return fibonacci_recursive(n - 1) + fibonacci_recursive(n - 2)
# Example usage:
n=8
print(f"Fibonacci number at position {n} (recursive): {fibonacci_recursive(n)}")
OUTPUT:
Fibonacci number at position 8 (recursive): 13
7.3 Exercise:
1. Write a Python program that calculates the sum of the first n Fibonacci numbers using both iterative
and recursive approaches. Compare its efficiency with the iterative approach.
Answer: The Fibonacci sequence is a series of numbers where each number is the sum of the two
preceding ones, starting from 0 and 1. The sequence typically starts as 0, 1, 1, 2, 3, 5, 8, 13, etc.
2. How does the iterative approach to generating the Fibonacci sequence work?
Answer: The iterative approach uses a loop to compute the Fibonacci numbers sequentially. It starts
with the first two numbers and repeatedly calculates the next number by summing the last two numbers
in the sequence until the desired length is reached.
3. How does the recursive approach to generating the Fibonacci sequence work?
Answer: The recursive approach calculates the Fibonacci sequence by defining the sequence in terms
of itself. It calls the function recursively to find the previous two numbers and then sums them to get
the next number, building the sequence from the base cases.
4. What are the advantages of the iterative approach over the recursive approach for generating
Fibonacci numbers?
Answer: The iterative approach is generally more efficient in terms of both time and space because it
avoids the overhead of multiple function calls and stack space used in recursion. It is less likely to run
into issues with stack overflow for large inputs.
5. What are the disadvantages of the recursive approach to generating Fibonacci numbers?
Answer: The recursive approach can be less efficient due to repeated calculations and the overhead of
function calls. For large n, it can lead to exponential time complexity and stack overflow errors because
of deep recursion.
Answer: The recursive approach can be optimized using memorization or dynamic programming. By
storing previously computed values and reusing them, you can avoid redundant calculations and
significantly improve performance.
7. Can you provide an example of how the iterative approach is implemented in Python?
Answer: Yes, the iterative approach initializes the first two numbers and uses a loop to compute
subsequent numbers by summing the last two numbers, adding each result to the sequence until the
desired length is reached.
8. Can you provide an example of how the recursive approach is implemented in Python?
Answer: Yes, the recursive approach defines the base cases for n≤2 and calls itself to compute the
sequence up to n−1, appending the sum of the last two numbers to build the sequence.
Answer: The Fibonacci sequence appears in various natural phenomena and applications, such as the
branching patterns of trees, the arrangement of leaves on a stem, the reproduction patterns of rabbits,
and in algorithms related to computer science, such as sorting and searching.
10. How would you modify the Python programs to handle very large inputs efficiently?
Answer: For very large inputs, you would use the iterative approach or an optimized recursive
approach with memorization. For extremely large numbers, consider using specialized libraries or
algorithms designed to handle large integers efficiently and avoid stack overflow issues.
8.2.1 Check whether a given relation on a set satisfies the conditions of a partial
order.
class PartialOrder:
def __init__(self, elements, relation):
"""
Initialize a partial order.
def is_reflexive(self):
"""
Check if the relation is reflexive.
"""
for element in self.elements:
if (element, element) not in self.relation:
return False
return True
def is_antisymmetric(self):
"""
Check if the relation is antisymmetric.
"""
for (a, b) in self.relation:
if (b, a) in self.relation and a != b:
return False
def is_transitive(self):
"""
Check if the relation is transitive.
"""
for (a, b) in self.relation:
for (c, d) in self.relation:
if b == c and (a, d) not in self.relation:
return False
return True
def check_partial_order(self):
"""
Check if the relation satisfies all properties of a partial order.
"""
return (self.is_reflexive() and
self.is_antisymmetric() and
self.is_transitive())
# Example usage
elements = [1, 2, 3]
relation = {(1, 1), (2, 2), (3, 3), (1, 2), (1, 3), (2, 3)}
po = PartialOrder(elements, relation)
print("Elements:", po.elements)
print("Relation:", po.relation)
print("Is reflexive:", po.is_reflexive())
print("Is antisymmetric:", po.is_antisymmetric())
print("Is transitive:", po.is_transitive())
print("Is a partial order:", po.check_partial_order())
OUTPUT:
Elements: [1, 2, 3]
Relation: {(2, 3), (1, 2), (3, 3), (2, 2), (1, 1), (1, 3)}
Is reflexive: True
Is antisymmetric: True
Is transitive: True
Is a partial order: True
8.2.2 Verify Partial Order and draw Hasse diagram for the given
relation = [(1, 1), (1, 2), (2, 2), (2, 3), (3, 3)].
import networkx as nx
import matplotlib.pyplot as plt
class PartialOrder:
def __init__(self, elements, relation):
def is_reflexive(self):
return all((elem, elem) in self.relation for elem in self.elements)
def is_antisymmetric(self):
return all((b, a) not in self.relation or a == b for (a, b) in self.relation)
def is_transitive(self):
return all((a, d) in self.relation for (a, b) in self.relation for (c, d) in self.relation
if b == c)
def draw_hasse_diagram(self):
G = nx.DiGraph()
G.add_nodes_from(self.elements)
for (a, b) in self.relation:
if a != b:
G.add_edge(a, b)
# Example usage:
elements = [1, 2, 3]
relation = [(1, 1), (1, 2), (2, 2), (2, 3), (3, 3)]
po = PartialOrder(elements, relation)
po.draw_hasse_diagram()
OUTPUT:
def find_divisors(n):
"""
Function to find all positive divisors of a given number n.
"""
divisors = [ ]
for i in range(1, n + 1):
if n % i == 0:
divisors.append(i)
return divisors
def create_partial_order(divisors):
"""
Create a list of pairs representing the partial order relation.
Each pair (a, b) indicates that a divides b.
"""
relation = [ ]
for i in range(len(divisors)):
for j in range(i + 1, len(divisors)):
if divisors[j] % divisors[i] == 0:
relation.append((divisors[i], divisors[j]))
return relation
# Main program
number = 30
divisors = find_divisors(number)
OUTPUT:
The positive divisors of 30 are: [1, 2, 3, 5, 6, 10, 15, 30]
The relation (a divides b) is: [(1, 2), (1, 3), (1, 5), (1, 6), (1, 10), (1, 15), (1, 30), (2, 6), (2, 10), (2, 30),
(3, 6), (3, 15), (3, 30), (5, 10), (5, 15), (5, 30), (6, 30), (10, 30), (15, 30)]
8.2.4 Create a directed graph (DiGraph) with nodes and directed edges.
import networkx as nx
import matplotlib.pyplot as plt
def create_directed_graph():
"""
Create a directed graph (DiGraph) with nodes and directed edges.
"""
# Create a directed graph
G = nx.DiGraph( )
# Add nodes
G.add_nodes_from([1, 2, 3, 4, 5])
return G
def draw_directed_graph(G):
"""
Draw the directed graph using matplotlib.
"""
pos = nx.spring_layout(G, seed=4) # positions for all nodes
# Add title
plt.title("Directed Graph (DiGraph) Visualization")
# Show plot
plt.show( )
# Main program
if __name__ == "__main__":
# Create the directed graph
G = create_directed_graph( )
OUTPUT:
8.3 Exercise:
1. Generate and visualize the Hasse diagram to represent the partial order of the divisor 42.
2. Let A={1,2,3,4,6} and R be a relation on A define by aRb if and only if "a is multiple of b"
represent the relation R as a matrix , draw its digraph and relation R.
Viva Questions and Answers
Answer: A partial order is a relation on a set that is reflexive, antisymmetric, and transitive. It
generalizes the concept of ordering and allows some elements to be incomparable.
Answer: A relation is reflexive if every element in the set is related to itself. In terms of ordered
pairs, for every element x in the set, the pair (x, x) should be in the relation.
Answer: The antisymmetric property states that if (a, b) and (b, a) are both in the relation, then a
must be equal to b. This prevents elements from being mutually related unless they are identical.
Answer: A relation is antisymmetric if, for every pair (a, b) in the relation, if (b, a) is also in the
relation, then a must be equal to b.
Answer: The transitive property states that if (a, b) and (b, c) are in the relation, then (a, c) must also
be in the relation. This ensures that the relation is consistent and can be extended through
intermediate elements.
Answer: A relation is transitive if, for every pair (a, b) and (b, c) in the relation, the pair (a, c) must
also be in the relation.
Answer: An example of a partial order is the "less than or equal to" relation (≤) on the set of real
numbers. It is reflexive (every number is less than or equal to itself), antisymmetric (if a≤b and b≤a,
then a=b), and transitive (if a ≤b and b ≤c, then a ≤c).
8. How would you modify the Python program to handle larger sets or relations?
Answer: To handle larger sets or relations, ensure that the relation and the set of elements are
represented efficiently. The program should be capable of processing larger data structures and may
need optimization for performance if dealing with very large sets.
Answer: Checking if a relation is a partial order is important for understanding the structure and
properties of the set. It is used in various fields such as mathematics, computer science, and database
theory to define and analyze hierarchical structures and ordering systems.
10. How does the concept of partial order apply in computer science?
Answer: In computer science, partial orders are used in scheduling problems, data dependency
analysis, hierarchical data structures (like trees and graphs), and in database design to manage and
query relational data.
Use python
The Greedy Coloring Algorithm is a straightforward approach to graph coloring that assigns
colors to vertices of a graph such that no two adjacent vertices have the same color. Here's a step-by-
step description of the algorithm:
1. Initialize:
Create an empty list or dictionary to store the color assigned to each vertex.
Initialize an array to keep track of the available colors.
2. Order the Vertices:
For simplicity, we can process the vertices in any arbitrary order. However, in practice,
ordering vertices by degree or other heuristics can improve performance.
3. Color Assignment:
For each vertex v in the graph:
1. Determine the Colors of Adjacent Vertices:
Check the colors of all vertices adjacent to v.
2. Assign the Smallest Available Color:
Find the smallest color number that is not used by the adjacent vertices.
Assign this color to vertex v.
4. Output the Coloring:
Return or print the color assignments for all vertices.
9.2.1 Greedy algorithm to color the graph G such that no two adjacent vertices
share the same color.
import networkx as nx
def greedy_coloring(G):
"""
Greedy algorithm to color the graph G such that no two adjacent vertices share the same color.
"""
colors = {}
for node in G.nodes():
# Find the colors of adjacent nodes
adjacent_colors = set(colors.get(neighbor) for neighbor in G.neighbors(node))
# Assign the lowest color that is not used by adjacent nodes
color = 1
while color in adjacent_colors:
color += 1
colors[node] = color
return colors
def main( ):
# Create a sample graph
G = nx.Graph( )
G.add_edges_from([(1, 2), (1, 3), (2, 3), (3, 4), (4, 5), (5, 6), (4, 6), (2, 5)])
if __name__ == "__main__":
main( )
OUTPUT:
selected_activities = []
last_end_time = 0
return selected_activities
def main():
# Example activities (start_time, end_time)
activities = [(1, 4), (2, 6), (3, 5), (5, 7), (8, 9), (5, 9)]
print("Selected activities:")
for activity in selected:
print(activity)
if __name__ == "__main__":
main()
OUTPUT:
Selected activities:
(1, 4)
(5, 7)
(8, 9)
9.3 Exercise:
1. Find the minimum number of coins required to make the given amount using Greedy algorithm.
Answer: Graph coloring is the assignment of colors to the vertices of a graph such that no two adjacent
vertices share the same color. The goal is to minimize the number of colors used while satisfying this
constraint.
Answer: The greedy algorithm for graph coloring assigns colors to vertices one by one, choosing the
smallest possible color that is not used by adjacent vertices. This approach does not guarantee the
optimal number of colors but provides a valid coloring.
3. How does the is_safe function work in the graph coloring program?
Answer: The is_safe function checks if it's safe to assign a particular color to a vertex by ensuring
that no adjacent vertices have the same color. It returns True if the color can be assigned safely, and
False otherwise.
Answer: The graph_coloring_util function is a recursive utility function used to assign colors to
vertices. It tries to color the current vertex and recursively proceeds to the next vertex. If it encounters
a conflict, it backtracks and tries a different color.
6. How can you determine if a graph can be colored with a given number of colors?
Answer: You can determine if a graph can be colored with a given number of colors by attempting to
assign colors using an algorithm, such as the greedy algorithm or backtracking. If a valid coloring is
found, the graph can be colored with that number of colors.
7. What is the significance of the adjacency matrix in the graph coloring problem?
Answer: The adjacency matrix is a representation of the graph where the element at row iii and column
j indicates whether there is an edge between vertex I and vertex j. It is used to check adjacency when
assigning colors.
8. Can the greedy algorithm always find the optimal number of colors for a graph?
Answer: No, the greedy algorithm does not always find the optimal number of colors. It provides a
valid coloring but may use more colors than necessary. Finding the optimal number of colors is a more
complex problem, often requiring advanced techniques.
Answer: The time complexity of the greedy graph coloring algorithm depends on the graph
representation and implementation. For the provided algorithm, it can be O(V2) where V is the number
of vertices, due to the need to check each vertex and its adjacent vertices.
10. How would you modify the program to handle larger graphs or different types of input?
Answer: To handle larger graphs or different input types, you would need to optimize the algorithm
for performance, potentially using more efficient data structures or algorithms. We could also
implement more advanced coloring algorithms or heuristics depending on the problem's requirements.
Use python
This method involves generating all permutations of vertices, calculating the path cost for each
permutation, and selecting the one with the minimum cost. This method is feasible for small graphs
due to its factorial time complexity.
10.2.1 Find the shortest Hamiltonian path in a weighted graph using a brute-force
approach
import itertools
def shortest_hamiltonian_path(weights):
"""
Find the shortest Hamiltonian path in a weighted graph using a brute-force approach.
:param weights: A 2D list (adjacency matrix) where weights[i][j] represents the cost to travel from
node i to node j.
:return: The minimum cost of the Hamiltonian path and the corresponding path.
"""
n = len(weights)
min_cost = float('inf')
best_path = [ ]
# Example usage:
weights = [
[0, 5, 15, 20],
[5, 0, 35, 25],
[15, 35, 0, 10],
[20, 25, 10, 0]
]
OUTPUT:
10.2.2 Write a program to optimize the placement of cell towers to cover all
geographic regions with minimal total installation and maintenance costs
using TSP.
import itertools
import math
import matplotlib.pyplot as plt
import networkx as nx
def create_distance_matrix(locations):
n = len(locations)
distance_matrix = [[0] * n for _ in range(n)]
for i in range(n):
for j in range(i + 1, n):
distance = euclidean_distance(locations[i], locations[j])
distance_matrix[i][j] = distance
distance_matrix[j][i] = distance
return distance_matrix
plt.figure(figsize=(10, 7))
nx.draw(G, pos, with_labels=True, node_color='lightblue', node_size=500, font_size=16,
font_weight='bold', edge_color='gray', linewidths=2, width=2)
nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels)
if path:
path_edges = [(path[i], path[i + 1]) for i in range(len(path) - 1)]
nx.draw_networkx_edges(G, pos, edgelist=path_edges, edge_color='red', width=3)
distance_matrix = create_distance_matrix(locations)
min_cost, best_path = find_shortest_tsp_path(distance_matrix)
draw_tower_placement(locations, best_path)
OUTPUT:
Minimum cost of cell tower placement: 16.423574833929543
Optimal path: (0, 1, 2, 3)
10.3 Exercise:
1. Develop a Python program to find the shortest route that a fleet of trucks should take to deliver
goods to multiple locations, minimizing the total distance traveled.
Viva Questions and Answers
Answer: The Traveling Salesman Problem (TSP) is a problem in which a salesman must find the
shortest possible route that visits each city exactly once and returns to the origin city. It is a classic
optimization problem in computer science and operations research.
Answer: A Hamiltonian path is a path in a graph that visits each vertex exactly once. In the context of
TSP, finding a Hamiltonian path means finding a route that visits each city exactly once.
3. How does the brute-force approach work for solving TSP using a Hamiltonian path?
Answer: The brute-force approach generates all possible permutations of the vertices (cities) and
calculates the path length for each permutation. It then selects the permutation with the minimum path
length as the solution.
Answer: The time complexity of the brute-force solution is O(n!), where n is the number of vertices.
This is because there are n! possible permutations of the vertices, and each permutation requires
computing the path length.
Answer: The calculate_path_length function calculates the total length of a given path by
summing the weights of the edges between consecutive vertices in the path.
6. What are the limitations of using the brute-force approach for TSP?
Answer: The brute-force approach is impractical for large graphs because the number of permutations
grows factorially with the number of vertices. It is only feasible for small instances due to its
exponential time complexity.
Answer: For larger instances of TSP, more efficient algorithms and heuristics are used, such as
dynamic programming (e.g., Held-Karp algorithm), branch and bound, genetic algorithms, and
approximation algorithms.
8. Can the Hamiltonian path approach be used to solve the TSP problem directly?
Answer: The Hamiltonian path approach is a step towards solving TSP, but the standard TSP usually
requires finding a Hamiltonian cycle (a path that returns to the starting point). The brute-force approach
provided here does not account for returning to the start city but can be adapted to find the Hamiltonian
cycle if required.
Answer: An adjacency matrix is a square matrix used to represent a graph, where the element at row
iii and column j represents the weight of the edge between vertex iii and vertex j. In the TSP
implementation, the adjacency matrix is used to look up the weight of edges when calculating the path
length.
Answer: TSP has practical applications in logistics (e.g., optimizing delivery routes), manufacturing
(e.g., minimizing tool movement), circuit design (e.g., designing electronic circuits), and various other
fields requiring optimization of routes or sequences.
11.1 Objectives:
Use python
to implement the Edmonds-Karp algorithm.
to calculate the maximum number of edge-disjoint paths from source to sink.
to find the flow values on edges.
class EdmondsKarp:
def __init__(self, n):
self.size = n
self.capacity = defaultdict(lambda: defaultdict(int))
while queue:
u = queue.popleft()
if u == sink:
return True
for v in self.graph[u]:
if v not in visited and self.capacity[u][v] > self.flow[u][v]:
parent[v] = u
visited.add(v)
queue.append(v)
if v == sink:
return True
return False
while s != source:
path_flow = min(path_flow, self.capacity[parent[s]][s] - self.flow[parent[s]][s])
s = parent[s]
v = sink
while v != source:
u = parent[v]
self.flow[u][v] += path_flow
self.flow[v][u] -= path_flow
v = parent[v]
max_flow += path_flow
# Example usage
if __name__ == "__main__":
n = 6 # Number of nodes
ek = EdmondsKarp(n) # Create an instance of EdmondsKarp
source = 0
sink = 5
max_edge_disjoint_paths = ek.edmonds_karp(source, sink)
print(f"The maximum number of edge-disjoint paths from source {source} to sink {sink} is
{max_edge_disjoint_paths}.")
OUTPUT:
The maximum number of edge-disjoint paths from source 0 to sink 5 is 2.
11.2.2 Find the maximum flow in a flow network using the Edmonds-
Karp algorithm.
import networkx as nx
Args:
- graph (nx.DiGraph): The directed graph with capacities as edge attributes.
- source (node): The source node.
- sink (node): The sink node.
Returns:
- max_flow (float): The maximum flow value from source to sink.
- flow_dict (dict): A dictionary with flow values for each edge.
"""
# Initialize flow to zero for all edges
flow_value, flow_dict = nx.maximum_flow(graph, source, sink,
flow_func=nx.algorithms.flow.edmonds_karp)
return flow_value, flow_dict
G.add_weighted_edges_from(edges, weight='capacity')
if __name__ == "__main__":
main()
OUTPUT:
Maximum Flow: 15
Flow values on edges:
Edge s -> a: Flow = 10, Capacity = 10
Edge s -> b: Flow = 5, Capacity = 5
Edge a -> b: Flow = 0, Capacity = 15
Edge a -> t: Flow = 10, Capacity = 10
Edge b -> t: Flow = 5, Capacity = 10
11.3 Exercise:
1. Write a program to optimize routing in transportation systems to ensure multiple independent
routes between key locations using Python.
2. Design networks to ensure multiple independent communication paths between key nodes for
reliability and fault tolerance using Python.
Answer: The algorithm repeatedly finds augmenting paths from the source to the sink using BFS. It
then updates the capacities of the edges along these paths and adds the flow to the total maximum flow.
This process continues until no more augmenting paths can be found.
Answer: The bfs function is used to find an augmenting path from the source to the sink in the residual
graph. It updates the parent array to keep track of the path and checks if a path exists.
Answer: The edmonds_karp function calculates the maximum flow from the source to the sink in the
flow network. It uses the bfs function to find augmenting paths and updates the capacities accordingly.
5. How is the maximum flow related to the maximum number of edge-disjoint paths?
Answer: The maximum number of edge-disjoint paths between two vertices in a network is equal to
the maximum flow from the source to the sink. This is due to the Max-Flow Min-Cut Theorem, which
states that the maximum flow in a network equals the capacity of the minimum cut separating the
source and sink.
Answer: The time complexity of the Edmonds-Karp algorithm is O(VE2), where V is the number of
vertices and E is the number of edges. This is due to the BFS operation running in O(E) time and being
performed O(V) times.
Answer: The path_flow variable represents the minimum capacity along the augmenting path found
by BFS. This value is used to update the capacities of the edges and to add to the total maximum flow.
Answer: A residual graph is a modified version of the original graph that reflects the remaining
capacities of the edges after some flow has been sent. It is used to find augmenting paths and update
edge capacities during the execution of the algorithm.
10. How can you modify the program to handle larger graphs or different types of inputs?
Answer: For larger graphs or different inputs, ensure the program is efficient in terms of memory and
processing. Consider using more advanced flow algorithms, such as the Dinic's algorithm, if
performance becomes an issue. Additionally, ensure that the capacity matrix is correctly formatted and
normalized for the given problem.
LAB 12: Using Menger’s theorem, find the minimum vertex cut
between source and target.
to find the minimum vertex cut between a source and target node in a graph
using Menger's theorem
The size of the minimum vertex cut between two vertices s and t in a graph is equal to the
maximum number of vertex-disjoint paths between s and t.
To find the minimum vertex cut using Menger’s theorem, we can follow these steps:
1. Convert the Graph: Create a flow network where each edge has capacity 1, and then use the
maximum flow algorithm to find the maximum number of vertex-disjoint paths.
2. Find the Maximum Flow: Use an algorithm like Edmonds-Karp (a specific implementation of
Ford-Fulkerson) to find the maximum flow in the network. This corresponds to the maximum
number of vertex-disjoint paths.
3. Derive the Minimum Vertex Cut: Once we have the maximum flow, the minimum vertex cut
can be derived from the residual flow network by identifying which vertices are reachable from
the source in the residual graph.
12.2.1 Find the minimum vertex cut between source and target
using Menger’s theorem.
import networkx as nx
Args:
- graph (nx.Graph): The undirected graph.
- source (node): The source node.
- target (node): The target node.
Returns:
- min_cut (set): The minimum vertex cut set.
"""
# Convert undirected graph to a directed graph with capacity 1 on all edges
flow_network = nx.DiGraph()
for u, v in graph.edges:
# The minimum vertex cut is the set of nodes in the reachable set excluding the
source
min_cut = set(reachable) - {source}
return min_cut
def main():
# Create an undirected graph
G = nx.Graph()
# Add edges
edges = [
('s', 'a'),
('s', 'b'),
('a', 'b'),
('a', 'c'),
('b', 'c'),
('b', 'd'),
('c', 'd'),
('c', 't'),
('d', 't')
]
G.add_edges_from(edges)
if __name__ == "__main__":
main( )
OUTPUT:
12.3 Exercise:
1. Determine the minimum number of nodes (e.g., routers, switches) that need to be removed to
disconnect a network using Python.
Viva Questions and Answers
Answer: Menger's theorem states that in a graph, the minimum number of vertices (or edges) that
need to be removed to disconnect a source vertex from a target vertex is equal to the maximum
number of vertex-disjoint (or edge-disjoint) paths between the source and target.
2. How can Menger's theorem be used to find the minimum vertex cut in a graph?
Answer: To find the minimum vertex cut using Menger’s theorem, you compute the maximum
number of vertex-disjoint paths between the source and target. This number is equal to the minimum
number of vertices that need to be removed to disconnect the source from the target.
3. What is the Edmonds-Karp algorithm, and how does it relate to Menger's theorem?
Answer: The bfs function finds an augmenting path from the source to the sink in the residual graph
using Breadth-First Search. It updates the parent array to track the path and determines if a path
exists.
Answer: The edmonds_karp function calculates the maximum flow from the source to the sink in the
flow network. It repeatedly finds augmenting paths and updates the capacities of the edges along
these paths.
6. How is the minimum vertex cut determined from the residual graph?
Answer: The minimum vertex cut is found by identifying vertices reachable from the source in the
residual graph but not reachable from the sink. The edges between these reachable and non-reachable
vertices form the minimum vertex cut.
Answer: A residual graph reflects the remaining capacities of the edges after some flow has been
sent. It is used to find augmenting paths and update edge capacities during the execution of the
maximum flow algorithm.
Answer: No, the algorithm assumes non-negative capacities. Negative capacities are not supported
because they complicate the notion of flow and capacity in the network.
10. How can the program be adapted for larger graphs or different types of inputs?
Answer: For larger graphs, ensure efficient memory usage and consider using advanced algorithms
or optimizations. For different types of inputs, validate and pre-process the capacity matrix to ensure
it is correctly formatted for the algorithm.