每日算法 — 使用java实现配对堆:自适应优先队列与Dijkstra加速

引言

优先队列是图算法中最核心的辅助数据结构之一。二叉堆以其简洁的实现和稳定的O(log n)操作复杂度成为首选,但在实际应用中,尤其是涉及大量decrease-key操作的场景(如Dijkstra、Prim算法),二叉堆并非最优选择。

配对堆(Pairing Heap)由Fredman、Sedgewick、Sleator和Tarjan于1986年提出,是一种自适应的堆结构。它在实际运行中往往比斐波那契堆更快(尽管理论复杂度尚不完全确定),同时实现远比斐波那契堆简单。本文将用Java实现配对堆,并展示其如何加速Dijkstra算法。

核心原理

配对堆是一种多叉堆,通过”配对合并”(pairing merge)操作维护堆性质。其核心操作极其简单:

  • merge(合并):将两个堆的根节点中较小者作为新根,较大者成为其第一个子节点。
  • insert(插入):将新节点与原堆合并。
  • delete-min(删除最小值):移除根节点后,将其所有子节点通过两两配对合并的方式重新组织。
  • decrease-key(减小键值):如果新键值仍满足堆序,只需更新值;否则剪断该节点,将其与原堆合并。

删除最小值时的两两配对合并策略:
1. 从左到右,相邻两个子树两两合并(第一轮配对)。
2. 从右到左,将配对结果依次合并(第二轮累积合并)。

Java完整实现

import java.util.*;

/**
 * 配对堆(Pairing Heap)实现
 * 一种自适应优先队列,decrease-key操作实际效率极高
 * @param <T> 元素类型
 */
public class PairingHeap<T extends Comparable<T>> {

    // 堆节点
    static class Node<T> {
        T key;                    // 键值
        Node<T> child;           // 第一个子节点
        Node<T> sibling;         // 右兄弟节点
        Node<T> parent;          // 父节点(用于decrease-key)

        Node(T key) {
            this.key = key;
        }
    }

    private Node<T> root;        // 根节点
    private int size;            // 节点数量

    public PairingHeap() {
        this.root = null;
        this.size = 0;
    }

    /**
     * 判断堆是否为空
     */
    public boolean isEmpty() {
        return root == null;
    }

    /**
     * 获取堆中元素数量
     */
    public int size() {
        return size;
    }

    /**
     * 查看最小元素(不移除)
     */
    public T findMin() {
        if (root == null) {
            throw new NoSuchElementException("堆为空");
        }
        return root.key;
    }

    /**
     * 合并两个配对堆,返回新堆的根
     * 核心操作:较小的根成为新根
     */
    private Node<T> merge(Node<T> h1, Node<T> h2) {
        if (h1 == null) return h2;
        if (h2 == null) return h1;

        // h1的根更小,h2成为h1的第一个子节点
        if (h1.key.compareTo(h2.key) <= 0) {
            h2.sibling = h1.child;
            h1.child = h2;
            h2.parent = h1;
            return h1;
        } else {
            // h2的根更小
            h1.sibling = h2.child;
            h2.child = h1;
            h1.parent = h2;
            return h2;
        }
    }

    /**
     * 插入新元素
     */
    public Node<T> insert(T key) {
        Node<T> newNode = new Node<>(key);
        root = merge(root, newNode);
        size++;
        return newNode;
    }

    /**
     * 删除并返回最小元素
     * 核心:将根的所有子节点通过两两配对合并重新组织
     */
    public T deleteMin() {
        if (root == null) {
            throw new NoSuchElementException("堆为空");
        }

        T minKey = root.key;
        Node<T> firstChild = root.child;

        // 两两配对合并(从左到右)
        Node<T> newRoot = mergePairs(firstChild);
        root = newRoot;
        size--;

        return minKey;
    }

