【问题标题】:Java Priority Queue reordering when editing elements编辑元素时Java优先队列重新排序
【发布时间】:2011-10-20 15:02:54
【问题描述】:

我正在尝试实现 Dijkstra 的算法,以使用优先级队列查找最短路径。在算法的每一步中,我从优先级队列中删除距离最短的顶点,然后更新优先级队列中每个邻居的距离。现在我读到,当您编辑其中的元素(确定排序的元素)时,Java 中的优先级队列不会重新排序,所以我试图通过插入和删除一个虚拟顶点来强制它重新排序。但这似乎不起作用,我一直试图弄清楚。

这是顶点对象和比较器的代码

class vertex {
    int v, d;
    public vertex(int num, int dis) {
        v=num;
        d=dis;
    }
}

class VertexComparator implements Comparator {
    public int compare (Object a, Object b) {
        vertex v1 = (vertex)a;
        vertex v2 = (vertex)b;
        return v1.d-v2.d;
    }
 }

这是我运行算法的地方:

    int[] distances=new int[p];
    Comparator<vertex> comparator = new VertexComparator();
    PriorityQueue<vertex> queue = new PriorityQueue<vertex>(p, comparator);
    for(int i=0; i<p; i++) {
        if(i!=v) {
            distances[i]=MAX;
        }
        else {
            distances[i]=0;
        }
        queue.add(new vertex(i, distances[i]));
    }
    // run dijkstra
    for(int i=0; i<p; i++) {
        vertex cur=queue.poll();
        Iterator itr = queue.iterator();
        while(itr.hasNext()) {
            vertex test = (vertex)(itr.next());
            if(graph[cur.v][test.v]!=-1) {
                test.d=Math.min(test.d, cur.d+graph[cur.v][test.v]);
                distances[test.v]=test.d;
            }
        }
        // force the PQ to resort by adding and then removing a dummy vertex
        vertex resort = new vertex(-1, -1);
        queue.add(resort);
        queue.remove(resort);
    }

我已经运行了几个文本案例,并且我知道每次通过并更新顶点距离时优先级队列都没有正确重新排序,但我不知道为什么。我是不是在某个地方出错了?

【问题讨论】:

标签: java priority-queue


【解决方案1】:

您必须删除并重新插入每个已编辑的元素。 (实际元素,而不是虚拟元素!)。因此,每次更新 distances 时,都需要删除和添加受更改的主条目影响的元素。

据我所知,这不是 Java 独有的,但是对于所有操作以 O(logn) 运行的每个优先级队列都必须以这种方式工作。

【讨论】:

  • remove(Object o) 不必是 O(n)。只是 Java 库使用天真的 O(n) 方法来定位要删除的元素。相反,如果您使用像地图这样的索引结构来存储元素在堆中的位置,则可以在 O(log(n)) 中执行 remove
  • remove(Object) 取 O(n),但 remove() 和 add 取 O(log(N)) docs.oracle.com/javase/7/docs/api/java/util/…
【解决方案2】:

问题是您更新了distances 数组,但没有更新queue 中的相应条目。要更新队列中相应的对象,您需要先移除再添加。

