每日算法 — 使用java实现图着色:Welsh-Powell贪心着色算法

图着色问题是图论中最经典的NP完全问题之一,在课程表编排、寄存器分配、地图着色、频谱分配等领域有广泛应用。本文用Java实现Welsh-Powell贪心着色算法,并结合DSatur改进策略,深入讲解图着色的核心思想与实现细节。

一、问题定义

给定无向图G=(V,E),图着色是指为每个顶点分配一种颜色,使得任意相邻的两个顶点颜色不同。色数χ(G)是完成合法着色所需的最少颜色数。确定色数是NP完全问题。

1.1 相关概念

  • 色数(Chromatic Number):图G正常着色所需的最少颜色数
  • 色多项式:用k种颜色着色图G的方案数
  • Welsh-Powell算法:按度数降序排列顶点,依次选择最小可用颜色
  • DSatur算法:每次选择饱和度(已着色邻居的不同颜色数)最高的顶点

二、算法原理

2.1 Welsh-Powell算法

  1. 将所有顶点按度数从大到小排序
  2. 依次为每个顶点分配最小的可用颜色
  3. 返回使用的颜色总数

该算法是贪心策略,虽然不能保证最优解,但通常能得到较好的近似结果。

2.2 DSatur算法(改进版)

  1. 选择度数最大的顶点作为起始,赋予颜色1
  2. 在剩余未着色顶点中,选择饱和度最高的顶点
  3. 饱和度相同时,选择度数最大的顶点
  4. 为该顶点分配最小可用颜色
  5. 重复步骤2-4直到所有顶点着色

DSatur通常比Welsh-Powell获得更少的颜色数。

三、Java实现

import java.util.*;

/**
 * 图着色问题求解
 * 实现Welsh-Powell算法和DSatur算法
 */
public class GraphColoring {

    private final int vertices;
    private final List<Set<Integer>> adjacencyList;

    public GraphColoring(int vertices) {
        this.vertices = vertices;
        this.adjacencyList = new ArrayList<>();
        for (int i = 0; i < vertices; i++) {
            adjacencyList.add(new HashSet<>());
        }
    }

    /**
     * 添加无向边
     */
    public void addEdge(int u, int v) {
        adjacencyList.get(u).add(v);
        adjacencyList.get(v).add(u);
    }

    /**
     * 获取顶点度数
     */
    public int degree(int v) {
        return adjacencyList.get(v).size();
    }

    /**
     * Welsh-Powell贪心着色算法
     * 按度数降序排列,依次分配最小可用颜色
     * @return 每个顶点的颜色数组(颜色从0开始编号)
     */
    public int[] welshPowellColoring() {
        int[] colors = new int[vertices];
        Arrays.fill(colors, -1); // -1表示未着色

        // 按度数降序排列顶点
        Integer[] vertexOrder = new Integer[vertices];
        for (int i = 0; i < vertices; i++) {
            vertexOrder[i] = i;
        }
        Arrays.sort(vertexOrder, (a, b) -> degree(b) - degree(a));

        int maxColor = -1;

        for (int v : vertexOrder) {
            // 收集邻居已使用的颜色
            boolean[] usedColors = new boolean[vertices];
            for (int neighbor : adjacencyList.get(v)) {
                if (colors[neighbor] != -1) {
                    usedColors[colors[neighbor]] = true;
                }
            }

            // 分配最小的可用颜色
            for (int c = 0; c < vertices; c++) {
                if (!usedColors[c]) {
                    colors[v] = c;
                    maxColor = Math.max(maxColor, c);
                    break;
                }
            }
        }

        return colors;
    }

