每日算法 — 使用java实现支配集:图论中的最小支配集与贪心近似算法

支配集问题是图论中的经典NP难问题,在无线网络部署、监控摄像头选址、服务器集群容灾等领域有广泛应用。本文用Java实现最小支配集的贪心近似算法,并深入分析其近似比与优化策略。

一、问题定义

给定无向图G=(V,E),一个支配集D是顶点集V的一个子集,使得对于图中的任意顶点v,要么v∈D,要么v与D中的某个顶点相邻。最小支配集问题要求找到基数最小的支配集。

1.1 相关概念

  • 支配集(Dominating Set):满足上述条件的顶点集合
  • 最小支配集:所有支配集中顶点数最少的一个
  • 连通支配集:诱导子图连通的支配集
  • 独立支配集:集合内顶点两两不相邻的支配集

二、算法原理

由于最小支配集是NP难问题,不存在已知的多项式时间精确算法(除非P=NP)。因此我们采用贪心近似算法:

贪心策略:每次选择能够支配最多未被支配顶点的顶点加入支配集,直到所有顶点都被支配。

该贪心算法的近似比为O(log Δ),其中Δ是图的最大度。

三、Java实现

import java.util.*;

/**
 * 最小支配集问题求解
 * 包含贪心近似算法和精确回溯算法(适用于小规模图)
 */
public class DominatingSet {

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