【讨论】:

    【解决方案3】:

    正如您所发现的,无论何时添加或删除元素,优先级队列都不会处理所有元素。这样做太昂贵了(记住比较排序的 n log n 下限),而任何合理的优先级队列实现(包括PriorityQueue)都承诺在 O(log n) 中添加/删除节点。

    事实上,它根本不对它的元素进行排序(这就是它的迭代器不能保证按排序顺序迭代元素的原因)。

    PriorityQueue 不提供 api 来通知它有关更改的节点,因为这需要它提供有效的节点查找,其底层算法不支持。实现一个优先级队列是相当复杂的。 Wikipedia article on PriorityQueues 可能是一个很好的阅读起点。不过,我不确定这样的实现会更快。

    一个简单的想法是删除然后添加更改的节点。 不要这样做,因为remove() 需要 O(n)。相反,将同一节点的另一个条目插入 PriorityQueue,并在轮询队列时忽略重复项,即执行以下操作:

    PriorityQueue<Step> queue = new PriorityQueue();
    
    void findShortestPath(Node start) {
        start.distance = 0;
        queue.addAll(start.steps());
    
        Step step;
        while ((step = queue.poll()) != null) {
            Node node = step.target;
            if (!node.reached) {
                node.reached = true;
                node.distance = step.distance;
                queue.addAll(node.steps());
            }
        }
    
    }
    

    编辑:不建议更改 PQ 中元素的优先级,因此需要插入 Steps 而不是 Nodes。

    【讨论】:

    • 太好了,我也有同样的想法,但这不会将算法的运行时间从 O(m log n) 更改为 O(m log m)。因为堆现在将包含重复元素,几乎等于图中 O(m) 中的边数。我认为这不是最好的解决方案。
    • 路径查找图通常是平面的,因此具有恒定的平均邻居数。也就是说,PQ 的大小将增加一个常数因子,它的高度将增加一个常数偏移量。这应该不会对运行时产生太大影响。另一方面,为了有效地修改节点,调用者需要拥有每个节点的句柄。在 Java 中,这只能通过对象引用来完成,这需要 PriorityQueue 为每个条目存储一个节点对象。与使用数组索引对其结构进行编码的优先级队列相比,这又会占用更多的内存。
    • 因此,哪个实现会更快真的很模糊。
    • 在必须保持时间复杂度的情况下,这不是最佳解决方案,但在其他情况下,它已经足够了,而不是实现您自己的或使用其他一些 ds。
    【解决方案4】:

    您可以避免更新队列中的项目,只需将每个节点默认标记为 visited=false,然后在执行过程中将新项目添加到队列中。

    然后从队列中弹出一个节点并仅在之前未访问过的情况下对其进行处理。

    Dijkstra 的算法保证每个节点只被访问一次,因此即使队列中可能有陈旧的节点,您也永远不会真正处理它们。

    如果将算法内部与图形数据结构分开,可能会更容易。

    public void dijkstra(Node source) throws Exception{
        PriorityQueue q = new PriorityQueue();
        source.work.distance = 0;
        q.add(new DijkstraHeapItem(source));
    
        while(!q.isEmpty()){
            Node n = ((DijkstraHeapItem)q.remove()).node;
            Work w = n.work;
    
            if(!w.visited){
                w.visited = true;
    
                Iterator<Edge> adiacents = n.getEdgesIterator();
                while(adiacents.hasNext()){
                    Edge e = adiacents.next();
                    if(e.weight<0) throw new Exception("Negative weight!!");
                    Integer relaxed = e.weight + w.distance;
    
                    Node t = e.to;
                    if (t.work.previous == null || t.work.distance > relaxed){
                        t.work.distance = relaxed;
                        t.work.previous = n;
                        q.add(new DijkstraHeapItem(t));
                    }
                }
            }
        }
    }
    

    【讨论】:

    • 我不明白 - 如果您从队列中 .remove() 一个节点,您将触发队列的重新排序,从而违背设置 .visited = true 的目的?
    【解决方案5】:

    我通过将我的进程划分为 timeSlots(一个时间调度程序就可以了)并扩展本机 PriorityQueue 来解决这个问题。所以我实现了一个 notify 方法,这个方法的关键是下面的代码:

    // If queue has one or less elements, then it shouldn't need an ordering
    // procedure
    if (size() > 1)
    {
        // holds the current size, as during this process the size will
        // be vary
        int tmpSize = size();
        for (int i = 1; i < tmpSize; i++)
        {
            add(poll());
        }
    }
    

    我希望它有所帮助。

    【讨论】:

      【解决方案6】:

      Java 的 PriorityQueue 的缺点是 remove(Object) 需要 O(n) 时间,如果您想通过再次删除和添加元素来更新优先级,则需要 O(n) 时间。然而,它可以在 O(log(n)) 时间内完成。由于我无法通过 Google 找到可行的实现,因此我尝试使用 TreeSet 自己实现它(尽管在 Kotlin 中,因为我真的更喜欢这种语言而不是 Java)。 这似乎可行,并且应该有 O(log(n)) 用于添加/更新/删除(更新通过 add 完成):

      // update priority by adding element again (old occurrence is removed automatically)
      class DynamicPriorityQueue<T>(isMaxQueue: Boolean = false) {
      
          private class MyComparator<A>(val queue: DynamicPriorityQueue<A>, isMaxQueue: Boolean) : Comparator<A> {
              val sign = if (isMaxQueue) -1 else 1
      
              override fun compare(o1: A, o2: A): Int {
                  if (o1 == o2)
                      return 0
                  if (queue.priorities[o2]!! - queue.priorities[o1]!! < 0)
                      return sign
                  return -sign
              }
      
          }
      
          private val priorities = HashMap<T, Double>()
          private val treeSet = TreeSet<T>(MyComparator(this, isMaxQueue))
      
          val size: Int
              get() = treeSet.size
      
          fun isEmpty() = (size == 0)
      
          fun add(newElement: T, priority: Double) {
              if (newElement in priorities)
                  treeSet.remove(newElement)
              priorities[newElement] = priority
              treeSet.add(newElement)
          }
      
          fun remove(element: T) {
              treeSet.remove(element)
              priorities.remove(element)
          }
      
          fun getPriorityOf(element: T): Double {
              return priorities[element]!!
          }
      
      
          fun first(): T = treeSet.first()
          fun poll(): T {
              val res = treeSet.pollFirst()
              priorities.remove(res)
              return res
          }
      
          fun pollWithPriority(): Pair<T, Double> {
              val res = treeSet.pollFirst()
              val priority = priorities[res]!!
              priorities.remove(res)
              return Pair(res, priority)
          }
      
      }
      

      【讨论】:

      • 您可以使用树图而不是树集。
      • 我认为这行不通。优先级队列的一个核心特性是它允许重复,而集合则不允许。据我所知,您试图通过将条目视为“不相等”来允许重复,除非它们的指针在您的比较器中匹配。我在纯 Java 中尝试了类似的实现,并且该策略似乎有效,但是,如果存在具有相同优先级的不同元素,它有时无法删除集合中的元素。
      • 我很确定,当与具有相同优先级的元素进行比较时,在树中搜索要删除的元素时会偶尔走错分支。如果我在这里错了,请纠正我,但我认为 TreeSets 不能被改装以允许重复。
      【解决方案7】:

      我实现了一个自适应 MinHeap,它支持在更新对象优先级时重新排序 (O(nlogn)),用 Python 编写。

      class Node:
          """
          Model an object in Heap.
          """
      
          def __init__(self, key, val, i=-1) -> None:
              self.key = key  # object ID
              self.val = val  # object priority
              self.i = i  # index in heap array
      
      
      class AdaptiveMinHeap:
          """
          Heap for objects. Support reorderining when objects' priority are updated.
          """
      
          def __init__(self) -> None:
              self.hp = {0: Node(-1, -1, 0)}  # Use dict to simulate list (key as the index) to support efficient reordering.
              self.d = dict()
      
          def __len__(self):
              return len(self.hp)-1
      
          def _swap(self, anode, bnode):
              d = self.d
              anode.key, bnode.key = bnode.key, anode.key
              anode.val, bnode.val = bnode.val, anode.val
              d[anode.key] = anode
              d[bnode.key] = bnode
      
          def _swim(self, i):
              hp = self.hp
              while i//2 > 0 and hp[i].val < hp[i//2].val:
                  self._swap(hp[i], hp[i//2])
                  i = i//2
      
          def _sink(self, i):
              hp = self.hp
              while i*2 < len(hp):
                  if i*2 + 1 >= len(hp) or hp[i*2+1].val >= hp[i*2].val:
                      min_child = i*2
                  else:
                      min_child = i*2+1
                  if hp[min_child].val < hp[i].val:
                      self._swap(hp[min_child], hp[i])
                  i = min_child
      
          def push(self, key, val):
              hp = self.hp
              d = self.d
              if key in d:
                  self.remove(key)
              node = Node(key, val, len(hp))
              d[key] = node
              hp[node.i] = node
              self._swim(node.i)
      
          def pop(self):
              hp = self.hp
              if len(hp) > 1:
                  return self.remove(hp[1].key)
      
          def remove(self, key):
              hp = self.hp
              d = self.d
              node = d[key]
              self._swap(hp[node.i], hp[len(hp)-1])
              hp.pop(len(hp)-1)
              self._sink(node.i)
              return d.pop(key)
      
      hp = AdaptiveMinHeap()
      hp.push(1, 40)
      hp.push(4, 8900)
      hp.push(2, 500)
      hp.push(3, 1075)
      hp.push(1, 1)
      for _ in range(len(hp)):
          poped = hp.pop()
          print(poped.key, poped.val)
      # 1
      # 500
      # 1075
      # 8900
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-11-01
        • 2014-08-05
        • 1970-01-01
        • 2019-09-18
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多