    /**
     * DSatur着色算法(改进的贪心算法)
     * 优先选择饱和度最高的顶点
     * @return 每个顶点的颜色数组
     */
    public int[] dsaturColoring() {
        int[] colors = new int[vertices];
        Arrays.fill(colors, -1);
        boolean[] colored = new boolean[vertices];

        // 记录每个顶点的饱和度(已着色邻居的不同颜色数)
        Set<Integer>[] saturationSets = new HashSet[vertices];
        for (int i = 0; i < vertices; i++) {
            saturationSets[i] = new HashSet<>();
        }

        // 第一步:选择度数最大的顶点,赋予颜色0
        int maxDegreeVertex = 0;
        for (int i = 1; i < vertices; i++) {
            if (degree(i) > degree(maxDegreeVertex)) {
                maxDegreeVertex = i;
            }
        }
        colors[maxDegreeVertex] = 0;
        colored[maxDegreeVertex] = true;
        updateSaturation(maxDegreeVertex, colors, saturationSets);

        // 后续步骤:每次选择饱和度最高的未着色顶点
        for (int step = 1; step < vertices; step++) {
            int selectedVertex = -1;
            int maxSaturation = -1;
            int maxDeg = -1;

            for (int v = 0; v < vertices; v++) {
                if (colored[v]) continue;

                int sat = saturationSets[v].size();
                int deg = degree(v);

                // 优先选饱和度高的,饱和度相同选度数大的
                if (sat > maxSaturation || 
                    (sat == maxSaturation && deg > maxDeg)) {
                    maxSaturation = sat;
                    maxDeg = deg;
                    selectedVertex = v;
                }
            }

            // 为选中的顶点分配最小可用颜色
            boolean[] usedColors = new boolean[vertices];
            for (int neighbor : adjacencyList.get(selectedVertex)) {
                if (colors[neighbor] != -1) {
                    usedColors[colors[neighbor]] = true;
                }
            }

            for (int c = 0; c < vertices; c++) {
                if (!usedColors[c]) {
                    colors[selectedVertex] = c;
                    break;
                }
            }

            colored[selectedVertex] = true;
            updateSaturation(selectedVertex, colors, saturationSets);
        }

        return colors;
    }

    /**
     * 更新饱和度集合
     */
    private void updateSaturation(int coloredVertex, int[] colors, Set<Integer>[] saturationSets) {
        for (int neighbor : adjacencyList.get(coloredVertex)) {
            if (colors[neighbor] == -1) {
                saturationSets[neighbor].add(colors[coloredVertex]);
            }
        }
    }

    /**
     * 普通贪心着色(按顶点编号顺序)
     * 用于与Welsh-Powell和DSatur对比
     */
    public int[] simpleGreedyColoring() {
        int[] colors = new int[vertices];
        Arrays.fill(colors, -1);

        for (int v = 0; v < vertices; v++) {
            boolean[] usedColors = new boolean[vertices];
            for (int neighbor : adjacencyList.get(v)) {
                if (colors[neighbor] != -1) {
                    usedColors[colors[neighbor]] = true;
                }
            }

            for (int c = 0; c < vertices; c++) {
                if (!usedColors[c]) {
                    colors[v] = c;
                    break;
                }
            }
        }

        return colors;
    }

    /**
     * 回溯法精确求解色数(仅适用于小规模图 V <= 20)
     * @return 最优着色方案
     */
    public int[] exactColoring() {
        if (vertices > 20) {
            throw new IllegalArgumentException("精确算法仅适用于顶点数<=20的图");
        }

        int[] bestColors = new int[vertices];
        int[] currentColors = new int[vertices];
        Arrays.fill(bestColors, -1);

        int[] bestColorCount = {vertices + 1};

        backtrack(0, currentColors, 0, bestColors, bestColorCount);

        return bestColors;
    }

    private void backtrack(int vertex, int[] currentColors, int colorCount, 
                           int[] bestColors, int[] bestColorCount) {
        if (vertex == vertices) {
            if (colorCount < bestColorCount[0]) {
                bestColorCount[0] = colorCount;
                System.arraycopy(currentColors, 0, bestColors, 0, vertices);
            }
            return;
        }

        // 剪枝:如果当前使用的颜色数已经不小于最优解,停止搜索
        if (colorCount >= bestColorCount[0]) {
            return;
        }

        // 尝试所有已使用的颜色
        boolean[] usedByNeighbors = new boolean[colorCount];
        for (int neighbor : adjacencyList.get(vertex)) {
            if (currentColors[neighbor] != -1 && currentColors[neighbor] < colorCount) {
                usedByNeighbors[currentColors[neighbor]] = true;
            }
        }

        for (int c = 0; c < colorCount; c++) {
            if (!usedByNeighbors[c]) {
                currentColors[vertex] = c;
                backtrack(vertex + 1, currentColors, colorCount, bestColors, bestColorCount);
                currentColors[vertex] = -1;
            }
        }

        // 尝试使用新颜色
        currentColors[vertex] = colorCount;
        backtrack(vertex + 1, currentColors, colorCount + 1, bestColors, bestColorCount);
        currentColors[vertex] = -1;
    }

    /**
     * 计算色数(使用的颜色数量)
     */
    public static int chromaticNumber(int[] colors) {
        int max = -1;
        for (int c : colors) {
            max = Math.max(max, c);
        }
        return max + 1;
    }

