每日算法 — 使用java实现次小生成树:最小生成树变种与替换边策略

问题背景

次小生成树(Second Minimum Spanning Tree, SMST)是图论中一个重要的变种问题。给定一个带权无向连通图,要求找出权值仅次于最小生成树(MST)的生成树。该问题在网络冗余设计、备用路由规划、电力系统备份等场景中具有实际意义。

与MST的唯一性判定直接相关:若图存在多棵MST,则次小生成树的权值等于MST的权值。若MST唯一,则需要通过”替换边”策略寻找略大于MST的最优解。

核心算法思想

替换边策略

假设已求得一棵MST T。对于每条非树边 (u, v, w),将其加入 T 会形成一个环。从该环中去掉一条权值最大的树边(不能是刚加入的边),即可得到一棵新的生成树。次小生成树必然存在于这些新生成树之中。

关键问题转化为:对于每条非树边 (u, v),找到MST中 u 到 v 路径上的最大边权。

LCA预处理路径最大值

利用LCA(最近公共祖先)预处理方法,可以在 O(1) 或 O(log n) 时间内回答路径最大值查询。具体做法:
– 从MST的根节点出发进行DFS,记录每个节点的深度
– 使用倍增法预处理 up[k][v](v的2^k级祖先)和 maxEdge[k][v](v到该祖先路径上的最大边权)

严格次小与非严格次小

  • 非严格次小:允许权值等于MST(存在多棵MST时)
  • 严格次小:要求权值严格大于MST

本文实现严格次小生成树,需在路径最大值等于非树边权时,寻找次大值进行替换。

Java完整实现

import java.util.*;

/**
 * 次小生成树求解器
 * 基于Kruskal求MST,再用LCA预处理路径最大边权
 */
public class SecondMST {

    static class Edge implements Comparable<Edge> {
        int u, v, w;
        boolean inMST;  // 是否在MST中
        Edge(int u, int v, int w) {
            this.u = u;
            this.v = v;
            this.w = w;
        }
        @Override
        public int compareTo(Edge o) {
            return Integer.compare(this.w, o.w);
        }
    }

    private int n, m;
    private List<Edge> edges;
    private List<int[]>[] tree;             // MST的邻接表 (to, weight)
    private int[] parent;                   // 并查集
    private int LOG;                        // 倍增层数
    private int[][] up;                     // up[k][v]: v的2^k祖先
    private int[][] maxEdge;                // maxEdge[k][v]: v到2^k祖先路径上的最大边权
    private int[][] secondMaxEdge;          // 严格次大边权
    private int[] depth;

    public SecondMST(int n) {
        this.n = n;
        this.edges = new ArrayList<>();
        this.parent = new int[n + 1];
        this.LOG = 32 - Integer.numberOfLeadingZeros(n);
    }

    public void addEdge(int u, int v, int w) {
        edges.add(new Edge(u, v, w));
    }

    private int find(int x) {
        return parent[x] == x ? x : (parent[x] = find(parent[x]));
    }

    private boolean union(int x, int y) {
        int fx = find(x), fy = find(y);
        if (fx == fy) return false;
        parent[fx] = fy;
        return true;
    }

    /**
     * Kruskal算法求MST
     * @return MST的总权值
     */
    public long kruskal() {
        for (int i = 1; i <= n; i++) parent[i] = i;
        Collections.sort(edges);
        tree = new ArrayList[n + 1];
        for (int i = 1; i <= n; i++) tree[i] = new ArrayList<>();

        long mstWeight = 0;
        int count = 0;
        for (Edge e : edges) {
            if (union(e.u, e.v)) {
                e.inMST = true;
                mstWeight += e.w;
                tree[e.u].add(new int[]{e.v, e.w});
                tree[e.v].add(new int[]{e.u, e.w});
                count++;
                if (count == n - 1) break;
            }
        }
        return mstWeight;
    }

    /**
     * DFS预处理深度和倍增表
     */
    private void dfs(int u, int fa, int w) {
        up[0][u] = fa;
        maxEdge[0][u] = w;
        secondMaxEdge[0][u] = -1; // 到父节点只有一条边,无次大值

        for (int[] next : tree[u]) {
            int v = next[0], weight = next[1];
            if (v == fa) continue;
            depth[v] = depth[u] + 1;
            dfs(v, u, weight);
        }
    }

    /**
     * 构建LCA倍增表
     */
    public void buildLCA() {
        up = new int[LOG][n + 1];
        maxEdge = new int[LOG][n + 1];
        secondMaxEdge = new int[LOG][n + 1];
        depth = new int[n + 1];

        depth[1] = 0;
        dfs(1, 0, 0);

        for (int k = 1; k < LOG; k++) {
            for (int v = 1; v <= n; v++) {
                int mid = up[k - 1][v];
                up[k][v] = up[k - 1][mid];
                // 合并两段路径的最大和次大值
                int[] candidates = {maxEdge[k-1][v], secondMaxEdge[k-1][v],
                                    maxEdge[k-1][mid], secondMaxEdge[k-1][mid]};
                maxEdge[k][v] = -1;
                secondMaxEdge[k][v] = -1;
                for (int val : candidates) {
                    if (val > maxEdge[k][v]) {
                        secondMaxEdge[k][v] = maxEdge[k][v];
                        maxEdge[k][v] = val;
                    } else if (val != maxEdge[k][v] && val > secondMaxEdge[k][v]) {
                        secondMaxEdge[k][v] = val;
                    }
                }
            }
        }
    }