    public DominatingSet(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);
    }

    /**
     * 获取顶点v的闭邻域(v自身及其所有邻居)
     */
    private Set<Integer> getClosedNeighborhood(int v) {
        Set<Integer> neighborhood = new HashSet<>(adjacencyList.get(v));
        neighborhood.add(v);
        return neighborhood;
    }

    /**
     * 贪心近似算法求解最小支配集
     * 时间复杂度:O(V * E)
     * 近似比:O(log Δ)
     * @return 支配集
     */
    public Set<Integer> greedyDominatingSet() {
        Set<Integer> dominated = new HashSet<>(); // 已被支配的顶点
        Set<Integer> dominatingSet = new HashSet<>(); // 结果支配集

        while (dominated.size() < vertices) {
            int bestVertex = -1;
            int maxNewDominated = -1;

            // 遍历所有顶点,找到能支配最多新顶点的顶点
            for (int v = 0; v < vertices; v++) {
                if (dominatingSet.contains(v)) continue;

                Set<Integer> neighborhood = getClosedNeighborhood(v);
                // 计算该顶点能新支配多少顶点
                int newDominated = 0;
                for (int u : neighborhood) {
                    if (!dominated.contains(u)) {
                        newDominated++;
                    }
                }

                if (newDominated > maxNewDominated) {
                    maxNewDominated = newDominated;
                    bestVertex = v;
                }
            }

            // 将最优顶点加入支配集
            dominatingSet.add(bestVertex);
            dominated.addAll(getClosedNeighborhood(bestVertex));
        }

        return dominatingSet;
    }

    /**
     * 优化版贪心算法:选择后立即更新候选集
     */
    public Set<Integer> optimizedGreedyDominatingSet() {
        Set<Integer> undominated = new HashSet<>();
        for (int i = 0; i < vertices; i++) {
            undominated.add(i);
        }

        Set<Integer> dominatingSet = new HashSet<>();

        // 预计算每个顶点的闭邻域
        List<Set<Integer>> closedNeighborhoods = new ArrayList<>();
        for (int i = 0; i < vertices; i++) {
            closedNeighborhoods.add(getClosedNeighborhood(i));
        }

        while (!undominated.isEmpty()) {
            int bestVertex = -1;
            int maxCoverage = -1;

            for (int v = 0; v < vertices; v++) {
                if (dominatingSet.contains(v)) continue;

                // 计算v能覆盖多少未支配顶点
                int coverage = 0;
                for (int u : closedNeighborhoods.get(v)) {
                    if (undominated.contains(u)) {
                        coverage++;
                    }
                }

                if (coverage > maxCoverage) {
                    maxCoverage = coverage;
                    bestVertex = v;
                }
            }

            dominatingSet.add(bestVertex);
            undominated.removeAll(closedNeighborhoods.get(bestVertex));
        }

        return dominatingSet;
    }

    /**
     * 带权最小支配集贪心算法
     * @param weights 每个顶点的权重
     * @return 带权支配集
     */
    public Set<Integer> weightedGreedyDominatingSet(double[] weights) {
        Set<Integer> undominated = new HashSet<>();
        for (int i = 0; i < vertices; i++) {
            undominated.add(i);
        }

        Set<Integer> dominatingSet = new HashSet<>();
        List<Set<Integer>> closedNeighborhoods = new ArrayList<>();
        for (int i = 0; i < vertices; i++) {
            closedNeighborhoods.add(getClosedNeighborhood(i));
        }

        while (!undominated.isEmpty()) {
            int bestVertex = -1;
            double bestRatio = Double.MAX_VALUE;

            for (int v = 0; v < vertices; v++) {
                if (dominatingSet.contains(v)) continue;

                int coverage = 0;
                for (int u : closedNeighborhoods.get(v)) {
                    if (undominated.contains(u)) {
                        coverage++;
                    }
                }

                if (coverage == 0) continue;

                double ratio = weights[v] / coverage;
                if (ratio < bestRatio) {
                    bestRatio = ratio;
                    bestVertex = v;
                }
            }

            dominatingSet.add(bestVertex);
            undominated.removeAll(closedNeighborhoods.get(bestVertex));
        }

        return dominatingSet;
    }

    /**
     * 精确算法(回溯法,仅适用于小规模图 V <= 25)
     * @return 最小支配集
     */
    public Set<Integer> exactBacktracking() {
        if (vertices > 25) {
            throw new IllegalArgumentException("精确算法仅适用于顶点数<=25的图");
        }

        // 将图转换为位掩码表示以加速运算
        int[] neighborMask = new int[vertices];
        for (int v = 0; v < vertices; v++) {
            int mask = 1 << v;
            for (int u : adjacencyList.get(v)) {
                mask |= 1 << u;
            }
            neighborMask[v] = mask;
        }

        int fullMask = (1 << vertices) - 1;
        Set<Integer> bestSolution = null;
        int bestSize = vertices + 1;

        // 按子集大小递增搜索
        for (int size = 1; size <= vertices; size++) {
            Set<Integer> result = backtrack(neighborMask, fullMask, size, 0, 0, 0);
            if (result != null) {
                return result;
            }
        }

        // 如果上面的没找到(理论上不可能),返回全集
        Set<Integer> all = new HashSet<>();
        for (int i = 0; i < vertices; i++) all.add(i);
        return all;
    }

    private Set<Integer> backtrack(int[] neighborMask, int fullMask, 
                                    int targetSize, int start, int currentMask, int depth) {
        if (depth == targetSize) {
            if (currentMask == fullMask) {
                // 找到了有效解,但需要回溯构造集合
                return new HashSet<>(); // 占位,外层处理
            }
            return null;
        }

        for (int i = start; i < vertices; i++) {
            int newMask = currentMask | neighborMask[i];
            Set<Integer> result = backtrack(neighborMask, fullMask, targetSize, i + 1, newMask, depth + 1);
            if (result != null) {
                result.add(i);
                return result;
            }
        }

        return null;
    }

    /**
     * 验证一个集合是否为支配集
     */
    public boolean isDominatingSet(Set<Integer> set) {
        Set<Integer> dominated = new HashSet<>();
        for (int v : set) {
            dominated.addAll(getClosedNeighborhood(v));
        }
        return dominated.size() == vertices;
    }

    /**
     * 独立支配集检测:检查支配集是否为独立集
     */
    public boolean isIndependentSet(Set<Integer> set) {
        for (int v : set) {
            for (int u : set) {
                if (v != u && adjacencyList.get(v).contains(u)) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * 计算支配数(最小支配集的大小)的上下界
     * @return [下界, 上界]
     */
    public int[] dominatingNumberBounds() {
        // 简单下界:ceil(n / (Δ + 1))
        int maxDegree = 0;
        for (int v = 0; v < vertices; v++) {
            maxDegree = Math.max(maxDegree, adjacencyList.get(v).size());
        }
        int lowerBound = (int) Math.ceil((double) vertices / (maxDegree + 1));

        // 上界:贪心算法结果
        int upperBound = greedyDominatingSet().size();

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

    public static void main(String[] args) {
        // 示例:5个节点的路径图 0-1-2-3-4
        DominatingSet graph = new DominatingSet(5);
        graph.addEdge(0, 1);
        graph.addEdge(1, 2);
        graph.addEdge(2, 3);
        graph.addEdge(3, 4);

        System.out.println("=== 贪心近似算法 ===");
        Set<Integer> greedyDS = graph.greedyDominatingSet();
        System.out.println("支配集: " + greedyDS);
        System.out.println("大小: " + greedyDS.size());
        System.out.println("是否为支配集: " + graph.isDominatingSet(greedyDS));

        System.out.println("\n=== 优化版贪心算法 ===");
        Set<Integer> optDS = graph.optimizedGreedyDominatingSet();
        System.out.println("支配集: " + optDS);
        System.out.println("大小: " + optDS.size());

        System.out.println\n("\n=== 支配数上下界 ===");
        int[] bounds = graph.dominatingNumberBounds();
        System.out.println("下界: " + bounds[0] + ", 上界: " + bounds[1]);

        // 带权示例
        System.out.println("\n=== 带权贪心算法 ===");
        double[] weights = {1.0, 2.0, 1.5, 3.0, 1.0};
        Set<Integer> weightedDS = graph.weightedGreedyDominatingSet(weights);
        System.out.println("带权支配集: " + weightedDS);

        // 小规模精确解
        System.out.println("\n=== 精确回溯算法 ===");
        Set<Integer> exactDS = graph.exactBacktracking();
        System.out.println("精确最小支配集: " + exactDS);
        System.out.println("精确支配数: " + exactDS.size());
    }
}

四、复杂度分析

算法 时间复杂度 空间复杂度 近似比
贪心近似算法 O(V² × E) O(V + E) O(log Δ)
优化版贪心 O(V² × E) O(V + E) O(log Δ)
带权贪心 O(V² × E) O(V + E) O(log Δ)
精确回溯 O(2^V) O(V) 1(精确解)

五、应用场景

  • 无线网络:最小化基站/AP数量同时保证全覆盖
  • 安防监控:用最少数量的摄像头覆盖所有区域
  • 社交网络:找到最少的关键用户影响全网
  • 生物信息学:基因调控网络中的核心基因集识别
  • 电路设计:测试点最小化选择

六、总结

本文实现了最小支配集问题的多种求解方法。贪心近似算法在多项式时间内给出较好的近似解,适合大规模图;精确回溯算法适用于小规模图的验证和对比实验。理解支配集问题有助于在实际应用中做出资源部署的最优决策。