Dense Graphs In C++ - C++ Programming Tutorial
C++ Course / Data Structures / Dense Graphs In C++

Dense Graphs In C++

BLUF: Mastering Dense Graphs 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: Dense Graphs In C++

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

Introduction

Graphs are fundamental components in the realms of computer science and mathematics, representing interconnected networks through nodes. Within graph theory, these structures are categorized into sparsely connected and densely connected graphs, aiding in the selection of appropriate algorithms and data structures. The focus here is on dense graphs, exploring their characteristics, challenges, and utilization within the context of C++.

What is a Dense Graph?

A dense graph is a specific kind of graph where the number of edges approaches the maximum limit within the graph. In a basic undirected graph, the maximum number of edges can be calculated using the following formula:

Example

Emax=(V×(V−1))/2

A graph is classified as dense when the number of edges E scales approximately with O(V^2), where V represents the number of vertices. A prime example of a dense graph is a complete graph, where every possible pair of unique vertices is linked by an edge.

Problem Statement

In scenarios involving dense graphs, the significance of their representation and algorithms is notably high because of the substantial quantity of edges present. The challenge lies in efficiently performing tasks like adding or removing edges, as well as traversing the graph, in a manner that is both rapid and resource-efficient in terms of memory and processing time. This tutorial delves into the practical implementation of dense graphs in C++, addressing both the theoretical concepts and hands-on coding strategies.

Adjacency Matrix

In scenarios involving a dense graph, an adjacency matrix plays a crucial role due to the following reasons:

  • Efficient Edge Lookup: It enables quick identification of edge existence between two vertices in constant O(1) time.
  • Effortless Edge Addition and Removal: The addition and removal of edges can be effortlessly executed in constant O(1) time as well.

However, an adjacency matrix requires a memory of O(V2), which can be a significant drawback, particularly with large graphs.

Example Implementation in C++

Program 1:

Here is a basic example demonstrating the implementation of an adjacency matrix to represent a dense graph in the C++ programming language.

Example

#include <iostream>
#include <vector>

class DenseGraph {
public:
    // Constructor

    DenseGraph(int vertices) : V(vertices), adjMatrix(vertices, std::vector<bool>(vertices, false)) {}

    // Add an edge
    void addEdge(int u, int v) {
        if (u >= 0 && u < V && v >= 0 && v < V) {
            adjMatrix[u][v] = true;
            adjMatrix[v][u] = true; // For undirected graph
        }
    }

    // Remove an edge
    void removeEdge(int u, int v) {
        if (u >= 0 && u < V && v >= 0 && v < V) {
            adjMatrix[u][v] = false;
            adjMatrix[v][u] = false; // For undirected graph
        }
    }

    // Check if there is an edge
    bool hasEdge(int u, int v) const {
        return u >= 0 && u < V && v >= 0 && v < V && adjMatrix[u][v];
    }

    // Print the adjacency matrix
    void printGraph() const {
        for (const auto& row : adjMatrix) {
            for (bool cell : row) {
                std::cout << cell << " ";
            }
            std::cout << std::endl;
        }
    }

private:
    int V; // Number of vertices
    std::vector<std::vector<bool>> adjMatrix; // Adjacency matrix
};

int main() {
    DenseGraph g(5); // Create a graph with 5 vertices

    g.addEdge(0, 1);
    g.addEdge(0, 2);
    g.addEdge(1, 2);
    g.addEdge(3, 4);

    std::cout << "Adjacency Matrix:" << std::endl;
    g.printGraph();

    std::cout << "Edge between 0 and 1: " << (g.hasEdge(0, 1) ? "Yes" : "No") << std::endl;

    return 0;
}

Output:

Output

Adjacency Matrix:
0 1 1 0 0 
1 0 1 0 0 
1 1 0 0 0 
0 0 0 0 1 
0 0 0 1 0 
Edge between 0 and 1: Yes

Explanation:

  • Constructor: It creates a graph of vertices ?. The adjacency matrix is being built having dimensions of ?×? and all the positions are filled with false to show there are no edges.
  • Add Edge: It creates an undirected edge between vertices u and v by changing the relevant places in the adjacency matrix to true.
  • Remove Edge: It dismisses an edge connecting vertices u and v by changing the respective places in the adjacency matrix to false.
  • Has Edge: It determines whether there exists an up-linking edge joining vertices u and v by accessing the relevant sites in the adjacency's matrix.
  • Print Graph: It shows the adjacency matrix which consists the determination of edges to the vertices, where 1 (true) indicates that an edge is present and 0 (false) that there is no edge.
  • In the main function, it is constructed a DenseGraph with 5 vertices, edges are added, the graph is displayed, and it is verified whether there is an edge between vertices 0 and 1.
  • Time and Space Complexities:

  • Time Complexity: O(V^2)
  • Space Complexity: O(V^2)
  • Program 2:

Utilizing the adjacency matrix facilitates the creation and manipulation of dense graphs, maintaining a straightforward implementation approach.

Example

#include <iostream>
#include <vector>
#include <queue>
#include <stack>
#include <limits>

class DenseGraph {
public:
    // Constructor
    DenseGraph(int vertices) : V(vertices), adjMatrix(vertices, std::vector<int>(vertices, -1)) {}

    // Destructor
    ~DenseGraph() = default;

    // Add an edge with weight
    void addEdge(int u, int v, int weight = 1) {
        if (isValidVertex(u) && isValidVertex(v)) {
            adjMatrix[u][v] = weight;
            adjMatrix[v][u] = weight; // For undirected graph
        }
    }

    // Remove an edge
    void removeEdge(int u, int v) {
        if (isValidVertex(u) && isValidVertex(v)) {
            adjMatrix[u][v] = -1;
            adjMatrix[v][u] = -1; // For undirected graph
        }
    }

    // Check if there is an edge
    bool hasEdge(int u, int v) const {
        return isValidVertex(u) && isValidVertex(v) && adjMatrix[u][v] != -1;
    }

    // Get weight of an edge
    int getEdgeWeight(int u, int v) const {
        if (isValidVertex(u) && isValidVertex(v)) {
            return adjMatrix[u][v];
        }
        return -1;
    }

    // Print the adjacency matrix
    void printGraph() const {
        for (const auto& row : adjMatrix) {
            for (int weight : row) {
                if (weight == -1) {
                    std::cout << "INF ";
                } else {
                    std::cout << weight << " ";
                }
            }
            std::cout << std::endl;
        }
    }

    // Depth-First Search (DFS)
    void DFS(int start) const {
        if (!isValidVertex(start)) return;

        std::vector<bool> visited(V, false);
        std::stack<int> stack;
        stack.push(start);

        while (!stack.empty()) {
            int vertex = stack.top();
            stack.pop();

            if (!visited[vertex]) {
                visited[vertex] = true;
                std::cout << vertex << " ";

                // Push all unvisited adjacent vertices
                for (int i = 0; i < V; ++i) {
                    if (adjMatrix[vertex][i] != -1 && !visited[i]) {
                        stack.push(i);
                    }
                }
            }
        }
        std::cout << std::endl;
    }

    // Breadth-First Search (BFS)
    void BFS(int start) const {
        if (!isValidVertex(start)) return;

        std::vector<bool> visited(V, false);
        std::queue<int> queue;
        queue.push(start);
        visited[start] = true;

        while (!queue.empty()) {
            int vertex = queue.front();
            queue.pop();
            std::cout << vertex << " ";

            // Queue all unvisited adjacent vertices
            for (int i = 0; i < V; ++i) {
                if (adjMatrix[vertex][i] != -1 && !visited[i]) {
                    queue.push(i);
                    visited[i] = true;
                }
            }
        }
        std::cout << std::endl;
    }

    // Resize the graph
    void resize(int newSize) {
        if (newSize <= V) {
            std::cerr << "New size must be greater than the current size!" << std::endl;
            return;
        }

        // Resize the matrix
        adjMatrix.resize(newSize);
        for (auto& row : adjMatrix) {
            row.resize(newSize, -1);
        }

        // Initialize new edges to -1 (infinity or no connection)
        for (int i = 0; i < V; ++i) {
            adjMatrix[i].resize(newSize, -1);
        }

        V = newSize;
    }

private:
    int V; // Number of vertices
    std::vector<std::vector<int>> adjMatrix; // Adjacency matrix with weights

    // Check if vertex is valid
    bool isValidVertex(int vertex) const {
        return vertex >= 0 && vertex < V;
    }
};

int main() {
    DenseGraph g(5); // Create a graph with 5 vertices

    g.addEdge(0, 1, 10);
    g.addEdge(0, 2, 20);
    g.addEdge(1, 2, 30);
    g.addEdge(3, 4, 40);

    std::cout << "Adjacency Matrix:" << std::endl;
    g.printGraph();

    std::cout << "DFS starting from vertex 0:" << std::endl;
    g.DFS(0);

    std::cout << "BFS starting from vertex 0:" << std::endl;
    g.BFS(0);

    std::cout << "Resizing the graph to 7 vertices." << std::endl;
    g.resize(7);

    std::cout << "Adjacency Matrix after resizing:" << std::endl;
    g.printGraph();

    return 0;
}

