Kruskals Algorithm In C++ - C++ Programming Tutorial
C++ Course / Graph Algorithms / Kruskals Algorithm In C++

Kruskals Algorithm In C++

BLUF: Mastering Kruskals Algorithm In C++ is a critical step in becoming a proficient C++ developer. This lesson provides a deep dive into the syntax, performance considerations, and real-world applications of this concept.
Key Performance Insight: Kruskals Algorithm In C++

C++ is renowned for its efficiency. Learn how Kruskals Algorithm In C++ enables low-level control and high-performance computing in the tutorial below.

Trees play a crucial role in the realm of computer science and data structures by efficiently arranging and overseeing data. In practical scenarios, trees serve as hierarchical arrangements that represent diverse connections and hierarchies. Due to their significance in algorithms and data processing, trees are considered fundamental in computer science. This article will delve into the basic concepts, different types, and practical applications of trees in data structures.

History

Kruskal's algorithm plays a vital role in graph theory and computer science by efficiently determining a minimum spanning tree within a connected, undirected graph. This tree comprises a selection of edges from the graph that create a connected tree with the least total edge weight. Joseph Kruskal introduced this algorithm in 1956 during his academic tenure at Harvard University.

The origins of the algorithm can be dated back to the middle of the 20th century during the early stages of computer science. Joseph Kruskal, under the guidance of Samuel Winograd, delved into enhancing network structures, sparking the birth of the algorithm. Kruskal's endeavors were shaped by prior investigations on minimum spanning trees, notably inspired by the contributions of Czech mathematician Otakar Bor?vka.

Kruskal's algorithm represents a methodical strategy for identifying the minimal spanning tree. It commences with a void collection of edges and progressively incorporates edges into the collection, maintaining a cycle-free condition. The edges are introduced based on their weights in ascending sequence, with only those that avoid generating cycles within the expanding forest being incorporated. This sequence persists until all nodes are linked, producing a minimum spanning tree.

The straightforwardness and effectiveness of the algorithm have established it as a fundamental aspect within the realm of network layout and enhancement. Its time complexity is O(E log E), with E representing the quantity of edges within the graph, rendering it suitable for numerous real-world scenarios. Kruskal's algorithm finds extensive application in computer networking, urban transportation strategies, and diverse fields where creating minimum spanning trees efficiently is crucial.

Kruskal's algorithm represents a significant advancement in the realm of computer science and graph theory, originating in the mid-1900s. Its sophisticated and effective method for identifying minimum spanning trees has established it as a fundamental technique for addressing a diverse range of practical challenges. This enduring impact extends to shaping algorithmic strategies and enhancing efficiency across multiple domains.

Understanding the Basics

Let's establish a few basic ideas before delving into the details of trees:

  • Nodes: Nodes are the basic constituent parts of trees. Each node has data and 0-node(s) child(ren) nodes. The top node is known as the "root," whereas nodes without offspring are known as "leaves."
  • Edges: In a tree, edges bind the nodes together. They show which nodes are parent and child nodes and the relationships between the nodes.
  • Hierarchy: Trees are hierarchical structures, which mean they adhere to a predetermined hierarchy. With the root node at the top and several layers of child nodes below it, nodes are arranged in a hierarchy.
  • Types of Trees

Different types of trees exist, each created to serve a particular function or address a particular issue. Typical tree species include:

  • Binary Tree: Also known as the left child and the right child, a binary tree allows each node to have a maximum of two offspring. Binary search trees and expression trees are only two examples of the many uses for binary trees.
  • Binary Search Tree (BST): A binary search tree is a kind of binary tree in which the nodes are arranged so that searching, insertion, and deletion operations may be carried out quickly. Nodes to the left and right of a parent node have values that are more and less than the parent, respectively.
  • AVL Tree: An AVL tree is a binary search tree that self-balances. By guaranteeing that every node's left and right subtrees are at most one height apart, it keeps the structure balanced. This harmony makes searching activities effective.
  • Red-Black Tree: Another self-balancing binary search tree is the red-black tree. It maintains a balanced structure using a set of principles, making it appropriate for a variety of applications.
  • B-Tree: Multi-way trees known as B-trees are frequently utilized in file systems and databases. Through the maintenance of a balanced structure with a variable number of child nodes, they offer effective data storage and retrieval.
  • Trie: A trie (pronounced "try") is a data structure that resembles a tree and is used to store and search a dynamic set of strings, such as words in a dictionary or IP addresses in a router.
  • Practical Applications

