Skip to content
Related Articles

Related Articles

Improve Article
Save Article
Like Article

DSatur Algorithm for Graph Coloring

  • Difficulty Level : Medium
  • Last Updated : 08 Nov, 2021

Graph colouring is the task of assigning colours to the vertices of a graph so that:

  • pairs of adjacent vertices are assigned different colours, and
  • the number of different colours used across the graph is minimal.

The following graph has been coloured using just three colours (red, blue and green here). This is actually the minimum number of colours needed for this particular graph – that is, we cannot colour this graph using fewer than three colours while ensuring that adjacent vertices are coloured differently.

Proper three-colouring of a graph

The minimum number of colours needed to colour a graph G is known as the chromatic number and is usually denoted by χ(G). Determining the chromatic number of a graph is NP-hard. The corresponding decision problem of deciding whether a k-colouring exists for a graph G is also NP-complete.  

Similar posts on this website have already described the greedy algorithm for graph colouring. This algorithm is simple to apply, but the number of colours used in its solutions depends very much on the order that the vertices are considered. In the best case, the right ordering will produce a solution using χ(G) colours; however bad orderings can result in solutions using many additional colours. 

The DSatur algorithm (abbreviated from “degree of saturation”) has similar behaviour to the Greedy algorithm. The difference lies in the way that it generates the vertex ordering. Specifically, the next vertex to colour is always chosen as the uncoloured vertex with the highest saturation degree. The saturation degree of a vertex is defined as the number of different colours currently assigned to neighbouring vertices. Other rules are also then used to break ties. 

Let G be a graph with n vertices and m edges. In addition, assume that we will use the colour labels 0, 1, 2, …, n-1. (More than n colours are never required in a solution). The DSatur algorithm operates as follows

  1. Let v be the uncoloured vertex in G with the largest saturation degree. In cases of ties, choose the vertex among these with the largest degree in the subgraph induced by the uncoloured vertices. Further ties can be broken arbitrarily.
  2. Assign v to colour i, where i is the smallest integer from the set {0, 1, 2, …, n} that is not currently assigned to any neighbour of v.
  3. If there remain uncoloured vertices, repeat all steps again, otherwise, end at this step.

The DSatur algorithm is similar to the Greedy algorithm in that once a vertex has been selected, it is assigned to the lowest colour label not assigned to any of its neighbours. The actions of Step 1, therefore, provide the main power behind the algorithm in that they prioritise vertices that are seen to be the “most constrained” – that is, vertices that currently have the fewest colour options available to them. Consequently, these “more constrained” vertices are dealt with first, allowing the less constrained vertices to be coloured later.

Analysis of DSatur

Because the DSatur algorithm generates a vertex ordering during execution, the number of colours it uses is more predictable than the greedy algorithm. Its solutions also tend to have fewer colours than those of the greedy algorithm. One feature of the algorithm is that, if a graph is composed of multiple components, then all vertices of a single component will be coloured before the other vertices are considered. DSatur is also exact for several graph topologies including bipartite graphs, cycle graphs and wheel graphs. (With these graphs, a solution using χ(G) colours will always be produced.) 

The overall complexity of the DSatur algorithm is O(n2), where n is the number of vertices in the graph. This can be achieved by performing n separate applications of an O(n) process that:

  • Identifies the next vertex to colour according to DSatur’s selection rules.
  • Colours this vertex.

Below we present a C++ implementation of DSatur that operates in O((n + m) log n) time, where m is the number of edges in the graph. This is much faster than O(n2) for all but the densest of graphs. This implementation involves using a red-black binary tree to store all vertices that are not yet coloured, together with their saturation degrees and their degrees in the subgraph induced by the uncoloured vertices. Red-black trees are a type of self-balancing binary tree that are used with the set container in C++’s standard template library. This allows the selection of the next vertex to colour (according to DSatur’s selection rules) to be performed in constant time. It also allows items to be inserted and removed in logarithmic time. 

C++




// A C++ program to implement the DSatur algorithm for graph
// coloring
 
#include <iostream>
#include <set>
#include <tuple>
#include <vector>
using namespace std;
 
// Struct to store information
// on each uncoloured vertex
struct nodeInfo {
    int sat; // Saturation degree of the vertex
    int deg; // Degree in the uncoloured subgraph
    int vertex; // Index of vertex
};
struct maxSat {
    bool operator()(const nodeInfo& lhs,
                    const nodeInfo& rhs) const
    {
        // Compares two nodes by
        // saturation degree, then
        // degree in the subgraph,
        // then vertex label
        return tie(lhs.sat, lhs.deg, lhs.vertex)
               > tie(rhs.sat, rhs.deg, rhs.vertex);
    }
};
 
