diff --git a/grapp/src/main/kotlin/fr/ulille/grapp/exercises/DijkstraExercise.kt b/grapp/src/main/kotlin/fr/ulille/grapp/exercises/DijkstraExercise.kt
index 970f46197ec36854fded15a4c5c6c4786794d90a..099c252987f41af537afd0f2604b3de5636d7ecd 100644
--- a/grapp/src/main/kotlin/fr/ulille/grapp/exercises/DijkstraExercise.kt
+++ b/grapp/src/main/kotlin/fr/ulille/grapp/exercises/DijkstraExercise.kt
@@ -24,7 +24,7 @@ import fr.ulille.grapp.algorithms.traces.DijkstraTraces
 import fr.ulille.grapp.algorithms.traces.ExecutionTrace
 import fr.ulille.grapp.exercises.formatting.ClozeFormatter
 import fr.ulille.grapp.exercises.formatting.GraphOnGridTikzFormatter
-import fr.ulille.grapp.exercises.graphgen.GridGraphGenerator
+import fr.ulille.grapp.exercises.gen.GridGraphGenerator
 import fr.ulille.grapp.graph.Graph
 import fr.ulille.grapp.graph.GrappType
 import fr.ulille.grapp.graph.Vertex
@@ -167,66 +167,62 @@ ${predecessorsQuestion(graph, pred, start, pointsPred)}
         CommandLineUtil.executeDotToPng(fileName("result", "dot"), fileName("result-from-$start", "png"))
     }
 