    /**
     * 两两配对合并子节点列表
     * 第一轮:从左到右相邻两个合并
     * 第二轮:从右到左依次累积合并
     */
    private Node<T> mergePairs(Node<T> firstChild) {
        if (firstChild == null) return null;
        if (firstChild.sibling == null) return firstChild;

        // 第一轮:从左到右两两合并
        Node<T> pairs = null;  // 配对结果链表的头
        Node<T> pairsTail = null;

        Node<T> curr = firstChild;
        while (curr != null && curr.sibling != null) {
            Node<T> next = curr.sibling;
            Node<T> afterNext = next.sibling;

            // 断开连接
            curr.sibling = null;
            next.sibling = null;

            // 合并curr和next
            Node<T> merged = merge(curr, next);

            // 加入配对结果链表
            if (pairs == null) {
                pairs = merged;
            } else {
                pairsTail.sibling = merged;
            }
            pairsTail = merged;

            curr = afterNext;
        }

        // 处理奇数个的情况,最后一个单独加入
        if (curr != null) {
            if (pairs == null) {
                pairs = curr;
            } else {
                pairsTail.sibling = curr;
            }
            pairsTail = curr;
        }

        // 第二轮:从右到左累积合并
        // 找到尾部
        if (pairs == null) return null;

        // 将链表反转以便从右到左处理,或者递归处理
        // 这里采用递归方式:将链表分为第一个和剩余部分
        return mergePairsRecursive(pairs);
    }

    /**
     * 递归地从右到左合并配对结果
     */
    private Node<T> mergePairsRecursive(Node<T> node) {
        if (node == null || node.sibling == null) {
            return node;
        }

        // 先合并后面的
        Node<T> rest = mergePairsRecursive(node.sibling);
        node.sibling = null;
        return merge(node, rest);
    }

    /**
     * 减小指定节点的键值
     * 这是配对堆最大的优势所在
     */
    public void decreaseKey(Node<T> node, T newKey) {
        if (newKey.compareTo(node.key) >= 0) {
            throw new IllegalArgumentException("新键值必须小于当前键值");
        }

        node.key = newKey;

        // 如果节点是根,直接完成
        if (node == root) {
            return;
        }

        // 将节点从其父节点中剪断
        // 找到node在父节点的子节点列表中的位置
        Node<T> parent = node.parent;
        if (parent != null) {
            if (parent.child == node) {
                parent.child = node.sibling;
            } else {
                // 在兄弟链表中查找
                Node<T> prev = parent.child;
                while (prev != null && prev.sibling != node) {
                    prev = prev.sibling;
                }
                if (prev != null) {
                    prev.sibling = node.sibling;
                }
            }
        }

        node.sibling = null;
        node.parent = null;

        // 将剪断的子树与原堆合并
        root = merge(root, node);
    }

    /**
     * 中序遍历打印(用于调试)
     */
    public void printHeap() {
        System.out.println("堆大小: " + size);
        if (root != null) {
            System.out.print("堆结构: ");
            printNode(root, 0);
            System.out.println();
        }
    }

    private void printNode(Node<T> node, int depth) {
        if (node == null) return;
        System.out.println("  ".repeat(depth) + node.key);
        printNode(node.child, depth + 1);
        printNode(node.sibling, depth);
    }

    // ==================== Dijkstra加速应用 ====================

    static class Edge {
        int to, weight;
        Edge(int to, int weight) {
            this.to = to;
            this.weight = weight;
        }
    }

    /**
     * 使用配对堆优化的Dijkstra算法
     */
    public static int[] dijkstraWithPairingHeap(List<Edge>[] graph, int source) {
        int n = graph.length;
        int[] dist = new int[n];
        Arrays.fill(dist, Integer.MAX_VALUE);
        dist[source] = 0;

        // 使用配对堆作为优先队列
        PairingHeap<int[]> heap = new PairingHeap<>();
        // 记录每个顶点在堆中的节点,用于decrease-key
        @SuppressWarnings("unchecked")
        Node<int[]>[] nodes = new Node[n];

        // 初始化:源点距离为0
        nodes[source] = heap.insert(new int[]{0, source});

        while (!heap.isEmpty()) {
            int[] curr = heap.deleteMin();
            int d = curr[0], u = curr[1];

            // 跳过过期条目
            if (d > dist[u]) continue;

            for (Edge e : graph[u]) {
                if (dist[u] + e.weight < dist[e.to]) {
                    dist[e.to] = dist[u] + e.weight;

                    // 如果该顶点已在堆中,执行decrease-key
                    // 否则插入新节点
                    if (nodes[e.to] != null) {
                        heap.decreaseKey(nodes[e.to], new int[]{dist[e.to], e.to});
                    } else {
                        nodes[e.to] = heap.insert(new int[]{dist[e.to], e.to});
                    }
                }
            }
        }

        return dist;
    }