    /**
     * 查询u到v路径上的最大边权和严格次大边权
     */
    private int[] queryMax(int u, int v) {
        int max1 = -1, max2 = -1; // max1为最大,max2为严格次大

        if (depth[u] < depth[v]) {
            int tmp = u; u = v; v = tmp;
        }

        // 将u提升到与v同一深度
        int diff = depth[u] - depth[v];
        for (int k = LOG - 1; k >= 0; k--) {
            if ((diff >> k & 1) == 1) {
                int[] candidates = {max1, max2, maxEdge[k][u], secondMaxEdge[k][u]};
                max1 = -1; max2 = -1;
                for (int val : candidates) {
                    if (val > max1) { max2 = max1; max1 = val; }
                    else if (val != max1 && val > max2) { max2 = val; }
                }
                u = up[k][u];
            }
        }

        if (u == v) return new int[]{max1, max2};

        for (int k = LOG - 1; k >= 0; k--) {
            if (up[k][u] != up[k][v]) {
                int[] candidates = {max1, max2, maxEdge[k][u], secondMaxEdge[k][u],
                                    maxEdge[k][v], secondMaxEdge[k][v]};
                max1 = -1; max2 = -1;
                for (int val : candidates) {
                    if (val > max1) { max2 = max1; max1 = val; }
                    else if (val != max1 && val > max2) { max2 = val; }
                }
                u = up[k][u];
                v = up[k][v];
            }
        }

        // 最后一步到LCA
        int[] candidates = {max1, max2, maxEdge[0][u], secondMaxEdge[0][u],
                            maxEdge[0][v], secondMaxEdge[0][v]};
        max1 = -1; max2 = -1;
        for (int val : candidates) {
            if (val > max1) { max2 = max1; max1 = val; }
            else if (val != max1 && val > max2) { max2 = val; }
        }

        return new int[]{max1, max2};
    }

    /**
     * 求解严格次小生成树
     * @return [MST权值, 严格次小生成树权值],若不存在返回null
     */
    public long[] solve() {
        long mstWeight = kruskal();
        buildLCA();

        long secondMST = Long.MAX_VALUE;
        for (Edge e : edges) {
            if (e.inMST) continue; // 只考虑非树边
            int[] res = queryMax(e.u, e.v);
            int maxOnPath = res[0];
            int secondMaxOnPath = res[1];

            if (e.w > maxOnPath) {
                // 用非树边替换最大边,得到更大的生成树
                secondMST = Math.min(secondMST, mstWeight - maxOnPath + e.w);
            } else if (e.w == maxOnPath && secondMaxOnPath != -1) {
                // 需要严格次大值替换
                secondMST = Math.min(secondMST, mstWeight - secondMaxOnPath + e.w);
            }
        }

        if (secondMST == Long.MAX_VALUE) return null;
        return new long[]{mstWeight, secondMST};
    }

    public static void main(String[] args) {
        // 示例图:5个节点,7条边
        SecondMST solver = new SecondMST(5);
        solver.addEdge(1, 2, 1);
        solver.addEdge(1, 3, 2);
        solver.addEdge(2, 3, 3);
        solver.addEdge(2, 4, 4);
        solver.addEdge(3, 4, 5);
        solver.addEdge(3, 5, 6);
        solver.addEdge(4, 5, 7);

        long[] result = solver.solve();
        if (result != null) {
            System.out.println("MST权值: " + result[0]);
            System.out.println("严格次小生成树权值: " + result[1]);
        } else {
            System.out.println("不存在次小生成树");
        }

        // 验证:MST应为 1+2+4+6 = 13
        // 次小生成树:用边(3,4,5)替换(2,4,4),得到 1+2+5+6 = 14
        // 或用边(2,3,3)替换(1,3,2),得到 1+3+4+6 = 14
    }
}

算法复杂度分析

  • Kruskal求MST:O(m log m),m为边数
  • DFS预处理:O(n)
  • 倍增表构建:O(n log n)
  • 枚举非树边:共 m - n + 1 条,每条查询 O(log n)
  • 总时间复杂度:O(m log m + m log n)
  • 空间复杂度:O(n log n + m)

算法扩展

第K小生成树

将边按权值排序后,利用MST的基尔霍夫矩阵树定理或拟阵交理论,可以推广到第K小生成树问题,复杂度显著上升。

带约束的生成树

次小生成树的思路可推广到度约束生成树、边数受限生成树等问题,核心仍是”替换边”策略配合适当的数据结构。

次小生成树展示了如何将MST作为基准,通过局部替换探索次优解。这种”基准+微调”的策略在组合优化中十分普遍,是理解更复杂优化问题的重要阶梯。