Let's look at some actual uses for trees in the real world now that we've looked at their fundamental principles and varieties:

  • File Systems: To organize files and directories, file systems, like the one on your computer, frequently employ tree topologies. A hierarchical structure may be formed by each directory's ability to house files and subdirectories.
  • Database Systems: To store and retrieve data effectively, many database systems employ B-trees and other tree architectures. Even for huge datasets, these structures provide rapid access to the data.
  • Network Routing: To choose the best route for data packets to go from their source to their destination, routers in computer networks employ tree-based algorithms.
  • Abstract Syntax Trees (ASTs) are used by compilers to represent the structure of program code. ASTs are essential for code generation and parsing.
  • Organizational Hierarchies: In a firm, organizational hierarchies may be represented as trees, with each node denoting a department or employee and their corresponding hierarchical connections.
  • Game production: Trees are used to organize game elements, their interactions, and their behaviors in video game production. Behavior trees are a typical illustration of this use.
  • Understanding Greedy Algorithms: Optimizing Choices Step by Step

In the field of computer science and optimization, a notable and straightforward approach that frequently stands out is the Greedy Algorithm. Greedy algorithms are flexible methodologies for problem-solving that choose the best option at each stage to optimize a particular objective function. Despite their uncomplicated nature, these algorithms can prove to be extremely efficient across various scenarios. This guide will provide an in-depth look into the realm of greedy algorithms, examining their fundamental concepts, benefits, and constraints.

What is a Greedy Algorithm?

Fundamentally, a greedy algorithm functions by making sequential choices that seem optimal at each stage in order to achieve an optimal solution overall. It functions by picking the most favorable alternative at each juncture without taking into account the broader ramifications. This short-sighted strategy is akin to an individual who repeatedly opts for the most favorable choice available in the moment, with the expectation of attaining the most favorable end result.

Key Characteristics of Greedy Algorithms

  1. Greedy Choice Property

The core characteristic of a greedy algorithm lies in its "greedy choice property." In each iteration, the algorithm picks the option that seems most favorable at that instant, without considering the overall scheme. This decision is guided by a particular criterion, which could entail maximizing or minimizing a specific value.

  1. Optimal Substructure

Greedy algorithms depend on the idea of "optimal substructure," which implies that finding an optimal solution to a smaller subproblem aids in finding an optimal solution to the overall problem. This suggests that the main problem can be broken down into smaller, solvable subproblems that can also be tackled effectively using the same greedy strategy.

Here is an example of Kruskal's Algorithm coded in C++:

Example

// C++ program for Kruskal's algorithm to find Minimum 
// Spanning Tree of a given connected, undirected and 
// weighted graph 
#include<bits/stdc++.h> 
using namespace std; 

// Creating shortcut for an integer pair 
typedef pair<int, int> iPair; 

// Structure to represent a graph 
struct Graph 
{ 
	int V, E; 
	vector< pair<int, iPair> > edges; 

	// Constructor 
	Graph(int V, int E) 
	{ 
		this->V = V; 
		this->E = E; 
	} 

	// Utility function to add an edge 
	void addEdge(int u, int v, int w) 
	{ 
		edges.push_back({w, {u, v}}); 
	} 

	// Function to find MST using Kruskal's 
	// MST algorithm 
	int kruskalMST(); 
}; 

// To represent Disjoint Sets 
struct DisjointSets 
{ 
	int *parent, *rnk; 
	int n; 

	// Constructor. 
	DisjointSets(int n) 
	{ 
		// Allocate memory 
		this->n = n; 
		parent = new int[n+1]; 
		rnk = new int[n+1]; 

		// Initially, all vertices are in 
		// different sets and have rank 0. 
		for (int i = 0; i <= n; i++) 
		{ 
			rnk[i] = 0; 

			//every element is parent of itself 
			parent[i] = i; 
		} 
	} 

	// Find the parent of a node 'u' 
	// Path Compression 
	int find(int u) 
	{ 
		/* Make the parent of the nodes in the path 
		from u--> parent[u] point to parent[u] */
		if (u != parent[u]) 
			parent[u] = find(parent[u]); 
		return parent[u]; 
	} 