    /**
     * 使用Java标准PriorityQueue的Dijkstra(对比基准)
     */
    public static int[] dijkstraWithPriorityQueue(List<Edge>[] graph, int source) {
        int n = graph.length;
        int[] dist = new int[n];
        Arrays.fill(dist, Integer.MAX_VALUE);
        dist[source] = 0;

        PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a[0]));
        pq.offer(new int[]{0, source});

        while (!pq.isEmpty()) {
            int[] curr = pq.poll();
            int d = curr[0], u = curr[1];

            if (d > dist[u]) continue;

            for (Edge e : graph[u]) {
                if (dist[u] + e.weight < dist[e.to]) {
                    dist[e.to] = dist[u] + e.weight;
                    pq.offer(new int[]{dist[e.to], e.to});
                }
            }
        }

        return dist;
    }

    public static void main(String[] args) {
        System.out.println("=== 配对堆基础测试 ===");
        PairingHeap<Integer> heap = new PairingHeap<>();

        // 插入测试
        int[] values = {5, 3, 8, 1, 9, 2, 7};
        @SuppressWarnings("unchecked")
        Node<Integer>[] nodes = new Node[values.length];
        for (int i = 0; i < values.length; i++) {
            nodes[i] = heap.insert(values[i]);
            System.out.println("插入 " + values[i] + ", 最小值: " + heap.findMin());
        }

        // decrease-key测试
        System.out.println("\n将 9 减小到 0");
        heap.decreaseKey(nodes[4], 0);  // 将9减小到0
        System.out.println("decrease-key后最小值: " + heap.findMin());

        // 删除最小值测试
        System.out.println("\n依次删除最小值:");
        while (!heap.isEmpty()) {
            System.out.print(heap.deleteMin() + " ");
        }
        System.out.println();

        System.out.println("\n=== Dijkstra性能对比测试 ===");
        int n = 1000;
        int m = 5000;
        Random rand = new Random(42);

        @SuppressWarnings("unchecked")
        List<Edge>[] graph = new ArrayList[n];
        for (int i = 0; i < n; i++) graph[i] = new ArrayList<>();

        for (int i = 0; i < m; i++) {
            int u = rand.nextInt(n);
            int v = rand.nextInt(n);
            int w = rand.nextInt(100) + 1;
            graph[u].add(new Edge(v, w));
            graph[v].add(new Edge(u, w));  // 无向图
        }

        long start1 = System.currentTimeMillis();
        int[] dist1 = dijkstraWithPairingHeap(graph, 0);
        long time1 = System.currentTimeMillis() - start1;

        long start2 = System.currentTimeMillis();
        int[] dist2 = dijkstraWithPriorityQueue(graph, 0);
        long time2 = System.currentTimeMillis() - start2;

        // 验证结果一致
        boolean match = Arrays.equals(dist1, dist2);
        System.out.println("结果一致性: " + match);
        System.out.println("配对堆Dijkstra耗时: " + time1 + "ms");
        System.out.println("优先队列Dijkstra耗时: " + time2 + "ms");
    }
}

复杂度分析

配对堆的理论复杂度至今仍是开放问题,但已证明和实际观测的复杂度如下:

操作 均摊复杂度 实际表现
insert O(1) 极快,仅需一次合并
find-min O(1) 直接读取根节点
delete-min O(log n) 两两配对合并非常高效
decrease-key O(1) ~ O(log n) 实际中接近O(1),远快于二叉堆
merge O(1) 单次比较和指针调整

decrease-key的优势:在Dijkstra算法中,标准PriorityQueue不支持decrease-key,需要插入重复节点(造成”过期条目”),堆大小可能达到O(E)。配对堆通过decrease-key将堆大小控制在O(V),对于稠密图优势尤为明显。

应用场景

  1. Dijkstra/Prim加速:大量decrease-key操作的图算法场景。
  2. 任务调度器:支持动态调整任务优先级的调度系统。
  3. 实时系统:实现简单、常数因子小的优先队列。

总结

配对堆以其极简的实现和出色的实际性能,成为优先队列领域的一颗明珠。它没有斐波那契堆复杂的级联剪切(cascading cut)机制,却能在实际运行中匹敌甚至超越后者。对于涉及大量decrease-key操作的算法(如Dijkstra、Prim),配对堆是理想的优先队列实现选择。