web-dev-qa-db-fra.com

Réorganisation de la file d'attente prioritaire Java lors de la modification d'éléments

J'essaie d'implémenter l'algorithme de Dijkstra pour trouver les chemins les plus courts en utilisant une file d'attente prioritaire. À chaque étape de l'algorithme, je supprime le sommet dont la distance est la plus courte de la file d'attente prioritaire, puis met à jour les distances pour chacun de ses voisins dans la file d'attente prioritaire. Maintenant, je lis qu'une file de priorité en Java ne sera pas réorganisée lorsque vous en modifierez les éléments (les éléments qui déterminent l'ordre), j'ai donc essayé de le forcer à réorganiser en insérant et en supprimant un sommet factice. Mais cela ne semble pas fonctionner, et je suis coincé pour essayer de le comprendre. 

Ceci est le code de l'objet sommet et du comparateur

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;
    }
 }

Voici où je lance l'algorithme:

    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);
    }

J'ai exécuté plusieurs casse de texte et je sais que la file d'attente prioritaire ne se réorganise pas correctement chaque fois que je parcours et mets à jour les distances pour les sommets, mais je ne sais pas pourquoi. Ai-je commis une erreur quelque part?

23
novice

Comme vous l'avez découvert, une file d'attente prioritaire ne recourt pas à tous les éléments lorsqu'un élément est ajouté ou supprimé. Cela serait trop coûteux (rappelez-vous la limite inférieure n log n pour le tri de comparaison), alors que toute implémentation de file d'attente à priorité raisonnable (y compris PriorityQueue) promet d'ajouter/de supprimer des nœuds dans O (log n). 

En fait, il ne trie pas du tout ses éléments (c'est pourquoi son itérateur ne peut pas promettre de répéter les éléments dans l'ordre).

PriorityQueue n'offre pas une interface API pour l'informer d'un nœud modifié, car cela l'obligerait à fournir une recherche de nœud efficace, ce que son algorithme sous-jacent ne prend pas en charge. L'implémentation d'une file d'attente prioritaire est très complexe. L’article Wikipedia sur PriorityQueues pourrait être un bon point de départ pour en savoir plus à ce sujet. Je ne suis pas certain qu'une telle mise en œuvre serait plus rapide, cependant.

Une idée simple consiste à supprimer puis à ajouter le nœud modifié. Do not faites cela en tant que remove() prend O (n). Au lieu de cela, insérez une autre entrée pour le même nœud dans PriorityQueue et ignorez les doublons lors de l'interrogation de la file d'attente, c'est-à-dire procédez comme suit:

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

}

Edit: Il n’est pas conseillé de changer les priorités des éléments du PQ, d’où la nécessité d’insérer Steps au lieu de Nodes.

19
meriton

vous devrez supprimer et réinsérer chaque élément édité. (L'élément réel, et pas un factice!). ainsi, chaque fois que vous mettez à jour distances, vous devez supprimer et ajouter les éléments affectés par l'entrée modifiée.

autant que je sache, ceci n'est pas propre à Java, mais chaque file d'attente prioritaire exécutée à O(logn) pour tous les ops doit fonctionner de cette façon.

4
amit

L'inconvénient de la PriorityQueue de Java est que remove(Object) a besoin de O(n) temps, ce qui entraîne un temps de O(n) si vous souhaitez mettre à jour les priorités en supprimant et en ajoutant de nouveaux éléments. Cela peut toutefois être fait à temps O(log(n)). N'ayant pas réussi à trouver une implémentation fonctionnelle via Google, j'ai essayé de l'implémenter moi-même (avec Kotlin, car je préfère vraiment ce langage à Java) en utilisant TreeSet. Cela semble fonctionner et devrait avoir O(log(n)) pour ajouter/mettre à jour/supprimer (la mise à jour se fait via 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)
    }

}
3
winterriver

Vous pouvez éviter de mettre à jour des éléments de la file d'attente en marquant chaque nœud comme étant {visited = = false} par défaut et en ajoutant de nouveaux éléments à la file au fur et à mesure. 

Retirez ensuite un nœud de la file et ne le traitez que s'il n'a pas été visité auparavant. 

L'algorithme de Dijkstra garantit que chaque nœud n'est visité qu'une seule fois. Ainsi, même si vous avez des nœuds obsolètes dans la file d'attente, vous ne les traitez jamais vraiment.

En outre, il est probablement plus facile de séparer les éléments internes de l'algorithme de la structure de données du graphique.

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));
                }
            }
        }
    }
}
2
Savino Sguera

Le problème est que vous mettez à jour le tableau distances, mais pas l'entrée correspondante dans queue. Pour mettre à jour les objets appropriés dans la file d'attente, vous devez supprimer, puis ajouter.

0
Petar Ivanov

Je résous ce problème en divisant mon processus en timeSlots (un planificateur d'heure conviendra parfaitement) et en étendant la PriorityQueue native. Donc, j'implémente une méthode notify où la clé de cette méthode est le code suivant:

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

J'espère que ça a aidé.

0
Evan P