	// Union by rank 
	void merge(int x, int y) 
	{ 
		x = find(x), y = find(y); 

		/* Make tree with smaller height 
		a subtree of the other tree */
		if (rnk[x] > rnk[y]) 
			parent[y] = x; 
		else // If rnk[x] <= rnk[y] 
			parent[x] = y; 

		if (rnk[x] == rnk[y]) 
			rnk[y]++; 
	} 
}; 

/* Functions returns weight of the MST*/

int Graph::kruskalMST() 
{ 
	int mst_wt = 0; // Initialize result 

	// Sort edges in increasing order on basis of cost 
	sort(edges.begin(), edges.end()); 

	// Create disjoint sets 
	DisjointSets ds(V); 

	// Iterate through all sorted edges 
	vector< pair<int, iPair> >::iterator it; 
	for (it=edges.begin(); it!=edges.end(); it++) 
	{ 
		int u = it->second.first; 
		int v = it->second.second; 

		int set_u = ds.find(u); 
		int set_v = ds.find(v); 

		// Check if the selected edge is creating 
		// a cycle or not (Cycle is created if u 
		// and v belong to same set) 
		if (set_u != set_v) 
		{ 
			// Current edge will be in the MST 
			// so print it 
			cout << u << " - " << v << endl; 

			// Update MST weight 
			mst_wt += it->first; 

			// Merge two sets 
			ds.merge(set_u, set_v); 
		} 
	} 

	return mst_wt; 
} 

// Driver program to test above functions 
int main() 
{ 
	/* Let us create above shown weighted 
	and undirected graph */
	int V = 9, E = 14; 
	Graph g(V, E); 

	// making above shown graph 
	g.addEdge(0, 1, 4); 
	g.addEdge(0, 7, 8); 
	g.addEdge(1, 2, 8); 
	g.addEdge(1, 7, 11); 
	g.addEdge(2, 3, 7); 
	g.addEdge(2, 8, 2); 
	g.addEdge(2, 5, 4); 
	g.addEdge(3, 4, 9); 
	g.addEdge(3, 5, 14); 
	g.addEdge(4, 5, 10); 
	g.addEdge(5, 6, 2); 
	g.addEdge(6, 7, 1); 
	g.addEdge(6, 8, 6); 
	g.addEdge(7, 8, 7); 

	cout << "Edges of MST are \n"; 
	int mst_wt = g.kruskalMST(); 

	cout << "\nWeight of MST is " << mst_wt; 

	return 0; 
}

Output:

Output

Edges of MST are 
6 - 7
2 - 8
5 - 6
0 - 1
2 - 5
2 - 3
0 - 7
3 - 4
Weight of MST is 37
...................................
Process executed in 0.11 seconds
Press any key to continue.

