In this assignment, we will implement some graph algorithms using C++.
we have two main classes in this assignment:
- Graph
- Algorithms
we will explain each class in the following sections.
One of the assignment goals is to use namespace
in the code, so we will use the shayg
namespace for wrapping the classes and functions in this assignment.
This is a class that represents a graph. The graph can be directed or undirected, weighted or unweighted and with positive or negative weights.
All the graph don't have self-loops and multiple edges.
To Represent the graph, we will use an adjacency matrix.
each graph object saves the following properties:
- isDirected : a boolean that represents if the graph is directed or not.
- isWeighted : a boolean that represents if the graph is weighted or not.
- haveNegativeEdgeWeight : a boolean that represents if the graph have negative edge weights or not.
To initialize a Graph
object, you can use the following code:
Graph g;
a graph with symmetric adjacency matrix will be undirected, and a graph with asymmetric adjacency matrix will be directed.
for example, the following code:
Graph g;
g.loadGraph({{0, 1}, {1, 0}}); // g is undirected graph
Graph g;
g.loadGraph({{0, 1}, {0, 0}}); // g is directed graph
We define the constants INF
and NO_EDGE
to represent the infinity and no edge in the graph.
INF
=std::numeric_limits<int>::max()
=2147483647
(so make sure not to doINF + INF
so you don't get overflow)NO_EDGE
=0
, but can be any other value that is not used in the graph.
This function will initialize the graph with the given adjacency matrix.
it will check for valid input, and initialize the graph properties.
if the input is invalid, the function will throw an invalid_argument
exception.
print information about the graph,in the following format: Directed/Undirected graph with |V| vertices and |E| edges.
where
Note: This graph, contains one edge and 2 vertices.
graph LR;
A --- B
This class have only static functions that perform some algorithms on the graph.
A helper enum that represents the color of the vertices in the graph. it can be WHITE
, GRAY
, BLACK
, BLUE
or RED
.
The WHITE
, GRAY
and BLACK
colors are used in the DFS algorithm, and the BLUE
and RED
colors are used in the bipartite algorithm.
This is a custom exception class that will be thrown when the graph contains a negative cycle, in the Bellman-Ford algorithm.
we find a negative cycle in the graph if the Bellman-Ford algorithm we can relax the edges in the last iteration. when we find that edge, we will throw this exception, we the vertex and the parents array to construct the negative cycle. The constructor of this class will take the vertex and the parents array, and build the cycle path.
In this function, we will use the DFS algorithm to check if the graph is connected or not.
to check if undirected graph is connected, we can perform DFS on the graph and check if all the vertices are discovered.
The way to check if a directed graph is connected is transformed DFS twice:
- Perform DFS on the graph.
- If the DFS discovers all the vertices, then the graph is connected. (if we got only one DFS tree)
- Perform DFS on the root of the last DFS tree.
- If the DFS discovers all the vertices, then the graph is connected. otherwise, the graph is not connected.
In this function, we will use the one of 3 algorithms to find the shortest path between two vertices in a graph.
- if the graph is unweighted, we will use the BFS algorithm to find the shortest path between two vertices.
- if the graph is weighted and the weights are positive, we will use the Dijkstra algorithm to find the shortest path between two vertices.
- if the graph is weighted and the weights are negative, we will use the Bellman-Ford algorithm to find the shortest path between two vertices.
Note: we represent the graph as an adjacency matrix, so both Dijkstra and Bellman-Ford algorithms run in
$O(V^3)$ time complexity.
If there is no path between the two vertices, the function will return "-1".
In this function we check if the graph contains a cycle or not. If the graph contains a cycle, the function will return one of the cycles in this format: v1->v2->v3->...->v1
. otherwise, the function will return "-1".
for this function, we will use two helper functions:
isContainsCycleUtil
: this function is slightly modified version of the DFS algorithm. it runs until it finds a back edge in the graph, when it finds a back edge, it will call theconstructCyclePath
function to construct the cycle path.constructCyclePath
: this function will construct the cycle path by backtracking the path array. A graph contains a cycle if there is a back edge in the graph. so we can use the DFS algorithm to check if the graph contains a cycle or not.
In an undirected graph, a cycle needs at least 3 vertices. so the next graph DON'T contain a cycle.
graph LR;
A --- B
In this function, we will use the BFS algorithm to check if the graph is bipartite or not. A graph is bipartite iff it is 2-colorable. so we can use the BFS algorithm to check if the graph is bipartite or not.
For directed graphs, we need to convert the directed graph to an undirected graph, because we don't care about the direction of the edges in this function (and the weights).
If the graph is bipartite, the function will return any to sets of vertices that represent a bipartite graph. otherwise, the function will return "The graph is not bipartite"
.
The returned format will be: "The graph is bipartite: A={...}, B={...}"
This function will use the Bellman-Ford algorithm to check if the graph contains a negative cycle or not. If the graph contains a negative cycle, the function will return one of the negative cycles in this format: v1->v2->v3->...->v1
. otherwise, the function will return "No negative cycle"
The way we can find a negative cycle in the graph is to add new vertex s
and connect it with all the vertices in the graph with weight 0, and for each s
. if the Bellman-Ford algorithm finds a negative cycle, then the graph contains a negative cycle.
Note: the Bellman-Ford algorithm DON'T work with undirected graphs with negative weights.
in assignment 2, we will implement operators for the graph class.
to change the graph value, we add to methods to the graph class (same name, different parameters):
void modifyEdgeWeights(const function<int(int)>& func)
void modifyEdgeWeights(const Graph& other, const function<int(int, int)>& func)
we can pass to the method a function that will change the value of the edge in the graph, if we pass another graph, we can pass a function that will change the value of the of the current edge with the value of the two edges of the two graphs.
we overload the following operators:
+
: unary +, binary +, +=, prefix ++, postfix ++-
: unary -, binary -, -=, prefix --, postfix --*
:Graph * int
,Graph * Graph
,*=
(for both)/
:Graph / int
,/=
we have this two definitions for the comparison operators: let G1 and G2 be two graphs, and A and B be the adjacency matrices of G1 and G2 respectively.
- G1 = G2 if A = B, or if not G1 < G2 and not G2 < G1
- G1 < G2 if A ⊂ B, or (A ⊄ B and B ⊄ A and |E(G1)| < |E(G2)|) or (A ⊄ B and B ⊄ A and |E(G1)| = |E(G2)| and |V(G1)| < |V(G2)|)
with this definitions, we can use the following operators:
==
,!=
<
,<=
,>
,>=
we diffine the <<
operator to print the graph in the following format:
[X, 1, 1],
[1, X, 1],
[1, 1, X]
where X
represent the NO_EDGE
value.
I wrote a full README file for the test, you can find it here
you can see how to use the code it the test files, or in the main.cpp file.
you can clone the repository and run the following commands:
- build the code:
make all
- run the code:
- you can use make run to run the code:
- or you can build the code and run it manually:
run the following command for building and running the code:
make run
run the following commands for building and running the code manually:
make all
./main
- test the code:
make test
- check for memory leaks with valgrind: (make sure that you have valgrind installed)
make valgrind
- check for code style:
make tidy
- clean the code:
make clean