    /**
     * 验证着色是否合法
     */
    public boolean isValidColoring(int[] colors) {
        for (int v = 0; v < vertices; v++) {
            if (colors[v] == -1) return false;
            for (int neighbor : adjacencyList.get(v)) {
                if (colors[v] == colors[neighbor]) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * 获取色数的下界(团数)和上界(最大度+1)
     */
    public int[] colorNumberBounds() {
        // 简单上界:最大度 + 1
        int maxDegree = 0;
        for (int v = 0; v < vertices; v++) {
            maxDegree = Math.max(maxDegree, degree(v));
        }
        int upperBound = maxDegree + 1;

        // 简单下界:最大团的大小(用贪心近似)
        int lowerBound = approximateClique();

        return new int[]{lowerBound, upperBound};
    }

    /**
     * 近似最大团大小(作为色数下界)
     */
    private int approximateClique() {
        int maxClique = 1;
        for (int v = 0; v < vertices; v++) {
            Set<Integer> clique = new HashSet<>();
            clique.add(v);
            for (int u = v + 1; u < vertices; u++) {
                boolean connectedToAll = true;
                for (int c : clique) {
                    if (!adjacencyList.get(c).contains(u) && c != u) {
                        connectedToAll = false;
                        break;
                    }
                }
                if (connectedToAll) {
                    clique.add(u);
                }
            }
            maxClique = Math.max(maxClique, clique.size());
        }
        return maxClique;
    }

    /**
     * 打印着色结果(带颜色名称)
     */
    public static void printColoring(int[] colors) {
        String[] colorNames = {"红", "绿", "蓝", "黄", "紫", "橙", "粉", "青", "灰", "棕"};
        for (int i = 0; i < colors.length; i++) {
            String name = colors[i] < colorNames.length ? colorNames[colors[i]] : "C" + colors[i];
            System.out.println("顶点 " + i + " -> " + name + " (" + colors[i] + ")");
        }
        System.out.println("总颜色数: " + chromaticNumber(colors));
    }

    public static void main(String[] args) {
        // 构建示例图(Petersen图的一个子图)
        GraphColoring graph = new GraphColoring(6);
        graph.addEdge(0, 1);
        graph.addEdge(0, 2);
        graph.addEdge(1, 2);
        graph.addEdge(1, 3);
        graph.addEdge(2, 3);
        graph.addEdge(2, 4);
        graph.addEdge(3, 4);
        graph.addEdge(3, 5);
        graph.addEdge(4, 5);

        System.out.println("=== 普通贪心着色 ===");
        int[] simpleColors = graph.simpleGreedyColoring();
        printColoring(simpleColors);
        System.out.println("是否合法: " + graph.isValidColoring(simpleColors));

        System.out.println("\n=== Welsh-Powell着色 ===");
        int[] wpColors = graph.welshPowellColoring();
        printColoring(wpColors);
        System.out.println("是否合法: " + graph.isValidColoring(wpColors));

        System.out.println("\n=== DSatur着色 ===");
        int[] dsColors = graph.dsaturColoring();
        printColoring(dsColors);
        System.out.println("是否合法: " + graph.isValidColoring(dsColors));

        System.out.println("\n=== 色数上下界 ===");
        int[] bounds = graph.colorNumberBounds();
        System.out.println("下界(团数): " + bounds[0] + ", 上界(最大度+1): " + bounds[1]);

        System.out.println("\n=== 精确着色(小规模) ===");
        int[] exactColors = graph.exactColoring();
        printColoring(exactColors);
        System.out.println("是否合法: " + graph.isValidColoring(exactColors));
    }
}

四、复杂度分析

算法 时间复杂度 空间复杂度 特点
普通贪心 O(V²) O(V + E) 按编号顺序,效果一般
Welsh-Powell O(V² + V log V) O(V + E) 按度数排序,效果较好
DSatur O(V³) O(V + E) 动态选择,效果最好
精确回溯 O(V!) O(V) 保证最优,仅适用于小图

五、应用场景

  • 课程表编排:冲突课程分配不同时段,最少时段数即色数
  • 寄存器分配:编译器中冲突变量不能共用寄存器
  • 频谱分配:相邻基站使用不同频段,最小化频段总数
  • 地图着色:相邻区域使用不同颜色
  • 任务调度:冲突任务分配到不同处理器或时间片

六、总结

本文实现了图着色问题的多种求解方法。Welsh-Powell和DSatur都是高效的贪心近似算法,DSatur在多数情况下能得到更少的颜色数。对于小规模图,精确回溯算法可以验证近似算法的质量。图着色作为NP完全问题的典型代表,其近似算法的研究对理解计算复杂性具有重要意义。