图形着色的 DSatur 算法
图形着色是将颜色分配给图形顶点的任务,以便:
- 成对的相邻顶点被分配不同的颜色,并且
- 图表中使用的不同颜色的数量最少。
下图仅使用三种颜色(此处为红色、蓝色和绿色)着色。这实际上是这个特定图形所需的最小颜色数——也就是说,我们不能使用少于三种颜色为这个图形着色,同时确保相邻顶点的颜色不同。
为图形G着色所需的最小颜色数称为色数,通常用χ(G)表示。确定图的色数是 NP 难的。决定图 G 是否存在k着色的相应决策问题也是 NP 完全的。
该网站上的类似帖子已经描述了图形着色的贪心算法。该算法易于应用,但其解决方案中使用的颜色数量很大程度上取决于考虑顶点的顺序。在最好的情况下,正确的排序将产生使用χ(G)颜色的解决方案;然而,糟糕的订购可能会导致解决方案使用许多额外的颜色。
DSatur 算法(缩写为“饱和度”)具有与贪心算法相似的行为。不同之处在于它生成顶点排序的方式。具体来说,总是选择下一个要着色的顶点作为饱和度最高的未着色顶点。顶点的饱和度定义为当前分配给相邻顶点的不同颜色的数量。然后还使用其他规则来打破平局。
设G是一个有n个顶点和m个边的图。此外,假设我们将使用颜色标签0, 1, 2, ..., n-1 。 (解决方案中永远不需要超过n种颜色)。 DSatur 算法操作如下
- 设v为G中饱和度最大的无色顶点。在有关系的情况下,选择由无色顶点诱导的子图中度数最大的顶点。进一步的联系可以任意中断。
- 将v分配给颜色i ,其中i是集合{0, 1, 2, ..., n}中当前未分配给v的任何邻居的最小整数。
- 如果仍有未着色的顶点,则再次重复所有步骤,否则,在此步骤结束。
DSatur 算法类似于贪心算法,因为一旦选择了一个顶点,它就会被分配给没有分配给任何邻居的最低颜色标签。因此,第 1 步的动作提供了算法背后的主要力量,因为它们优先考虑被视为“最受约束”的顶点——即当前可用颜色选项最少的顶点。因此,首先处理这些“更受约束”的顶点,允许稍后对受约束较少的顶点着色。
DSatur的分析
因为 DSatur 算法在执行过程中会生成一个顶点排序,所以它使用的颜色数量比贪心算法更容易预测。它的解决方案也往往比贪心算法的解决方案具有更少的颜色。该算法的一个特点是,如果一个图由多个组件组成,那么单个组件的所有顶点将在考虑其他顶点之前被着色。 DSatur 也适用于几种图拓扑,包括二分图、循环图和轮图。 (使用这些图表,将始终生成使用χ(G)颜色的解决方案。)
DSatur 算法的总体复杂度为O(n 2 ) ,其中n是图中的顶点数。这可以通过执行一个O(n)过程的n 个单独应用程序来实现:
- 根据 DSatur 的选择规则标识下一个要着色的顶点。
- 为该顶点着色。
下面我们展示了 DSatur 的 C++ 实现,它在O((n + m) log n)时间内运行,其中m是图中的边数。对于除了最密集的图之外的所有图,这都比O(n 2 )快得多。该实现涉及使用红黑二叉树来存储所有尚未着色的顶点,以及它们的饱和度以及它们在由未着色顶点引起的子图中的度数。红黑树是一种自平衡二叉树,与 C++ 标准模板库中的集合容器一起使用。这允许在恒定时间内执行下一个要着色的顶点的选择(根据 DSatur 的选择规则)。它还允许在对数时间内插入和删除项目。
C++
// A C++ program to implement the DSatur algorithm for graph
// coloring
#include
#include
#include
#include
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 > adj;
public:
// Constructor and destructor
Graph(int numNodes)
{
n = numNodes;
adj.resize(n, vector());
}
~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 used(n, false);
vector c(n), d(n);
vector > adjCols(n);
set Q;
set::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();
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;
}
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
该实现的第一部分涉及初始化数据结构。这涉及遍历每个顶点并填充红黑树。这需要O(n log n)时间。在算法的主要部分,红黑树允许在恒定时间内选择下一个要着色的顶点u 。一旦u被着色,对应于u未着色邻居的项需要在红黑树中更新。对每个顶点执行此操作会导致总运行时间为O(m log n) 。因此,总运行时间为O((n log n) + (m log n)) = O((n+m) log n) 。
有关此算法和其他图形着色算法的更多信息,请参阅本书:图形着色指南:算法和应用程序(2021 年)