Output:

Output

Adjacency Matrix:
INF 10 20 INF INF 
10 INF 30 INF INF 
20 30 INF INF INF 
INF INF INF INF 40 
INF INF INF 40 INF 
DFS starting from vertex 0:
0 2 1 
BFS starting from vertex 0:
0 1 2 
Resizing the graph to 7 vertices.
Adjacency Matrix after resizing:
INF 10 20 INF INF INF INF 
10 INF 30 INF INF INF INF 
20 30 INF INF INF INF INF 
INF INF INF INF 40 INF INF 
INF INF INF 40 INF INF INF 
INF INF INF INF INF INF INF 
INF INF INF INF INF INF INF

Explanation:

  • Weighted Adjacency Matrix: In the graph, edges with weight have been added and their weights represented by the integers. The initial weight of -1 depicts there is no edge.
  • Depth-First Search and Breadth-First Search Algorithms: Each of the two algorithms is applied to explore the graph structure to the extent of listing all its vertices in the order they are landed upon. A stack is employed in DFS whereas a queue is used in BFS.
  • Graph Resizing: The resize function makes it possible to increase the current size of the graph in the instances where new edges are assigned a weight of -1. This is beneficial in various graph cases especially those that are dynamic.
  • Error Handling and Validation: Contains some basic checking of edge some operations, especially resizing and addition and removal of edges guarantees that the performed actions will be within some reasonable limits.
  • Time and Space Complexities:

  • Time Complexity: Most operations such as addition, removal and checking for edges are O(1). Most of the traversal algorithms i.e. DFS BFS and graph printing algorithms are O (V^2) in complexity and graph resizing is also O (V^2).
  • Space Complexity: The portion of the space that will be filled by the adjacency matrix is O (V^2) in size. Usage of O (V) space for auxiliary structures such as stack, queue, and visited vector respectively for Depth First Search and Breadth First Search.
  • Program 3:

Example

#include <iostream>
#include <vector>
#include <queue>
#include <limits>

class DirectedDenseGraph {
public:
    // Constructor
    DirectedDenseGraph(int vertices) : V(vertices), adjMatrix(vertices, std::vector<int>(vertices, INF)) {}

    // Destructor
    ~DirectedDenseGraph() = default;

    // Add an edge with weight
    void addEdge(int u, int v, int weight) {
        if (isValidVertex(u) && isValidVertex(v)) {
            adjMatrix[u][v] = weight;
        }
    }

    // Remove an edge
    void removeEdge(int u, int v) {
        if (isValidVertex(u) && isValidVertex(v)) {
            adjMatrix[u][v] = INF;
        }
    }

    // Check if there is an edge
    bool hasEdge(int u, int v) const {
        return isValidVertex(u) && isValidVertex(v) && adjMatrix[u][v] != INF;
    }

    // Get weight of an edge
    int getEdgeWeight(int u, int v) const {
        if (isValidVertex(u) && isValidVertex(v)) {
            return adjMatrix[u][v];
        }
        return INF;
    }

    // Print the adjacency matrix
    void printGraph() const {
        for (const auto& row : adjMatrix) {
            for (int weight : row) {
                if (weight == INF) {
                    std::cout << "INF ";
                } else {
                    std::cout << weight << " ";
                }
            }
            std::cout << std::endl;
        }
    }

    // Dijkstra's Algorithm to find shortest paths from a given source
    void dijkstra(int start) const {
        if (!isValidVertex(start)) return;

        std::vector<int> dist(V, INF);
        std::vector<bool> visited(V, false);
        dist[start] = 0;

        using Pair = std::pair<int, int>;
        // Specify the type for std::greater
        std::priority_queue<Pair, std::vector<Pair>, std::greater<Pair>> pq;
        pq.push({0, start});

        while (!pq.empty()) {
            int u = pq.top().second;
            pq.pop();

            if (visited[u]) continue;
            visited[u] = true;

            for (int v = 0; v < V; ++v) {
                if (adjMatrix[u][v] != INF && !visited[v]) {
                    int newDist = dist[u] + adjMatrix[u][v];
                    if (newDist < dist[v]) {
                        dist[v] = newDist;
                        pq.push({dist[v], v});
                    }
                }
            }
        }

        // Output shortest distances from source
        std::cout << "Shortest distances from vertex " << start << ":" << std::endl;
        for (int i = 0; i < V; ++i) {
            if (dist[i] == INF) {
                std::cout << "Vertex " << i << ": INF" << std::endl;
            } else {
                std::cout << "Vertex " << i << ": " << dist[i] << std::endl;
            }
        }
    }

private:
    int V; // Number of vertices
    std::vector<std::vector<int>> adjMatrix; // Adjacency matrix with weights
    const int INF = std::numeric_limits<int>::max(); // Represents infinity

