UK Flag English version

TD 1 Optimisation Combinatoire: Graphes

Rappels


Exercice 1: Graphe dirigé

  1. Complétez le fichier graph.h et implémentez-le dans un fichier graph.cc dont voilà un début de squelette.

    Attention: chaque méthode devra avoir une complexité O(1).
    
    #include <vector>
    using std::vector;    
    // Un graphe dirigé est un ensemble de noeuds, liés par des arcs.
    // Chaque arc a une direction: il va d'un noeud vers un autre.
    // Dans cette implementation, les noeuds sont les entiers {0 .. num_nodes-1}.
    //
    // Par exemple, ce graphe:
    //
    //  0 <--- 1 <--> 3 ---> 4
    //  ^      |       \     ^
    //  |      v        `----'
    //  '----- 2
    //
    // Peut être obtenu en faisant:
    //   DirectedGraph g(5);
    //   g.AddArc(1, 0);
    //   g.AddArc(1, 3);
    //   g.AddArc(3, 1);
    //   g.AddArc(2, 0);
    //   g.AddArc(1, 2);
    //   g.AddArc(3, 4);
    //   g.AddArc(3, 4);
    //
    class DirectedGraph {
     public:
      // Après sa construction, un graphe n'a initialement pas d'arcs.
      explicit DirectedGraph(int num_nodes);
    
      int NumNodes() const;  // Renvoie le nombre de noeuds
    
      void AddArc(int from, int to);  // Ajoute un arc from → to
    
      // Renvoie le nombre d'arcs sortant du noeud "node".
      // Dans l'exemple ci-dessus, OutDegree(1) = 3, OutDegree(4) = 0.
      int OutDegree(int node) const;
    
      // Renvoie tous les voisins sortants de "node", i.e. tous les
      // noeuds X qui ont été ajoutés avec AddAdrc(node, X).
      const vector<int>& Neighbors(int node) const;
    
     private:
      // TODO
    };
    Testez votre code:
    rm test.tar.gz; wget --no-cache http://fabien.viger.free.fr/oc/td1/test.tar.gz
    tar xf test.tar.gz
    make g1
    RENDU: graph.h, graph.cc

  2. Ajoutez une fonction qui renvoie le nombre d'arcs: int NumArcs() const
    Comment faire pour qu'elle tourne en O(1)? Vous pourrez adapter la classe, mais attention, la complexité de toutes les opérations doit rester O(1).

    Test: make g2

    RENDU: graph.h, graph.cc avec la nouvelle fonction (déclarée dans le .h et définie dans le .cc).
    N'ajoutez pas des 2ème fichiers graph.h, graph.cc! Modifiez simplement les fichiers obtenus précédemment. Les exercices sont progressifs: au fur et à mesure, vos fichiers vont passer de plus en plus de tests.

  3. Ajoutez une fonction void MakeSimple() qui éliminera toutes les boucles et multi-arcs du graphe, en le rendant donc simple.
    Elle devra fonctionner en O(N + M log M), voire en O(N + M) si vous y arrivez (où N = nombre de noeuds et M = nombre d'arcs).

    Test: make g3

    RENDU: graph.h, graph.cc avec la nouvelle fonction


Exercice 2: Graphe non dirigé, composantes connexes

  1. Complétez le fichier ugraph.h et implémentez-le dans un fichier ugraph.cc!

    Attention: chaque méthode devra avoir une complexité O(1).
    #include "graph.h"
    
    // Un graph non dirigé est comme un graph dirigé, mais ses arcs sont appelés
    // "arêtes" et n'ont pas de direction: une arête (a, b) est matérialisée à la
    // fois dans la liste d'adjacence de a et dans celle de b.
    class UndirectedGraph {
     public:
      explicit UndirectedGraph(int num_nodes);
    
      void AddEdge(int a, int b);  // Ajoute une arête au graphe.
    
      int NumNodes() const;
      int NumEdges() const;
      int Degree(int node) const;
      const vector<int>& Neighbors(int node) const;
    
     private:
      // TODO
      // INDICE: Utilisez un DirectedGraph, et stockez-y chaque arête (a, b) comme 2
      // arcs a→b et b→a !
    };
    Test:
    make u1
    RENDU: ugraph.h, ugraph.cc

  2. Ajoutez une fonction vector<int> GetNodesConnectedTo(int node) qui renvoie les noeuds qui sont dans la même composante connexe que node

    Test: make u2

    RENDU: ugraph.h, ugraph.cc avec la nouvelle fonction

  3. Ajoutez une fonction vector<vector<int>> ConnectedComponents() qui renvoie toutes les composantes connexes du graphes, dans un ordre quelquonque.

    Test: make u3

    RENDU: ugraph.h, ugraph.cc avec la nouvelle fonction

  4. [*] (À faire seulement si vous avez le temps; privilégiez la suite du TD)
    Implémentez la fonction décrite dans triangles.h:
    #include "ugraph.h"
    
    // Renvoie le nombre de triplets ordonnés i<j<k tels que
    // (i,j), (j,k) et (k,i) sont des arêtes du graphe.
    int NumTriangles(const UndirectedGraph& graph);
    
    [**] Essayez d'obtenir une complexite meilleure que O(N3) dans le cas (commun en pratique) où le graphe n'est pas dense, c'est-à-dire que son nombre M d'arêtes est très inférieur à N2, où N est le nombre de noeuds.
    Indice: vous pouvez par exemple copier le graph dans un graph (dirigé?) temporaire, où vous aurez trié chaque liste d'adjacence et enlevé les doublons. Cela peut vous aider à trouver un algorithme plus efficace.

    Test: make triangles

    RENDU: triangles.cc (si besoin, les modifications éventuelles de ugraph.h et ugraph.cc devront être dans le rendu)


Exercice 3: BFS

  1. Implémentez dans un fichier bfs.1.cc la fonction BFS() décrite dans le fichier bfs.1.h:
    #include "ugraph.h"
    
    // Runs a Breadth-First-Search on the graph, starting from node "src".
    // See https://en.wikipedia.org/wiki/Breadth-first_search .
    // Returns a vector of size N (N is the number of nodes of the
    // graph) representing the "parent" tree: parent[i] is the parent of
    // node #i in the BFS tree. The parent of "src" is itself, and the
    // parent of a node that wasn't reached by the BFS exploration is -1.
    vector<int> BFS(const UndirectedGraph& graph, int src);
    La complexité devra être O(M + N), où M est le nombre d'arcs et N le nombre de noeuds.

    Utilisez une file de priorité deque de la librairie standard (#include <deque>). Les méthodes dont vous avez besoin sont :
    Test: make bfs.1
    RENDU: bfs.1.cc

  2. Implémentez la fonction GetBfsDistances() décrite dans le fichier bfs.2.h:
    #include <vector>
    using std::vector;
    
    // Extracts the distances of each node in the given BFS tree, which
    // is the returned format described in bfs.1.h
    // Eg. in the following tree, whose root is "2":
    //
    //         .---- 4
    //         v
    //   2 <-- 3 <-- 1
    //   ^
    //   '---- 0 <-- 5
    //
    // The bfs tree is represented by the following 'parent' vector:
    // [2, 3, 2, 2, 3, 0]
    // And the distance vector should be:
    // [1, 2, 0, 1, 2, 2]
    //
    // If a node was not reached by the BFS, its parent is -1, and its distance
    // should also be -1.
    vector<int> GetBfsDistances(const vector<int>& parents);
    Comment faire pour obtenir une complexité de O(N), où N le nombre de noeuds ?

    Test: make bfs.2
    RENDU: bfs.2.cc

  3. Implémentez la fonction GetShortestPathFromRootedTree() décrite dans le fichier bfs.3.h:
    #include <vector>
    using std::vector;
    
    // Returns the shortest path, from the source of a BFS to the given target node.
    // The argument is the target node and the BFS "parent" tree.
    // If the target node was not reached by the BFS, the returned path should be
    // empty.
    // Example: using the same example as in bfs.2.h, with BFS 'parent' tree:
    // [2, 3, 2, 2, 3, 0]
    // Then:
    // - the shortest path to node #4 should be: [2, 3, 4]
    // - the shortest path to node #0 should be: [2, 0]
    // - the shortest path to node #5 should be: [2, 0, 5]
    // - the shortest path to node #2 should be: [2]
    vector<int> GetShortestPathFromRootedTree(const vector<int>& parent, int target);
    Vous pourriez avoir besoin de la fonction reverse (#include <algorithm>) qui renverse l'ordre des éléments d'un vecteur : reverse(vec.begin(), vec.end())

    Test: make bfs.3
    RENDU: bfs.3.cc


Exercice 4: Dijkstra

  1. Copiez graph.h et graph.cc dans vgraph.h et vgraph.cc, et modifiez la fonction AddArc() pour qu'elle prenne un argument supplémentaire: double length.
    Modifiez également la fonction Neighbors() pour qu'elle renvoie un const vector<pair<int, double>>&.
    Ne reprenez pas la fonction MakeSimple().

    Test: make vgraph
    RENDU: vgraph.h et vgraph.cc

  2. (**) Implémentez la fonction Dijkstra() décrite dans le fichier dijkstra.h:
    #include "vgraph.h"
    
    // Runs a Dijkstra search on the graph, starting from node "src".
    // See https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm .
    // Returns the same "parent" vector as BFS() in bfs.1.h.
    vector<int> Dijkstra(const DirectedGraph& graph, int src);
    On utilisera priority_queue<> sur une struct qu'on définira, qui correspond à un noeud du graph associé à sa distance depuis la source, assorti d'un opérateur < adapté à ce qu'on en veut pour la priority_queue.
    La complexité devra être O(N + M log(M)).
    Test: make dijkstra
    RENDU: dijkstra.cc