French Flag Version française

Exercise Session 1: Graphs

Reminders


Exercise 1: Directed Graph

  1. Complete the file graph.h and implement it in another file graph.cc, whose skeleton I provide here.

    Important: each function must have constant-time complexity O(1).
    
    #include <vector>
    using std::vector;    
    // A directed graph is a set of nodes, linked by arcs. Arcs are directed: they
    // go from a node to another node.
    // In this implementation, the nodes are the integers {0 .. num_nodes-1}.
    //
    // For example, the following graph:
    //
    //  0 <--- 1 <--> 3 ---> 4
    //  ^      |       \     ^
    //  |      v        `----'
    //  '----- 2
    //
    // Can be obtained by:
    //   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:
      // At construction, the graph has no arcs.
      explicit DirectedGraph(int num_nodes);
    
      int NumNodes() const;
    
      void AddArc(int from, int to);
    
      // Returns the number of arcs originating from "node".
      // In the example above, OutDegree(1) = 3, OutDegree(4) = 0.
      int OutDegree(int node) const;
    
      // Returns all the destination nodes that were added with arcs
      // originating from "node".
      // In the example above, Neighbors(1) is {0, 2, 3} and Neighbors(2) is {0}.
      const vector<int>& Neighbors(int node) const;
    
     private:
      // TODO
    };
    Test your 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
    TO SUBMIT: graph.h, graph.cc

  2. Add a function (inside the DirectedGraph class) that returns the number of arcs of the graph: int NumArcs() const
    How can you make it work in O(1) constant time? Feel free to adapt the rest of the class. But be careful: the complexity of all other operations must remain O(1).

    Test: make g2

    TO SUBMIT: graph.h, graph.cc with the newly added function (declared in the .h and defined in the .cc).
    Do not create new files graph.h, graph.cc! Instead, modify the previous files you had.
    The exercises are incremental: as you progress, your files will pass more and more of the automated tests.

  3. Add a function void MakeSimple() which removes all self-arcs (x→x) and all multi-arcs (e.g. if the graph has the arc x→y 3 times, keep only one). A graph without self-arcs nor multi-arcs is called simple.
    It must work in time complexity O(N + M log M), or even O(N + M) if you can (where N = number of nodes and M = number of arcs).

    Test: make g3

    TO SUBMIT: graph.h, graph.cc with the added function.


Exercise 2: Undirected Graph, connected components

  1. Complete the file ugraph.h and implement it in another file ugraph.cc.

    Important: each function must have constant time complexity O(1).
    #include "graph.h"
    
    // An undirected graph is just like a directed graph, but arcs are called
    // "edges" and they don't have a direction: an edge (a, b) is present both
    // in the adjacency list of a and the adjacency list of b.
    class UndirectedGraph {
     public:
      explicit UndirectedGraph(int num_nodes);
    
      void AddEdge(int a, int b);
    
      int NumNodes() const;
      int NumEdges() const;
      int Degree(int node) const;
      const vector<int>& Neighbors(int node) const;
    
     private:
      // TODO
      // HINT: Use a DirectedGraph where you store each edge (a, b) as two arcs
      // a->b and b->a !
    };
    Test:
    make u1
    TO SUBMIT: ugraph.h, ugraph.cc

  2. Add a function vector<int> GetNodesConnectedTo(int node) that returns all the nodes that are in the same connected component as node.

    Test: make u2

    TO SUBMIT: ugraph.h, ugraph.cc with the added function

  3. Add a function vector<vector<int>> ConnectedComponents() that returns all the connected components of the graph, in any order.

    Test: make u3

    TO SUBMIT: ugraph.h, ugraph.cc with the added function

  4. [*] (Do this only if you have time! Please do the rest of the exercises before doing this)
    Implement the function described in triangles.h:
    #include "ugraph.h"
    
    // Returns the number of sorted triples of nodes i<j<k such that
    // (i,j), (j,k) and (k,i) are edges of the graph.
    int NumTriangles(const UndirectedGraph& graph);
    
    [**] Try to have a time complexity better than O(N3) in practice when the graph is sparse, meaning that its number of edges M is much smaller than N2, where N is the number of nodes.
    Hint: you could for example make a local, partial copy of the (const) input graph. Arc by arc. The local copy could be directed instead of undirected, after 'sorting' each input edge? Also, you could remove duplicate and self-arcs on the spot?
    Those hints can help you write a more efficient algorithm.

    Test: make triangles

    TO SUBMIT: triangles.cc (if you made any modifications to ugraph.h and ugraph.cc, they need to be in the submitted files too)


Exercise 3: BFS

  1. Implement in a file bfs.1.cc the function BFS() described in the file 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);
    Its time complexity must be O(M + N), where M is the number of arcs and N the number of nodes.

    Use a queue deque from the the C++ STL (Standard Template Library): #include <deque>. The methods you'll need are :
    Test: make bfs.1
    TO SUBMIT: bfs.1.cc

  2. Implement the function GetBfsDistances() described in file 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);
    Try to achieve a time complexity O(N), where N is the number of nodes.

    Test: make bfs.2
    TO SUBMIT: bfs.2.cc

  3. Implement the function GetShortestPathFromRootedTree() described in file 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);
    You may need the function reverse (#include <algorithm>) which reverses the order of the elements of a vector : reverse(vec.begin(), vec.end())

    Test: make bfs.3
    TO SUBMIT: bfs.3.cc


Exercise 4: Weighted graphs and Dijkstra

  1. Weighted (directed) graph:
    Copy graph.h and graph.cc to new files vgraph.h and vgraph.cc. In those new files, modify the function AddArc() to give it an additional argument: double length.
    Also modify the function Neighbors() so that it returns a const vector<pair<int, double>>&.
    Remove the MakeSimple() function, if you had added it.

    Test: make vgraph
    TO SUBMIT: vgraph.h et vgraph.cc

  2. (**) Implement the function Dijkstra() described in the file 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);
    You should use priority_queue<> on a custom struct that you'll define, which will hold a node of the graph and its distance from the source.
    That custom struct will lso have a custom comparison operator operator<, adapted to its use within our priority_queue.
    The time complexity must be O(N + M log(M)).
    Test: make dijkstra
    TO SUBMIT: dijkstra.cc