    // Check if vertex is valid
    bool isValidVertex(int vertex) const {
        return vertex >= 0 && vertex < V;
    }
};

int main() {
    DirectedDenseGraph g(5); // Create a graph with 5 vertices

    g.addEdge(0, 1, 10);
    g.addEdge(0, 2, 3);
    g.addEdge(1, 2, 1);
    g.addEdge(1, 3, 2);
    g.addEdge(2, 1, 4);
    g.addEdge(2, 3, 8);
    g.addEdge(2, 4, 2);
    g.addEdge(3, 4, 7);
    g.addEdge(4, 3, 9);

    std::cout << "Adjacency Matrix:" << std::endl;
    g.printGraph();

    std::cout << std::endl;
    g.dijkstra(0); // Run Dijkstra's algorithm from vertex 0

    return 0;
}

Output:

Output

Adjacency Matrix:
4214880 10 3 4214880 4214880 
4214880 4214880 1 2 4214880 
4214880 4 4214880 8 2 
4214880 4214880 4214880 4214880 7 
4214880 4214880 4214880 9 4214880 

Shortest distances from vertex 0:
Vertex 0: 0
Vertex 1: 7
Vertex 2: 3
Vertex 3: 9
Vertex 4: 5

Explanation:

  • Graph Representation: Adjacency Matrix: We use a 2D vector to represent the matrix, initialized to INF (infinity) to indicate no connection. For a directed graph, the matrix is not symmetric.
  • Edge Operations: Add Edge: Sets the weight for the edge from u to v. Remove Edge: Resets the weight to INF, which indicates no connection. Check Edge: Returns true if there's an edge with a weight not equal to INF.
  • Shortest Path Algorithm: Dijkstra's Algorithm: Finds the shortest paths from a source vertex to all other vertices. We use a priority queue (min-heap) to efficiently get the next vertex with the smallest distance.
  • Graph Output and Traversal: Print Graph: Displays the adjacency matrix. Dijkstra's Shortest Path Output: Prints the shortest distances from the source vertex.
  • Adjacency Matrix: We use a 2D vector to represent the matrix, initialized to INF (infinity) to indicate no connection. For a directed graph, the matrix is not symmetric.
  • Add Edge: Sets the weight for the edge from u to v.
  • Remove Edge: Resets the weight to INF, which indicates no connection.
  • Check Edge: Returns true if there's an edge with a weight not equal to INF.
  • Dijkstra's Algorithm: Finds the shortest paths from a source vertex to all other vertices. We use a priority queue (min-heap) to efficiently get the next vertex with the smallest distance.
  • Print Graph: Displays the adjacency matrix.
  • Dijkstra's Shortest Path Output: Prints the shortest distances from the source vertex.
  • Time and Space Complexities:

  • Time Complexity: It is easy to see that the only activity that takes considerable time is Dijkstra's algorithm in a dense graph due to priority queue manipulations and graphs traversing. Hence overall time complexity becomes O (V^2log V).
  • Space Complexity: Calculating from the adjacency matrix in addition to O (V) for other additional memory required in the course of Dijkstra's algorithm as distance vector, visited vector and priority queue makes the space complexity to be O (V^2).
  • Advantages of Dense Graphs

  • High Connectivity: Robust Networks: Dense graphs are well connected such that edges connect a good proportion of vertex pairs. This feature of high connectivity can result in creation of more reliable and robust networks since nodes have alternative ways of reaching each other. Redundancy: High connectivity allows for redundancy, which is advantageous especially in communication networks because other routes will improve the resilience of the network to failure.
  • Efficient Algorithms for Certain Problems: Floyd-Warshall Algorithm: For dense graphs, algorithms like Floyd-Warshall for finding shortest paths between all pairs of vertices are efficient because the adjacency matrix representation is well-suited for such algorithms. Matrix Operations: Dense graphs can leverage matrix-based operations for various computations, including those in spectral graph theory and network flow problems.
  • Better Performance for Graph Traversal Algorithms: Breadth-First Search (BFS) and Depth-First Search (DFS): For dense graphs, traversal algorithms can be more straightforward and sometimes faster due to the high edge density, which reduces the need for complex edge lookups.
  • Simplicity in Representation and Implementation: Printing Out An Adjacency Matrix: Cases of dense graphs where adjacency matrix representation is usually applicable increase efficiency in the operation of implementing various algorithms because of less structure complexity and ease in accessing edges for existence queries. Ease of Implementation: A dense graph configuration is convenient with multiple links reducing the demand for complicated data structures and violations for sparsely linked graphs.
  • Robust Networks: Dense graphs are well connected such that edges connect a good proportion of vertex pairs. This feature of high connectivity can result in creation of more reliable and robust networks since nodes have alternative ways of reaching each other.
  • Redundancy: High connectivity allows for redundancy, which is advantageous especially in communication networks because other routes will improve the resilience of the network to failure.
  • Floyd-Warshall Algorithm: For dense graphs, algorithms like Floyd-Warshall for finding shortest paths between all pairs of vertices are efficient because the adjacency matrix representation is well-suited for such algorithms.
  • Matrix Operations: Dense graphs can leverage matrix-based operations for various computations, including those in spectral graph theory and network flow problems.
  • Breadth-First Search (BFS) and Depth-First Search (DFS): For dense graphs, traversal algorithms can be more straightforward and sometimes faster due to the high edge density, which reduces the need for complex edge lookups.
  • Printing Out An Adjacency Matrix: Cases of dense graphs where adjacency matrix representation is usually applicable increase efficiency in the operation of implementing various algorithms because of less structure complexity and ease in accessing edges for existence queries.
  • Ease of Implementation: A dense graph configuration is convenient with multiple links reducing the demand for complicated data structures and violations for sparsely linked graphs.
  • Applications of Dense Graphs:

  • Efficiency Improvement for Applications Where Edge Checks are Required: Cheaper Edge Queries: Applications, which involve full accessibility of edges frequently employing edges existence check, can be well served with adjacent matrix which allows lookup of edges in constant time, more so in a dense graph where most edges are likely to be illegal.
  • Handling Of Nearly Complete Or Complete Graphs: Graphs Which Are Complete: Such dense graphs as complete graphs where any two vertices of the graph are joined by an edge find utilization in cases where an assurance of a fully connected graph is solicited. These types of networks are also employed in network optimization and various theoretical problems.
  • Use Cases In Some Networks Could Comprise The Following Uses: Social Networks: In the context of social networks, dense graphs serve the purpose of modeling clustered actors (vertices) with many connections (edges). Biological Networks: High-density graphs can also represent complex systems in biological networks, such as protein-protein interaction networks, where many proteins before connecting with other proteins.
  • Strong Graph Properties: Strong Connectivity: This also implies that dense graphs are generally strongly connected in that there is more than one way to get from one vertex to another which improves the strength of the entire structure of the graph. Clustering: Usually, the clustering coefficients of dense graphs will be high since the neighbors of certain vertices are likely to become neighbors amongst themselves and this is useful in areas such as community detection and clustering.
  • Cheaper Edge Queries: Applications, which involve full accessibility of edges frequently employing edges existence check, can be well served with adjacent matrix which allows lookup of edges in constant time, more so in a dense graph where most edges are likely to be illegal.
  • Graphs Which Are Complete: Such dense graphs as complete graphs where any two vertices of the graph are joined by an edge find utilization in cases where an assurance of a fully connected graph is solicited. These types of networks are also employed in network optimization and various theoretical problems.
  • Social Networks: In the context of social networks, dense graphs serve the purpose of modeling clustered actors (vertices) with many connections (edges).
  • Biological Networks: High-density graphs can also represent complex systems in biological networks, such as protein-protein interaction networks, where many proteins before connecting with other proteins.
  • Strong Connectivity: This also implies that dense graphs are generally strongly connected in that there is more than one way to get from one vertex to another which improves the strength of the entire structure of the graph.
  • Clustering: Usually, the clustering coefficients of dense graphs will be high since the neighbors of certain vertices are likely to become neighbors amongst themselves and this is useful in areas such as community detection and clustering.
  • Conclusion:

In summary, graphs that are considered dense, meaning they have a high edge-to-vertex ratio, come with their own set of benefits and difficulties. Dense graphs are best depicted using an adjacency matrix, which enables quick edge manipulations but demands more memory space. Utilizing the adjacency matrix representation in C++ necessitates well-designed wrappers that understand these trade-offs and address them effectively. As graph implementations grow in complexity and usage, optimizing for memory and performance becomes crucial for practicality.

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