English | 中文版
[TOC]
A Graph
- Each graph edge is a pair
$(v, w)$ , where$v, w \in V$ . Edges are sometimes refered to as arcs. - A directed graph is acyclic if it has no cycles.
- A directed graph with this property is called strongly connected.
- If a directed graph is not strongly connected, but the underlying graph (without direction to the arcs) is connected, then the graph is said to be weakly connected.
- An undirected graph is connected if there is a path from every vertex to every other vertex.
- A path in a graph is a sequence of vertices
$w_1, w_2, w_3, ..., w_N$ such that$(w_i, w_{i+1}) \in E$ for$1 \leq i < N$ . - A cycle in a directed graph is a path of length at least 1 such that
$w_1 = w_N$ . - A complete graph is a graph in which there is an edge between every pair of vertices.
A finite graph is a graph with a finite number of vertices and edges. In other words, both the number of vertices and the number of edges in a finite graph are limited and can be counted.
A graph is called an infinite graph if it has an infinite number of vertices and an infinite number of edges.
A finite graph is said to be trivial if it contains only one vertex and no edges. It is also known as a singleton graph or a single-vertex graph.
A simple graph is a graph that does not contain more than one edge between any pair of vertices.
Any graph that contains some parallel edges but doesn’t contain any self-loops is called a multigraph.
A graph of order n and size zero is a graph where there are only isolated vertices with no edges connecting any pair of vertices. A null graph is a graph with no edges.
A simple graph with n vertices is called a complete graph if the degree of each vertex is n-1, that is, one vertex is attached with n-1 edges or the rest of the vertices in the graph. A complete graph is also called Full Graph.
A directed graph is a graph where the edges have a direction associated with them. Directed graphs are sometimes referred to as digraphs.
An undirected graph is a graph where edges do not have a specific direction, meaning connections go both ways. If two places are connected, you can travel in either direction.
A weighted graph is a graph where each edge has a number (weight) that represents distance, cost, or time. These graphs help find the shortest or cheapest paths.
An unweighted graph is a graph where all edges are treated equally, with no extra values like distance or cost. It simply shows connections between points.
A pseudograph is a type of graph that allows for the existence of self-loops (edges that connect a vertex to itself) and multiple edges (more than one edge connecting two vertices).
A regular graph is a type of undirected graph in which every vertex has the same number of edges (or neighbors). In other words, all vertices in a regular graph have the same degree.
A sparse graph is a type of graph with relatively few edges compared to the number of vertices.
A dense graph is a type of graph with many edges compared to the number of vertices.
A graph G consisting of n vertices and n> = 3 that is V1, V2, V3... Vn and edges (V1, V2), (V2, V3), (V3, V4)... (Vn, V1) are called cyclic graph.
Graph is said to be connected if there exists at least one path between each and every pair of vertices in graph G, otherwise, it is disconnected.
A graph is said to be Biconnected if:
- It is connected, i.e. it is possible to reach every vertex from every other vertex, by a simple path.
- Even after removing any vertex the graph remains connected.
For each
For each vertex, we keep a list of all adjacent vertices. The space requirement is then
Breadth-first search is a graph traversal algorithm that starts from a source node and explores the graph level by level. First, it visits all nodes directly adjacent to the source. Then, it moves on to visit the adjacent nodes of those nodes, and this process continues until all reachable nodes are visited.
Algorithm: $$ \begin{align} & BFS(G, s) \ & for\ each\ vertex\ u \in G.V - {s} \ & \qquad u.color = WHITE \ & \qquad u.d = \infty \ & \qquad u.\pi = NIL \ & s.color = GRAY \ & s.d = 0 \ & s.\pi = NIL \ & Q = \phi \ & ENQUEUE(Q, s) \ & while\ Q \neq \phi \ & u = DEQUEUE(Q) \ & for\ each\ u \in G.Adj[u] \ & \qquad if\ u.color == WHITE \ & \qquad \qquad u.color = GRAY \ & \qquad \qquad u.d = u.d + 1 \ & \qquad \qquad u.\pi = u \ & \qquad \qquad ENQUEUE(Q, u) \ & u.color = BLACK \end{align} $$ Examples:
Implements:
std::vector<int> bfs(std::vector<std::vector<int>>& arr)
{
int v = arr.size();
std::vector<bool> visited(arr.size(), false);
std::vector<int> ret;
std::queue<int> q;
int src = 0;
visited[src] = true;
q.push(src);
while (!q.empty())
{
int curr = q.front();
q.pop();
ret.push_back(curr);
for (int x : arr[curr])
{
if (!visited[x])
{
visited[x] = true;
q.push(x);
}
}
}
return ret;
}Complexity:
| Scenario | Time Complexity | Space Complexity |
|---|---|---|
| Best Case | $O( | V |
| Average Case | $O( | V |
| Worst Case | $O( | V |
For the adjacency-list implementation above, each reachable vertex is enqueued and dequeued at most once, and each reachable edge is examined at most once, giving traversal cost visited array is initialized for all vertices, so even when BFS quickly finishes (for example, source with no outgoing edges), total time is still visited, queue, and output storage.
Lemma Let
Lemma Let
Lemma Suppose that during the execution of BFS on a graph
Corollary Suppose that vertices
Theorem (Correctness of breadth-first search) Let
For a graph
Lemma When applied to a directed or undirected graph
In Depth First Search (or DFS) for a graph, we traverse all adjacent vertices one by one. When we traverse an adjacent vertex, we completely finish the traversal of all vertices reachable through that adjacent vertex.
Algorithms:
Examples:
Implement:
void dfs(std::vector<std::vector<int>>& arr,
std::vector<bool>& visited,
int s,
std::vector<int>& ret)
{
visited[s] = true;
ret.push_back(s);
for (int i : arr[s])
if (visited[i] == false)
dfs(arr, visited, i, ret);
}
std::vector<int> dfs(std::vector<std::vector<int>>& arr)
{
std::vector<bool> visited(arr.size(), false);
std::vector<int> ret;
for (int i = 0; i < arr.size(); i++)
{
if (visited[i] == false)
dfs(arr, visited, i, ret);
}
return ret;
}Complexity:
| Scenario | Time Complexity | Space Complexity |
|---|---|---|
| Best Case | $O( | V |
| Average Case | $O( | V |
| Worst Case | $O( | V |
For the adjacency-list implementation above, the outer loop guarantees all vertices are considered, and each vertex is visited at most once. Across the full traversal, each edge is examined at most once in directed graphs (or twice in undirected graphs, once per endpoint), so total time is visited plus recursion call stack (up to
We define the predecessor subgraph of a depth-first search slightly differently from that of a breadth-first search: we let
We can define four edge types in terms of the depth-first forest
-
Tree edges are edges in the depth-first forest
$G_{\pi}$ . Edge$(u, v)$ is a tree edge if$v$ was first discovered by exploring edge$(u, v)$. -
Back edges are those edges$(u, v)$ connecting a vertex
$u$ to an ancestor$v$ in a depth-first tree. We consider self-loops, which may occur in directed graphs, to be back edges. -
Forward edges are those nontree edges$(u, v)$ connecting a vertex
$u$ to a descendant$v$ in a depth-first tree. - Cross edges are all other edges. They can go between vertices in the same depth-first tree, as long as one vertex is not an ancestor of the other, or they can go between vertices in different depth-first trees.
Theorem (Parenthesis theorem) In any depth-first search of a (directed or undirected) graph
- the intervals
$[u.d, u.f]$ and$[v.d, v.f]$ are entirely disjoint, and neither$u$ nor$v$ is a descendant of the other in the depth-first forest, - the interval
$[u.d, u.f]$ is contained entirely within the interval$[v.d, v.f]$ , and$u$ is a descendant of$v$ in a depth-first tree, or - the interval
$[v.d, v.f]$ is contained entirely within the interval$[u.d, u.f]$ , and$v$ is a descendant of$u$ in a depth-first tree.
Corollary (Nesting of descendants' intervals) Vertex
Theorem (White-path theorem) In a depth-first forest of a (directed or undirected) graph
Theorem In a depth-first search of an undirected graph
A topological sort is an ordering of vertices in a directed acyclic graph, such that if there is a path from
Examples:
Lemma A directed graph
Theorem TOPOLOGICAL-SORT produces a topological sort of the directed acyclic graph provided as its input.
Topological sorting for a Directed Acyclic Graph (DAG) is a linear ordering of vertices such that for every directed edge uv, vertex u comes before v in the ordering. Topological Sorting for a graph is not possible if the graph is not a DAG.
Example:
Implement:
int topological_sort_dfs(Graph* graph, int v, int* state, int* stack, int* top)
{
Node* cur;
state[v] = 1;
for (cur = graph->adj[v]; cur != NULL; cur = cur->next)
{
int to = cur->vertex;
if (state[to] == 1)
return 1;
if (state[to] == 0)
if (topological_sort_dfs(graph, to, state, stack, top))
return 1;
}
state[v] = 2;
stack[(*top)++] = v;
return 0;
}
void topological_sort(Graph* graph)
{
int i;
int* state = (int*)calloc((size_t)graph->V, sizeof(int));
int* stack = (int*)malloc((size_t)graph->V * sizeof(int));
int top = 0;
if (!state || !stack)
{
free(state);
free(stack);
return;
}
for (i = 0; i < graph->V; ++i)
{
if (state[i] != 0)
continue;
if (topological_sort_dfs(graph, i, state, stack, &top))
{
free(state);
free(stack);
return;
}
}
free(state);
free(stack);
}Complexity:
| Scenario | Time Complexity | Space Complexity |
|---|---|---|
| Best Case | $O( | V |
| Average Case | $O( | V |
| Worst Case | $O( | V |
For the DFS-based implementation above (adjacency list), each vertex is colored at most once and each edge is explored at most once, so full traversal costs state and stack arrays of size
Informally, a minimum spanning tree of an undirected graph G is a tree formed from graph edges that connects all the vertices of G at the lowest total cost. A minimum spanning tree exists if and only if G is connected.
graph G and it's mimimum spanning trees
Prim’s algorithm is a Greedy algorithm like Kruskal's algorithm. This algorithm always starts with a single node and moves through several adjacent nodes, in order to explore all of the connected edges along the way.
Algorithm:
- The algorithm starts with an empty spanning tree.
- The idea is to maintain two sets of vertices. The first set contains the vertices already included in the MST, and the other set contains the vertices not yet included.
- At every step, it considers all the edges that connect the two sets and picks the minimum-weight edge from these edges. After picking the edge, it moves the other endpoint of the edge to the set containing the MST.
Example:
Implement:
// A utility function to find the vertex with
// minimum key value, from the set of vertices
// not yet included in MST
int min_key(vector<int> &key, vector<bool> &mst_set)
{
// Initialize min value
int min = INT_MAX, min_index;
for (int v = 0; v < mst_set.size(); v++)
if (mst_set[v] == false && key[v] < min)
min = key[v], min_index = v;
return min_index;
}
// Function to construct and print MST for
// a graph represented using adjacency
// matrix representation
void prim_mst(vector<vector<int>> &graph)
{
int V = graph.size();
// Array to store constructed MST
vector<int> parent(V);
// Key values used to pick minimum weight edge in cut
vector<int> key(V);
// To represent set of vertices included in MST
vector<bool> mst_set(V);
// Initialize all keys as INFINITE
for (int i = 0; i < V; i++)
key[i] = INT_MAX, mst_set[i] = false;
// Always include first 1st vertex in MST.
// Make key 0 so that this vertex is picked as first
// vertex.
key[0] = 0;
// First node is always root of MST
parent[0] = -1;
// The MST will have V vertices
for (int count = 0; count < V - 1; count++)
{
// Pick the minimum key vertex from the
// set of vertices not yet included in MST
int u = min_key(key, mst_set);
// Add the picked vertex to the MST Set
mst_set[u] = true;
// Update key value and parent index of
// the adjacent vertices of the picked vertex.
// Consider only those vertices which are not
// yet included in MST
for (int v = 0; v < V; v++)
// graph[u][v] is non zero only for adjacent
// vertices of m mst_set[v] is false for vertices
// not yet included in MST Update the key only
// if graph[u][v] is smaller than key[v]
if (graph[u][v] && mst_set[v] == false && graph[u][v] < key[v])
parent[v] = u, key[v] = graph[u][v];
}
// Print the constructed MST
print_mst(parent, graph);
}Complexity:
| Scenario | Time Complexity | Space Complexity |
|---|---|---|
| Best Case | $O( | V |
| Average Case | $O( | V |
| Worst Case | $O( | V |
For this adjacency-matrix implementation, min_key scans all vertices in parent, key, and mst_set (excluding the input graph matrix).
A minimum spanning tree (MST) or minimum weight spanning tree for a weighted, connected, and undirected graph is a spanning tree (no cycles and connects all vertices) that has minimum weight. The weight of a spanning tree is the sum of all edges in the tree.
Algorithm:
- Sort all the edges in a non-decreasing order of their weight.
- Pick the smallest edge. Check if it forms a cycle with the spanning tree formed so far. If the cycle is not formed, include this edge. Otherwise, discard it. It uses the Disjoint Sets to detect cycles.
- Repeat step 2 until there are (V-1) edges in the spanning tree.
Example:
Implement:
bool comparator(std::vector<int> &a,std::vector<int> &b)
{
return a[2] < b[2];
}
int find(int i, std::vector<int> &parent)
{
return (parent[i] == i) ? i : (parent[i] = find(parent[i], parent));
}
void unite(int x, int y, std::vector<int> &parent, std::vector<int> &rank)
{
int s1 = find(x, parent), s2 = find(y, parent);
if (s1 == s2)
return;
if (rank[s1] < rank[s2])
parent[s1] = s2;
else if (rank[s1] > rank[s2])
parent[s2] = s1;
else
parent[s2] = s1;
rank[s1]++;
}
int kruskals_mst(int V, std::vector<std::vector<int>> &edges)
{
std::vector<int> parent, rank;
parent.resize(V);
rank.resize(V);
for (int i = 0; i < V; i++)
{
parent[i] = i;
rank[i] = 1;
}
// Sort all edges
std::sort(edges.begin(), edges.end(), comparator);
// Traverse edges in sorted order
int cost = 0, count = 0;
for (auto &e : edges)
{
int x = e[0], y = e[1], w = e[2];
// Make sure that there is no cycle
if (find(x, parent) == find(y, parent))
continue;
unite(x, y, parent, rank);
cost += w;
if (++count == V - 1)
break;
}
return cost;
}Complexity:
| Scenario | Time Complexity | Space Complexity |
|---|---|---|
| Best Case | $O( | E |
| Average Case | $O( | E |
| Worst Case | $O( | E |
This implementation always sorts all edges first, which costs find with path compression and unite with rank) are nearly constant amortized per edge, so traversal after sorting is parent and rank (excluding the input edge list).
Algorithms:
In DFS, we go as deep as possible from a starting node. If during this process, we reach a node that we’ve already visited in the same DFS path, it means we’ve gone back to an ancestor — this shows a cycle exists.
Examples:
Implement:
// Utility DFS function to detect cycle in a directed graph
bool is_cycle_by_dfs_util(
vector<vector<int>>& adj,
int u,
vector<bool>& visited,
vector<bool>& rec_stack)
{
// node is already in recursion stack cycle found
if (rec_stack[u]) return true;
// already processed no need to visit again
if (visited[u]) return false;
visited[u] = true;
rec_stack[u] = true;
// Recur for all adjacent nodes
for (int v : adj[u])
if (is_cycle_by_dfs_util(adj, v, visited, rec_stack))
return true;
// remove from recursion stack before backtracking
rec_stack[u] = false;
return false;
}
// Function to detect cycle in a directed graph
bool is_cycle_by_dfs(vector<vector<int>>& adj)
{
int V = adj.size();
vector<bool> visited(V, false);
vector<bool> rec_stack(V, false);
// Run DFS from every unvisited node
for (int i = 0; i < V; i++)
if (!visited[i] && is_cycle_by_dfs_util(adj, i, visited, rec_stack))
return true;
return false;
}Complexity:
| Scenario | Time Complexity | Space Complexity |
|---|---|---|
| Best Case | $O( | V |
| Average Case | $O( | V |
| Worst Case | $O( | V |
In this DFS-based cycle detection (adjacency list), each vertex is marked at most once and each directed edge is explored at most once before completion, so the full traversal cost is visited and rec_stack use
Algorithm:
When we start a DFS from a node, we visit all its connected neighbors one by one. If during this traversal, we reach a node that has already been visited before, it indicates that there might be a cycle, since we’ve come back to a previously explored vertex.
Example:
Implement:
bool dfs(int v, vector<vector<int>> &adj, vector<bool> &visited, int parent)
{
// Mark the current node as visited
visited[v] = true;
// Recur for all the vertices adjacent to this vertex
for (int i : adj[v])
{
// If an adjacent vertex is not visited,
//then recur for that adjacent
if (!visited[i])
{
if (dfs(i, adj, visited, v))
return true;
}
else if (i != parent)
{
// If an adjacent vertex is visited and is not
// parent of current vertex,
// then there exists a cycle in the graph.
return true;
}
}
return false;
}
// Returns true if the graph contains a cycle, else false.
bool is_cycle(vector<vector<int>> &adj)
{
int V= adj.size();
// Mark all the vertices as not visited
vector<bool> visited(V, false);
for (int u = 0; u < V; u++)
{
if (!visited[u])
{
if (dfs(u, adj, visited, -1))
return true;
}
}
return false;
}Complexity:
| Scenario | Time Complexity | Space Complexity |
|---|---|---|
| Best Case | $O( | V |
| Average Case | $O( | V |
| Worst Case | $O( | V |
In this DFS-based undirected cycle check (adjacency list), each vertex is visited at most once and each undirected edge is examined at most twice (once from each endpoint), so full traversal is visited array requires visited is initialized for all vertices, best-case time remains
Algorithm:
- Initialize a distance array
distwith all values as0 - Perform edge relaxation n - 1) times:
- For each edge
(u, v, wt) - If
dist[u] + wt < dist[v], updatedist[v]
- For each edge
- Run one more iteration over all edges:
- If any edge still relaxes → return
1(negative cycle exists)
- If any edge still relaxes → return
- If no relaxation happens → return
0
Implement:
bool is_negative_cycle(int n, vector<vector<int>> &edges)
{
vector<int> dist(n, 0);
// Relax edges n-1 times
for (int i = 0; i < n - 1; i++)
{
for (auto edge : edges)
{
int u = edge[0];
int v = edge[1];
int wt = edge[2];
if (dist[u] + wt < dist[v])
{
dist[v] = dist[u] + wt;
}
}
}
// Check for negative cycle
for (auto edge : edges)
{
int u = edge[0];
int v = edge[1];
int wt = edge[2];
if (dist[u] + wt < dist[v])
{
// negative cycle found
return true;
}
}
return false;
}Complexity:
| Scenario | Time Complexity | Space Complexity |
|---|---|---|
| Best Case | $O( | V |
| Average Case | $O( | V |
| Worst Case | $O( | V |
This implementation performs exactly dist array.
| Parameters | BFS | DFS |
|---|---|---|
| Stands for | BFS stands for Breadth First Search. | DFS stands for Depth First Search. |
| Data Structure | BFS(Breadth First Search) uses Queue data structure for finding the shortest path. | DFS(Depth First Search) uses Stack data structure. |
| Definition | BFS is a traversal approach in which we first walk through all nodes on the same level before moving on to the next level. | DFS is also a traversal approach in which the traverse begins at the root node and proceeds through the nodes as far as possible until we reach the node with no unvisited nearby nodes. |
| Conceptual Difference | BFS builds the tree level by level. | DFS builds the tree sub-tree by sub-tree. |
| Approach used | It works on the concept of FIFO (First In First Out). | It works on the concept of LIFO (Last In First Out). |
| Suitable for | BFS is more suitable for searching vertices closer to the given source. | DFS is more suitable when there are solutions away from source. |
| Applications | BFS is used in various applications such as bipartite graphs, shortest paths, etc. If weight of every edge is same, then BFS gives shortest path from source to every other vertex. | DFS is used in various applications such as acyclic graphs and finding strongly connected components etc. There are many applications where both BFS and DFS can be used like Topological Sorting, Cycle Detection, etc. |
| Feature | Prim's Algorithm | Kruskal's Algorithm |
|---|---|---|
| Approach | Vertex-based, grows the MST one vertex at a time | Edge-based, adds edges in increasing order of weight |
| Data Structure | Priority queue (min-heap) | Union-Find data structure |
| Graph Representation | Adjacency matrix or adjacency list | Edge list |
| Initialization | Starts from an arbitrary vertex | Starts with all vertices as separate trees (forest) |
| Edge Selection | Chooses the minimum weight edge from the connected vertices | Chooses the minimum weight edge from all edges |
| Cycle Management | Not explicitly managed; grows connected component | Uses Union-Find to avoid cycles |
| Complexity | O(V^2) for adjacency matrix, O((E + V) log V) with a priority queue | O(E log E) or O(E log V), due to edge sorting |
| Suitable for | Dense graphs | Sparse graphs |
| Implementation Complexity | Relatively simpler in dense graphs | More complex due to cycle management |
| Parallelism | Difficult to parallelize | Easier to parallelize edge sorting and union operations |
| Memory Usage | More memory for priority queue | Less memory if edges can be sorted externally |
| Example Use Cases | Network design, clustering with dense connections | Road networks, telecommunications with sparse connections |
| Starting Point | Requires a starting vertex | No specific starting point, operates on global edges |
| Optimal for | Dense graphs where the adjacency list is used | Sparse graphs, where the edge list is efficient |
[1] Thomas H.Cormen; Charles E.Leiserson; Ronald L. Rivest; Clifford Stein. Introduction to Algorithms. 3ED
[2] Mark Allen Weiss. Data Structures and Algorithm Analysis in C++. 4ED
[3] Graph Algorithms
[6] Types of Graphs with Examples
[7] Prim’s Algorithm for Minimum Spanning Tree (MST)
[8] Difference between Prim's and Kruskal's algorithm for MST













































