-    private fun executionTraceQuestion (columnNames: List<String>,
-                                        tableLines: List<Map<String, String>>,
-                                        points: Int) : String {
-        val s = StringBuilder()
-        ClozeFormatter.clozeQuestionTable(s,
-            columnNames,
-            tableLines,
-            points,
-            setOf(0))
-        return s.toString()
-    }
-
-    private fun distancesQuestion(graph: Graph, dist: Map<Vertex, Double>, points: Int) : String {
-        val s = StringBuilder()
-        val vertices = graph.vertexSet().sorted()
-        val columnNames = vertices.map{ "Dist($it)"}
-        val line = vertices.associate{
-            val d = dist[it]!!
-            Pair("Dist($it)", if (d == Double.POSITIVE_INFINITY) "inf" else Util.weightToString(d))
+    companion object {
+        private fun executionTraceQuestion (columnNames: List<String>,
+                                            tableLines: List<Map<String, String>>,
+                                            points: Int) : String {
+            val s = StringBuilder()
+            ClozeFormatter.clozeQuestionTable(s,
+                columnNames,
+                tableLines,
+                points,
+                setOf(0))
+            return s.toString()
         }
-        ClozeFormatter.clozeQuestionTable(s, columnNames, listOf(line), points)
-        return s.toString()
-    }
-
-    private fun predecessorsQuestion (graph: Graph, pred: Map<Vertex, Vertex>, start: Vertex, points: Int) : String {
-        val s = StringBuilder()
-        val vertices = graph.vertexSet().filter{ it != start }.sorted()
-        val columnNames = vertices.map{ "Pred($it)" }
-        val line =  vertices.associate {
-            Pair("Pred($it)", pred[it] ?: "aucun")
+        private fun distancesQuestion(graph: Graph, dist: Map<Vertex, Double>, points: Int) : String {
+            val s = StringBuilder()
+            val vertices = graph.vertexSet().sorted()
+            val columnNames = vertices.map{ "Dist($it)"}
+            val line = vertices.associate{
+                val d = dist[it]!!
+                Pair("Dist($it)", if (d == Double.POSITIVE_INFINITY) "inf" else Util.weightToString(d))
+            }
+            ClozeFormatter.clozeQuestionTable(s, columnNames, listOf(line), points)
+            return s.toString()
         }
-        ClozeFormatter.clozeQuestionTable(s, columnNames, listOf(line), points)
-        return s.toString()
-    }
-
-    private fun twoPaths (dist: Map<Vertex, Double>, result: OptimalPathResult, start: Vertex) : Pair<List<Vertex>,List<Vertex>> {
-        val sortedDistances = dist.entries.sortedByDescending { it.value }.toMutableList()
-        while (sortedDistances[0].value == Double.POSITIVE_INFINITY)
-            sortedDistances.removeAt(0)
-        val end1 = sortedDistances[0].key
-        val end2 = sortedDistances[1].key
-        val bfssp = BFSShortestPath(result.tree)
-        val path1 = bfssp.getPath(start, end1).vertexList
-        val path2 = bfssp.getPath(start, end2).vertexList
-        return Pair(path1, path2)
-    }
-
-
-    private fun pathQuestion (path: List<Vertex>, points: Int) : String {
-        val answer = path.joinToString(prefix="[", postfix="]", separator=",")
-        val s = StringBuilder()
-        s.appendLine(ClozeFormatter.clozeQuestion(answer, points))
-        return s.toString()
-    }
-
-    private fun nombreCasesARemplir (lines: List<Map<String, String>>) : Int {
-        var result = 0
-        for (line in lines.subList(1,lines.size)) {
-            result += line.entries.count { it.value.isNotEmpty() }
+        private fun predecessorsQuestion (graph: Graph, pred: Map<Vertex, Vertex>, start: Vertex, points: Int) : String {
+            val s = StringBuilder()
+            val vertices = graph.vertexSet().filter{ it != start }.sorted()
+            val columnNames = vertices.map{ "Pred($it)" }
+            val line =  vertices.associate {
+                Pair("Pred($it)", pred[it] ?: "aucun")
+            }
+            ClozeFormatter.clozeQuestionTable(s, columnNames, listOf(line), points)
+            return s.toString()
+        }
+        private fun twoPaths (dist: Map<Vertex, Double>, result: OptimalPathResult, start: Vertex) : Pair<List<Vertex>,List<Vertex>> {
+            val sortedDistances = dist.entries.sortedByDescending { it.value }.toMutableList()
+            while (sortedDistances[0].value == Double.POSITIVE_INFINITY)
+                sortedDistances.removeAt(0)
+            val end1 = sortedDistances[0].key
+            val end2 = sortedDistances[1].key
+            val bfssp = BFSShortestPath(result.tree)
+            val path1 = bfssp.getPath(start, end1).vertexList
+            val path2 = bfssp.getPath(start, end2).vertexList
+            return Pair(path1, path2)
+        }
+        fun pathQuestion(path: List<Vertex>, points: Int): String {
+            val answer = path.joinToString(prefix = "[", postfix = "]", separator = ",")
+            val s = StringBuilder()
+            s.appendLine(ClozeFormatter.clozeQuestion(answer, points))
+            return s.toString()
+        }
+        private fun nombreCasesARemplir(lines: List<Map<String, String>>): Int {
+            var result = 0
+            for (line in lines.subList(1, lines.size)) {
+                result += line.entries.count { it.value.isNotEmpty() }
+            }
+            return result
         }
-        return result
     }
 }
diff --git a/grapp/src/main/kotlin/fr/ulille/grapp/exercises/Exercises.kt b/grapp/src/main/kotlin/fr/ulille/grapp/exercises/Exercises.kt
index b34b5d769eef1e6038dbac59d73aa02e0ff38981..235aa235f9298c29ce6c0ec9428a5f9fe9363d08 100644
--- a/grapp/src/main/kotlin/fr/ulille/grapp/exercises/Exercises.kt
+++ b/grapp/src/main/kotlin/fr/ulille/grapp/exercises/Exercises.kt
@@ -19,21 +19,18 @@
 
 package fr.ulille.grapp.exercises
 
-import com.github.ajalt.clikt.output.defaultCliktConsole
 import com.github.ajalt.clikt.parameters.options.flag
 import com.github.ajalt.clikt.parameters.options.option
 import com.github.ajalt.clikt.parameters.options.required
 import com.github.ajalt.clikt.parameters.types.choice
 import com.github.ajalt.clikt.parameters.types.int
-import fr.ulille.grapp.ui.Convert
-import fr.ulille.grapp.ui.Grapp
 import fr.ulille.grapp.ui.Messages
 import fr.ulille.grapp.ui.MyCliktCommand
 
 object Exercises : MyCliktCommand(help= Messages.convertHelp, printHelpOnEmptyArgs = true) {
 
     val algorithm by option("-algorithm", help=Messages.convertOutFormatHelp)
-        .choice("Dijkstra")
+        .choice("Dijkstra", "Pert")
         .required()
     val nb by option("-nb", help="Nombre de graphes à générer")
         .int()
@@ -44,13 +41,14 @@ object Exercises : MyCliktCommand(help= Messages.convertHelp, printHelpOnEmptyAr
         .flag()
 
     override fun run () {
-        for (i in 1 .. nb) {
-            DijkstraExercise(".", "$name$i", directed).generate()
+        when (algorithm) {
+            "Dijkstra" -> for (i in 1 .. nb)
+                DijkstraExercise(".", "$name$i", directed).generate()
+            "Pert" -> PertExercises(".", "pert").generate(nb)
         }
     }
 }
 
-
 fun main(args: Array<String>) {
     Exercises.main(args)
 }
\ No newline at end of file
diff --git a/grapp/src/main/kotlin/fr/ulille/grapp/exercises/PertExercise.kt b/grapp/src/main/kotlin/fr/ulille/grapp/exercises/PertExercise.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f09b5913fa18588006ef5f8c39acd358a9a664dc
--- /dev/null
+++ b/grapp/src/main/kotlin/fr/ulille/grapp/exercises/PertExercise.kt
@@ -0,0 +1,243 @@
+/*
+ * Copyright (c) 2022 Iovka Boneva, Université de Lille
+ *
+ * This file is part of Grapp.
+ *
+ * Grapp is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Grapp is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Grapp.  If not, see <https://www.gnu.org/licenses/>
+ */
+
+package fr.ulille.grapp.exercises
+
+
+import fr.ulille.grapp.algorithms.Bellman
+import fr.ulille.grapp.algorithms.OptimalityCriterion
+import fr.ulille.grapp.algorithms.PathCombinationOperation
+import fr.ulille.grapp.exercises.formatting.ClozeFormatter
+import fr.ulille.grapp.exercises.gen.PertProblemGenerator
+import fr.ulille.grapp.graph.*
+import fr.ulille.grapp.io.DotIo
+import fr.ulille.grapp.io.YamlSerializer
+import fr.ulille.grapp.ui.AlgorithmsUtil
+import fr.ulille.grapp.ui.CommandLineUtil
+import fr.ulille.grapp.ui.OptimalPathKind
+import fr.ulille.grapp.ui.OptimalPathResult
+import org.jgrapht.alg.shortestpath.AllDirectedPaths
+import org.jgrapht.alg.shortestpath.BFSShortestPath
+import java.io.File
+import kotlin.random.Random
+
+/** With each task associates an int duration and a list of predecessor tasks. */
+data class TaskInfo (val duration: Int, val predecessors: List<String>)
+typealias PertProblem = Map<String, TaskInfo>
+/** Represents a solution of the PertProblem */
+
+/** Information about the acceptable solutions of the problem.
+ * @param graph is the Pert graph
+ * @param additionalEdges corresponds to the edges that are induced by the problem definition, but should be removed from the Pert graph because belong to the transitive closure for the minimal graph
+ */
+data class PertGraphAcceptableSolutions (val graph: DirectedWeightedGraph, val additionalEdges: Set<Edge>)
+
+data class PertSolution (val graph: PertGraphAcceptableSolutions,
+                         val dureeMinimale: Int,
+                         val datesPlusTot: Map<String, Int>,
+                         val datesPlusTard: Map<String, Int>,
+                         val cheminCritique : List<String>)
+
+/** Pert exercices are generated in a batch, as we want to guarantee that two different exercices
+ * would have different sets of tasks (used for automatic grading). */
+class PertExercises(private val outputFolderName: String, private val graphNamePrefix: String) {
+
+    val pointsDureeMinimale = 5
+    val pointsParDatePlusTot = 2
+    val pointsParDatePlusTard = 2
+    val pointsCheminCritique = 7
+
+    fun generate (nbExercises : Int) {
+
+        // Generate as many problems as requestednecessary with different sets of nodes
+        val nodeSets = mutableSetOf<String>()
+
+        while (nodeSets.size < nbExercises) {
+            val ns = randomString()
+            if (nodeSets.contains(ns))
+                continue
+
+            val problem = PertProblemGenerator.generate(ns.toCharArray().map { it.toString() })
+            val solution = computePert(problem) ?: continue
+            nodeSets.add(ns)
+            print(".")
+
+            val fileNameBase = "$outputFolderName/$graphNamePrefix-${nodeSets.size}"
+
+            // Write the graph
+            solution.graph.graph.setName("pert $ns")
+            val x = StringBuilder()
+            YamlSerializer.serialize(solution.graph.graph,x)
+            File("$fileNameBase-pertgraph.yaml").writeText(x.toString())
+
+            // Write the graph annotated with the dates
+            val solutionGraphToDraw = solutionGraphToDraw(solution)
+            solutionGraphToDraw.setName("pert solution $ns")
+            DotIo(solutionGraphToDraw, null).serialize("$fileNameBase-graph-with-dates.dot")
+            CommandLineUtil.executeDotToPng("$fileNameBase-graph-with-dates.dot", "$fileNameBase-graph-with-dates.png")
+
+            // Write the cloze file
+            val s = StringBuilder()
+            s.append("""
+<p>Voici les tâches d'un projet:</p>
+${problemDefinitionTable(problem)}
+<p>Vous devez:</p>
+<ul>
+<li>déterminer la durée minimale du projet, les dates au plus tôt et les dates au plus tard des tâches pour 70% de la note,</li>
+<li>donner le graphe potentiel-tâches associé au projet, au format Grapp YAML, pour 30% de la note.</li>
+</ul>
+<p>Quelle est la durée minimale du projet ? ${ClozeFormatter.clozeQuestion(solution!!.dureeMinimale.toString(), pointsDureeMinimale)}</p>
+<br><p>Pour chaque tâche, donner sa date au plus tôt.</p>
+${datesTable(solution!!.datesPlusTot, pointsParDatePlusTot, "Tôt")}
+<br><p>Pour chaque tâche, donner sa date au plus tard.</p>
+${datesTable(solution!!.datesPlusTard, pointsParDatePlusTard, "Tard")}
+<br><p>Donner un chemin critique sous la forme <b>[$alpha,A,B,C,$omega]</b> càd entre crochets, sommets séparés par des virgules, sans espaces, en incluant les sommets $alpha et $omega.</p> 
+<p><i>La correction étant automatique, <b>respectez strictement le format de réponse demandé.</b></i></p> 
+<p>Chemin critique: ${DijkstraExercise.pathQuestion(solution.cheminCritique, pointsCheminCritique)}</p>
+"""
+            )
+            File("$fileNameBase.cloze").writeText(s.toString())
+        }
+    }
+
+    companion object {
+        val alpha = "alpha"
+        val omega = "omega"
+        val alphaOmegaSet = setOf(alpha, omega)
+
+
+        private fun datesTable(dates: Map<String, Int>, pointsParDate: Int, columnTitle: String) : String {
+            val s = StringBuilder()
+            val tasks = dates.keys.sorted().minus(alphaOmegaSet)
+            val columnNames = tasks.map{ "$columnTitle($it)"}
+            val line = tasks.associate{
+                Pair("$columnTitle($it)", dates[it]!!.toString())
+            }
+            ClozeFormatter.clozeQuestionTable(s, columnNames, listOf(line), pointsParDate)
+            return s.toString()
+        }
+
+        private fun problemDefinitionTable (problem: PertProblem) : String {
+            val s = StringBuilder()
+            val columnNames = listOf("Tâche", "Durée", "Tâches antérieures")
+            val table = problem.entries.sortedBy { it.key }.map { (task, taskInfo) ->
+                mapOf(
+                    "Tâche" to task,
+                    "Durée" to taskInfo.duration.toString(),
+                    "Tâches antérieures" to taskInfo.predecessors.joinToString())
+            }.toList()
+            ClozeFormatter.clozeQuestionTable(s, columnNames, table, 0,
+                (table.indices).toSet(),
+                columnNames.toSet())
+            return s.toString()
+        }
+
+        private fun toPertGraph (problem: PertProblem) : PertGraphAcceptableSolutions {
+            val graph = Graphs.graphFromNodeSet(GrappType.diwgraph, problem.keys.union(alphaOmegaSet))
+            for ((succ, taskInfo) in problem.entries) {
+                for (pred in taskInfo.predecessors) {
+                    Graphs.addEdge(graph, pred, succ, problem[pred]!!.duration.toDouble())
+                }
+            }
+            for (vertex in graph.vertexSet().minus(alphaOmegaSet))
+                if (graph.incomingEdgesOf(vertex).isEmpty())
+                    Graphs.addEdge(graph, alpha, vertex, 0.0)
+                else if (graph.outgoingEdgesOf(vertex).isEmpty())
+                    Graphs.addEdge(graph, vertex, omega, problem[vertex]!!.duration.toDouble())
+
+            // Identify and remove the unnecessary edges
+            val unnecessaryEdges = mutableSetOf<Edge>()
+            val allPaths = AllDirectedPaths(graph)
+            for (edge in graph.edgeSet())
+                if (allPaths.getAllPaths(edge.src, edge.tgt, true, null).size > 1)
+                    unnecessaryEdges.add(edge)
+            for (edge in unnecessaryEdges)
+                graph.removeEdge(edge)
+            return PertGraphAcceptableSolutions(graph as DirectedWeightedGraph, unnecessaryEdges)
+        }
+
+        val allPossibleTasks = ('A'..'Z').joinToString(separator="")
+        private fun randomString () : String {
+            val indices = mutableSetOf<Int>()
+            for (i in 1..PertProblemGenerator.nbTasks)
+                indices.add(Random.nextInt(allPossibleTasks.length))
+            while (indices.size != PertProblemGenerator.nbTasks)
+                indices.add(Random.nextInt(allPossibleTasks.length))
+
+            return indices.sorted().joinToString(separator="") { allPossibleTasks[it].toString() }
+        }
+
+        private fun reverse (graph: DirectedWeightedGraph) : DirectedWeightedGraph {
+            val result = Graphs.graphFromNodeSet(GrappType.diwgraph, graph.vertexSet())
+            for (edge in graph.edgeSet())
+                Graphs.addEdge(result, edge.tgt, edge.src, edge.weight)
+            return result as DirectedWeightedGraph
+        }
+
+        /** Computes the solution of a pert problem
+         * Returns null if the problem does not have the desired properties : one critical path and all tasks are on a path from alpha to omega.
+         */
+        private fun computePert(problem: PertProblem) : PertSolution? {
+            val pertGraph = toPertGraph(problem)
+            val graph = pertGraph.graph
+            val bellman = Bellman(graph, PathCombinationOperation.Add, OptimalityCriterion.Max)
+            val dist = mutableMapOf<Vertex, Double>()
+            val pred = mutableMapOf<Vertex, Vertex>()
+            bellman.compute(alpha, dist, pred)
+            val tot = dist.entries.associate { Pair(it.key, it.value.toInt()) }
+            val dureeMinimale = dist[omega]!!.toInt()
+
+            val reverseGraph = reverse(graph)
+            val reverseBellman = Bellman(reverseGraph, PathCombinationOperation.Add, OptimalityCriterion.Max)
+            val reverseDist = mutableMapOf<Vertex, Double>()
+            reverseBellman.compute(omega, reverseDist, mutableMapOf())
+            val tard = reverseDist.entries.associate { Pair(it.key, dureeMinimale - it.value.toInt())}
+
+            val allPaths = AllDirectedPaths(graph).getAllPaths(alpha, omega, true, null)
+            val criticalPaths = allPaths.filter { path -> path.vertexList.all { vertex -> tot[vertex]!! == tard[vertex]!! }}
+            if (criticalPaths.size != 1)
+                return null
+            val allNodesOnPaths = allPaths.flatMap { path -> path.vertexList }.toSet()
+            if (problem.keys.minus(allNodesOnPaths).isNotEmpty())
+                // There is a node non-accessible from alpha or non co-accessible from omega
+                return null
+
+            val sptree = OptimalPathResult(graph, "exercise", pred, dist, AlgorithmsUtil.constructTraversalTree(graph, pred), OptimalPathKind.LONGEST_PATH)
+            val bfssp = BFSShortestPath(sptree.tree)
+            val criticalPath = bfssp.getPath(alpha, omega).vertexList
+
+            return PertSolution(pertGraph,
+                dureeMinimale,
+                tot,
+                tard,
+                criticalPath)
+        }
+
+        fun solutionGraphToDraw (solution: PertSolution) : DirectedWeightedGraph {
+            val nameWithDates : (Vertex) -> Vertex = {
+                "${it}_${solution.datesPlusTot[it]}_${solution.datesPlusTard[it]}"
+            }
+            val result = Graphs.graphFromNodeSet(GrappType.diwgraph,
+                solution.graph.graph.vertexSet().map(nameWithDates))
+            for (edge in solution.graph.graph.edgeSet())
+                Graphs.addEdge(result, nameWithDates(edge.src), nameWithDates(edge.tgt), edge.weight)
+            return result as DirectedWeightedGraph
+        }
+    }
+}
\ No newline at end of file
diff --git a/grapp/src/main/kotlin/fr/ulille/grapp/exercises/formatting/ClozeFormatter.kt b/grapp/src/main/kotlin/fr/ulille/grapp/exercises/formatting/ClozeFormatter.kt
index e734e576ea0a1414e6de0e34500d8ec59f9d50d7..2b98f4f266b98e963b4f39a07bb86cf59167185e 100644
--- a/grapp/src/main/kotlin/fr/ulille/grapp/exercises/formatting/ClozeFormatter.kt
+++ b/grapp/src/main/kotlin/fr/ulille/grapp/exercises/formatting/ClozeFormatter.kt
@@ -39,8 +39,8 @@ object ClozeFormatter {
                            columnNames: List<String>,
                            table: List<Map<String, String>>,
                            pointsPerQuestion: Int,
-                           indicesOfLinesThatAreNotQuestions : Set<Int> = setOf(),
-                           namesOfColumnsThatAreNotQuestions: Set<String> = setOf()) {
+                           indicesOfLinesThatAreNotQuestions : Set<Int> = emptySet(),
+                           namesOfColumnsThatAreNotQuestions: Set<String> = emptySet()) {
         s.append("<table ${clozeTableStyle}>\n")
         clozeTableHeader(s, columnNames)
         s.append("<tbody>\n")
diff --git a/grapp/src/main/kotlin/fr/ulille/grapp/exercises/graphgen/GridGraphGenerator.kt b/grapp/src/main/kotlin/fr/ulille/grapp/exercises/gen/GridGraphGenerator.kt
similarity index 99%
rename from grapp/src/main/kotlin/fr/ulille/grapp/exercises/graphgen/GridGraphGenerator.kt
rename to grapp/src/main/kotlin/fr/ulille/grapp/exercises/gen/GridGraphGenerator.kt
index ff8353880cc764c929b705b3e6c63ba7c95e5fb0..11d99f7591b47ec03598f0f9c550a236b268f074 100644
--- a/grapp/src/main/kotlin/fr/ulille/grapp/exercises/graphgen/GridGraphGenerator.kt
+++ b/grapp/src/main/kotlin/fr/ulille/grapp/exercises/gen/GridGraphGenerator.kt
@@ -17,7 +17,7 @@
  * along with Grapp.  If not, see <https://www.gnu.org/licenses/>
  */
 
-package fr.ulille.grapp.exercises.graphgen
+package fr.ulille.grapp.exercises.gen
 
 import fr.ulille.grapp.graph.Graph
 import fr.ulille.grapp.graph.Graphs
diff --git a/grapp/src/main/kotlin/fr/ulille/grapp/exercises/gen/PertProblemGenerator.kt b/grapp/src/main/kotlin/fr/ulille/grapp/exercises/gen/PertProblemGenerator.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9d1da897a491d619a42509b3f78f73a077c9338c
--- /dev/null
+++ b/grapp/src/main/kotlin/fr/ulille/grapp/exercises/gen/PertProblemGenerator.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright (c) 2022 Iovka Boneva, Université de Lille
+ *
+ * This file is part of Grapp.
+ *
+ * Grapp is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Grapp is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Grapp.  If not, see <https://www.gnu.org/licenses/>
+ */
+
+package fr.ulille.grapp.exercises.gen
+
+import fr.ulille.grapp.exercises.PertProblem
+import fr.ulille.grapp.exercises.TaskInfo
+import java.lang.IllegalArgumentException
+import kotlin.random.Random
+
+object PertProblemGenerator {
+
+    private val nbDependencies = 8
+    const val nbTasks = 7
+    private val minDuration = 5
+    private val maxDuration = 25
+
+    /** Generates a PERT problem instance with the given tasks and the given number of dependencies between tasks.
+     * @param tasks must be a set, i.e. w/o repetitions. */
+    fun generate (tasks: List<String>) : PertProblem {
+        if (tasks.toSet().size != nbTasks)
+            throw IllegalArgumentException("Le nombre de tâches doit être égal à 7.")
+
+        val pdescr = problem(setOf(1,2,3).random())
+        val X = tasks.subList(0, pdescr.cardX)
+        val Y = tasks.subList(pdescr.cardX, pdescr.cardX + pdescr.cardY)
+        val Z = tasks.subList(pdescr.cardX + pdescr.cardY, nbTasks)
+
+        val XwoSucc = X.toMutableSet()
+        val YwoPred = Y.toMutableSet()
+        val YwoSucc = Y.toMutableSet()
+        val ZwoPred = Z.toMutableSet()
+        val durations = (1..nbDependencies).map{ Random.nextInt(minDuration, maxDuration)}.toMutableList()
+
+        val result = mutableMapOf<String,TaskInfo>()
+        X.forEach { result[it] = TaskInfo(durations.removeFirst(), emptyList())}
+
+        val addPairToResult : (Pair<String, String>) -> Unit = { (pred, succ) ->
+            val ti = result.getOrPut(succ) { TaskInfo(durations.removeFirst(), mutableListOf())}
+            (ti.predecessors as MutableList).add(pred)
+        }
+        randomPairs(pdescr.xz, XwoSucc, X, ZwoPred, Z).forEach { addPairToResult(it) }
+        randomPairs(pdescr.xy, XwoSucc, X, YwoPred, Y).forEach { addPairToResult(it) }
+        randomPairs(pdescr.yz, YwoSucc, Y, ZwoPred, Z).forEach { addPairToResult(it) }
+        randomPairs(pdescr.yy, YwoSucc, Y, YwoPred, Y).forEach { addPairToResult(it) }
+        return result.entries.sortedBy { it.key }.associate { Pair(it.key, it.value) }
+    }
+
+    /** Selects a random element in 'from' and removes it from 'from'.
+     * If from is empty, then selects a random element in otherwise. */
+    private fun randomElement (from: MutableSet<String>, otherwise: Collection<String>) : String {
+        return if (from.isNotEmpty()) {
+            val r = from.random()
+            from.remove(r)
+            r
+        } else
+            return otherwise.random()
+
+    }
+
+    private fun randomPairs (nbPairs : Int,
+                             first: MutableSet<String>, firstOtherwise: Collection<String>,
+                             second: MutableSet<String>, secondOtherwise: Collection<String>) : Set<Pair<String, String>> {
+        val result = mutableSetOf<Pair<String,String>>()
+        var nb = 0
+        while (nb < nbPairs) {
+            val f = randomElement(first, firstOtherwise)
+            val s = randomElement(second, secondOtherwise)
+            val p = Pair(f,s)
+            if (! result.contains(p)) {
+                result.add(p)
+                nb ++
+            }
+        }
+        return result
+    }
+
+    data class ProblemTypeDescription (
+        val cardX: Int, val cardY: Int, val cardZ: Int,
+        val xy : Int, val yz : Int, val xz : Int, val yy: Int
+    )
+
+    /** Three different sets of parameters that influence the shape of the problem.
+     * Well adapted for nbTasks = 7 and nbDependencies = 8
+     * How it works:
+     * - nbTasks tasks
+     * - nbDependencies total number dependencies between tasks
+     * - 3 subsets of tasks : X (w/o predecessor), Y (with predecessor and with successor), Z (w/o successor)
+     *   The number of vertices from X to Y is denoted xy, similarly yz, xz and yy
+     * Type 1:
+     * - |X| = 2, |Z| = 2, |Y| = nbTasks-|X|-|Y|
+     * - xz = 1
+     * - xy = 3
+     * - yy = 1
+     * - yz = nbDependencies -xz-xy-yy
+     * Type 2:
+     * - |X| = 2, |Z| = 3, |Y| = nbTasks-|X|-|Y|
+     * - xz = 1 or 2
+     * - xy = 3
+     * - yy = 0
+     * - yz = nbDependencies - xz - xy - yy (so 3 or 4)
+     * Type 3:
+     * - symmetric to Type 2 w.r.t. X and Z
+     */
+    private fun problem (type: Int) : ProblemTypeDescription {
+        return when (type) {
+            1 -> {
+                ProblemTypeDescription(
+                    cardX=2, cardY=nbTasks-2-2, cardZ=2,
+                    xy=3, xz=1, yz=nbDependencies-3-1-1, yy=1)
+            }
+            2 -> {
+                val xz = setOf(1,2).random()
+                ProblemTypeDescription(
+                    cardX=2, cardY=nbTasks-2-3, cardZ=3,
+                    xy=3, xz=xz, yz=nbDependencies-3-xz-0, yy=0)
+            }
+            3 -> {
+                val xz = setOf(1,2).random()
+                ProblemTypeDescription(
+                    cardX=3, cardY=nbTasks-3-2, cardZ=2,
+                    xz=xz, xy=nbDependencies-xz-3-0, yz=3, yy=0)
+            }
+            else ->
+                throw IllegalArgumentException("Le type de problème peut être 1, 2 ou 3.")
+        }
+    }
+
+
+
+
+}
+
diff --git a/grapp/src/test/kotlin/fr/ulille/grapp/exercises/TestExercises.kt b/grapp/src/test/kotlin/fr/ulille/grapp/exercises/TestExercises.kt
index 32a139a58cc46cbe3051e7eeb1e2341ab1823577..65e119c386266ede20828851f34f36d48a5ddb5f 100644
--- a/grapp/src/test/kotlin/fr/ulille/grapp/exercises/TestExercises.kt
+++ b/grapp/src/test/kotlin/fr/ulille/grapp/exercises/TestExercises.kt
@@ -19,6 +19,7 @@
 
 package fr.ulille.grapp.exercises
 
+import fr.ulille.grapp.exercises.gen.PertProblemGenerator
 import fr.ulille.grapp.graph.GrappType
 import org.junit.Test
 
@@ -27,6 +28,15 @@ class TestExercises {
     @Test
     fun test () {
         val outputFolderName = "c:/Users/iovka/Downloads/tmp/"
-        DijkstraExercise(outputFolderName, "g", true).generate()
+        // DijkstraExercise(outputFolderName, "g", true).generate()
+        PertExercises(outputFolderName, "exo").generate(5)
     }
+
+    //@Test
+    fun testPertProblemGenerator () {
+        val problem = PertProblemGenerator.generate(listOf("a", "b", "c", "d", "e", "f", "g"))
+        println(problem.entries.joinToString(separator="\n") { "${it.key} -> ${it.value}" })
+    }
+
+
 }
\ No newline at end of file
diff --git a/grapp/src/test/kotlin/fr/ulille/grapp/exercises/graphgen/TestGridGraphGenerator.kt b/grapp/src/test/kotlin/fr/ulille/grapp/exercises/gen/TestGridGraphGenerator.kt
similarity index 96%
rename from grapp/src/test/kotlin/fr/ulille/grapp/exercises/graphgen/TestGridGraphGenerator.kt
rename to grapp/src/test/kotlin/fr/ulille/grapp/exercises/gen/TestGridGraphGenerator.kt
index cbb1b646a3fddd972411a887c9c79de0922c1ce6..b872c25f8014073976845ca0824eb837d24d4959 100644
--- a/grapp/src/test/kotlin/fr/ulille/grapp/exercises/graphgen/TestGridGraphGenerator.kt
+++ b/grapp/src/test/kotlin/fr/ulille/grapp/exercises/gen/TestGridGraphGenerator.kt
@@ -17,7 +17,7 @@
  * along with Grapp.  If not, see <https://www.gnu.org/licenses/>
  */
 
-package fr.ulille.grapp.exercises.graphgen
+package fr.ulille.grapp.exercises.gen
 
 import fr.ulille.grapp.graph.GrappType
 import kotlin.test.Test