Explanation:

  • #include<bits/stdc++.h> and using namespace std;
  • These lines include the necessary C++ standard libraries for this program.
  • typedef pair<int, int> iPair;
  • This line defines a shorthand iPair for a pair of integers.
  • struct Graph
  • This is the definition of a structure that represents a graph.
  • It has three member variables: V (the number of vertices), E (the number of edges), and edges (a vector of pairs, where each pair represents an edge's weight and its vertices).
  • Graph(int V, int E):
  • This is the constructor for the Graph structure, which initializes the number of vertices and edges.
  • void addEdge(int u, int v, int w):
  • This is a member function of the Graph structure used to add an edge to the graph. It takes the source vertex u, target vertex v, and the weight of the edge w.
  • int kruskalMST:
  • This is a member function of the Graph structure that implements Kruskal's algorithm to find the Minimum Spanning Tree. It returns the weight of the MST.
  • struct DisjointSets:
  • This structure represents disjoint sets for use in Kruskal's algorithm. It has member variables for parent, rank, and the number of elements in the set.
  • DisjointSets(int n):
  • The constructor for the DisjointSets structure that initializes the disjoint sets and ranks.
  • int find(int u):
  • A function to find the parent of a node 'u' using path compression.
  • void merge(int x, int y):
  • A function for union by rank, merging two sets.
  • int Graph::kruskalMST (method implementation):
  • This function implements Kruskal's algorithm to find the Minimum Spanning Tree of the graph.
  • main function:
  • The driver program that demonstrates the Kruskal's algorithm on a specific graph.
  • It creates a Graph instance, adds edges to it, and then calls the kruskalMST function to find the MST.
  • Finally, it prints the edges of the MST and its total weight.
  • The code uses the C++ standard library data structures and functions to implement Kruskal's algorithm for finding the MST of a graph.
  • Time and Space Complexity Analysis

Kruskal's algorithm operates as a greedy approach to identify the Minimum Spanning Tree (MST) by iteratively choosing edges with the lowest weight, ensuring cycle avoidance. To evaluate the efficiency of this implementation, a detailed examination of its time and space complexity is essential, necessitating a step-by-step deconstruction.

Time Complexity:

  • Initializing the graph and adding edges takes O(E) time, where E is the number of edges.
  • Sorting the edges using the sort function takes O(E * log(E)) time. Sorting is the most time-consuming operation in the code.
  • Creating the Disjoint Sets structure takes O(V) time, where V is the number of vertices.
  • The main loop iterates through all sorted edges and performs operations that depend on the number of edges, E.

Within the loop

  • Finding the parent of a node using path compression (Disjoint Sets) takes nearly constant amortized time, O(log(V)), where log is the iterated logarithm. In practice, it's almost constant, which is very close to O(1).
  • Merging two sets by rank also takes nearly constant time, O(1).
  • Hence, the overall time complexity of the code is dominated by the sorting step, which is O(E log(E)). In practice, for a dense graph, this is very close to O(E log(V)), which is the more commonly seen expression for Kruskal's algorithm's time complexity.

Space Complexity:

  • The space complexity is primarily determined by the data structures used in the code.
  • The edges vector stores all the edges with their weights, which takes O(E) space.
  • The Disjoint Sets structure requires additional space for parent and rank arrays, which takes O(V) space.
  • The additional space used for variables, iterators, and other constants is relatively small and can be considered constant.
  • Hence, the space complexity of the code is O(E + V), where E is the number of edges and V is the number of vertices. In most cases, E is much smaller than V, so the space complexity can be approximated as O(V).
  • In summary, the time complexity of this code is dominated by the sorting step, which is O(E * log(E)), and the space complexity is O(E + V). Kruskal's algorithm is efficient for finding the MST in sparse graphs and performs well in practice.
  • Applications of Greedy Algorithms

Greedy algorithms find applications in a range of domains such as finance, technology, and information technology. They are commonly employed in the following scenarios:

  1. Solving Shortest Path Problems

Greedy strategies, like the Dijkstra's algorithm, efficiently determine the shortest route between two nodes in graph theory and network routing. The algorithm selects the nearest unvisited node iteratively until it reaches the destination.

  1. Huffman Encoding

Data compression involves the utilization of Huffman coding to encode characters with codes of varying lengths. This method combines the least frequent characters iteratively to construct the Huffman tree, employing greedy algorithms.

  1. The Fractional Knapsack Problem

In this widely recognized optimization scenario, the objective is to maximize the total value while staying within a set weight constraint. This is achieved by selecting a combination of items that have varying weights and values. By opting for items with the most favorable value-to-weight ratio, greedy algorithms can provide an approximate solution.

Advantages of Greedy Algorithms

  1. Simplicity

One of the key benefits of greedy algorithms lies in their straightforwardness. They are straightforward to comprehend, execute, and evaluate, which makes them a favored option for addressing issues in a timely and effective manner.

  1. Efficiency

Greedy algorithms frequently exhibit impressive time and space efficiency, rendering them appropriate for real-time applications and situations involving extensive datasets.

Limitations of Greedy Algorithms

While greedy strategies can be effective, they may not be appropriate for every problem due to certain inherent constraints:

  1. Absence of Global Optimality

Greedy strategies involve making choices that prioritize immediate benefits over long-term outcomes, potentially leading to suboptimal solutions in some cases.

  1. Avoiding Backtracking

Once a selection is finalized in a greedy algorithm, it becomes irreversible. In case an initial choice results in a less-than-optimal outcome down the line, there exists no provision to backtrack and rectify it.

Greedy algorithms efficiently address optimization problems by choosing locally optimal solutions at each step. They play a crucial role in various domains of computer science and other fields, even though they come with inherent constraints and do not always ensure globally optimal outcomes. Mastering the art of applying greedy algorithms appropriately can lead to elegant and effective solutions across a range of practical scenarios.

Input Required

This code uses input(). Please provide values below:

Logic Practice
Install Logic Practice
Add to home screen for a faster app-like experience