// Class representing
// an undirected graph
class Graph {
 
    // Number of vertices
    int n;
 
    // Number of vertices
    vector<vector<int> > adj;
 
public:
    // Constructor and destructor
    Graph(int numNodes)
    {
        n = numNodes;
        adj.resize(n, vector<int>());
    }
    ~Graph() { adj.clear(); }
 
    // Function to add an edge to graph
    void addEdge(int u, int v);
 
    // Colour the graph
    // using the DSatur algorithm
    void DSatur();
};
 
void Graph::addEdge(int u, int v)
{
    adj[u].push_back(v);
    adj[v].push_back(u);
}
 
// Assigns colors (starting from 0)
// to all vertices and
// prints the assignment of colors
void Graph::DSatur()
{
    int u, i;
    vector<bool> used(n, false);
    vector<int> c(n), d(n);
    vector<set<int> > adjCols(n);
    set<nodeInfo, maxSat> Q;
    set<nodeInfo, maxSat>::iterator maxPtr;
 
    // Initialise the data structures.
    // These are a (binary
    // tree) priority queue, a set
    // of colours adjacent to
    // each uncoloured vertex
    // (initially empty) and the
    // degree d(v) of each uncoloured
    // vertex in the graph
    // induced by uncoloured vertices
    for (u = 0; u < n; u++) {
        c[u] = -1;
        d[u] = adj[u].size();
        adjCols[u] = set<int>();
        Q.emplace(nodeInfo{ 0, d[u], u });
    }
 
    while (!Q.empty()) {
 
        // Choose the vertex u
        // with highest saturation
        // degree, breaking ties with d.
        // Remove u from the priority queue
        maxPtr = Q.begin();
        u = (*maxPtr).vertex;
        Q.erase(maxPtr);
 
        // Identify the lowest feasible
        // colour i for vertex u
        for (int v : adj[u])
            if (c[v] != -1)
                used] = true;
        for (i = 0; i < used.size(); i++)
            if (used[i] == false)
                break;
        for (int v : adj[u])
            if (c[v] != -1)
                used] = false;
 
        // Assign vertex u to colour i
        c[u] = i;
 
        // Update the saturation degrees and
        // degrees of all uncoloured neighbours;
        // hence modify their corresponding
        // elements in the priority queue
        for (int v : adj[u]) {
            if (c[v] == -1) {
                Q.erase(
                    { int(adjCols[v].size()),
                      d[v], v });
                adjCols[v].insert(i);
                d[v]--;
                Q.emplace(nodeInfo{
                    int(adjCols[v].size()),
                    d[v], v });
            }
        }
    }
 
    // The full graph has been coloured.
    // Print the result
    for (u = 0; u < n; u++)
        cout << "Vertex " << u
             << " --->  Color " << c[u]
             << endl;
}
 
// Driver Code
int main()
{
    Graph G1(5);
    G1.addEdge(0, 1);
    G1.addEdge(0, 2);
    G1.addEdge(1, 2);
    G1.addEdge(1, 3);
    G1.addEdge(2, 3);
    G1.addEdge(3, 4);
    cout << "Coloring of graph G1 \n";
    G1.DSatur();
 
    Graph G2(5);
    G2.addEdge(0, 1);
    G2.addEdge(0, 2);
    G2.addEdge(1, 2);
    G2.addEdge(1, 4);
    G2.addEdge(2, 4);
    G2.addEdge(4, 3);
    cout << "\nColoring of graph G2 \n";
    G2.DSatur();
 
    return 0;
}


Output

Coloring of graph G1 
Vertex 0 --->  Color 0
Vertex 1 --->  Color 2
Vertex 2 --->  Color 1
Vertex 3 --->  Color 0
Vertex 4 --->  Color 1

Coloring of graph G2 
Vertex 0 --->  Color 0
Vertex 1 --->  Color 2
Vertex 2 --->  Color 1
Vertex 3 --->  Color 1
Vertex 4 --->  Color 0

The first part of this implementation involves initialising the data structures. This involves looping through each of the vertices and populating the red-black tree. This takes O(n log n) time. In the main part of the algorithm, the red-black tree allows the selection of the next vertex u to colour to be performed in constant time. Once u has been coloured, items corresponding to the uncoloured neighbours of u need to be updated in the red-black tree. Doing this for every vertex results in a total run time of O(m log n). Consequently, the overall run time is O((n log n) + (m log n)) = O((n+m) log n).

Further information on this algorithm and others for graph colouring can be found in the book: A Guide to Graph Colouring: Algorithms and Applications (2021) 


My Personal Notes arrow_drop_up
Recommended Articles
Page :

Start Your Coding Journey Now!