diff --git a/src/main/scala/Main.scala b/src/main/scala/Main.scala
index e3d7bf4bafbfa7c06643abccce14831e34291a0c..4363f07c19ef3f7a50c2a72c76d7faf06d52a962 100644
--- a/src/main/scala/Main.scala
+++ b/src/main/scala/Main.scala
@@ -1,5 +1,5 @@
 // Copyright (C) Maxime MORGE 2024
-import org.scata.core.SingleAssignmentProblem
+import org.scata.core.AssignmentProblem
 import org.scata.algorithm.CBAA
 import org.scata.patrol.Environment
 
@@ -7,7 +7,7 @@ object Main {
   def main(args: Array[String]): Unit = {
 
     val nbRobots= 4
-    val nbTargets = 3
+    val nbTargets = 4
     val env = Environment.randomEnvironment(nbRobots, nbTargets)
     println(env)
     val pb = env.toSAP()
diff --git a/src/main/scala/org/scata/algorithm/CBAA.scala b/src/main/scala/org/scata/algorithm/CBAA.scala
index 55cd064688704df8e07e3420a8ccfc1817e21e24..77b94690a5295bf990f22ced83d21b9ac9951ab3 100644
--- a/src/main/scala/org/scata/algorithm/CBAA.scala
+++ b/src/main/scala/org/scata/algorithm/CBAA.scala
@@ -6,7 +6,7 @@ import org.scata.core._
  * The Consensus-Based Auction Algorithm (CBAA) is
  * a single-assignment strategy
  */
-class CBAA(pb : SingleAssignmentProblem) {
+class CBAA(pb : AssignmentProblem) {
   private val debug = true
 
   // Generate a fully connected communication network
diff --git a/src/main/scala/org/scata/core/SingleAssignmentProblem.scala b/src/main/scala/org/scata/core/AssignmentProblem.scala
similarity index 84%
rename from src/main/scala/org/scata/core/SingleAssignmentProblem.scala
rename to src/main/scala/org/scata/core/AssignmentProblem.scala
index 3da60cc9be69427c3a2fe4987652b3199f929f0d..ecb8b0d9c63af721cefa1a1d14a081390648c2ce 100644
--- a/src/main/scala/org/scata/core/SingleAssignmentProblem.scala
+++ b/src/main/scala/org/scata/core/AssignmentProblem.scala
@@ -10,9 +10,9 @@ import org.scata.utils.RandomUtils
   * @param workers are the workers
   * @param tasks are the tasks
   */
-class SingleAssignmentProblem(val workers: SortedSet[Worker],
-                              val tasks : SortedSet[Task],
-                              val cost : Map[(Worker,Task), Int]) {
+class AssignmentProblem(val workers: SortedSet[Worker],
+                        val tasks : SortedSet[Task],
+                        val cost : Map[(Worker,Task), Int]) {
 
   /**
     * Returns a string describing the MASTAPlus problem
@@ -47,7 +47,7 @@ class SingleAssignmentProblem(val workers: SortedSet[Worker],
 /**
   * Factory for SingleAssignmentProblem
   */
-object SingleAssignmentProblem{
+object AssignmentProblem{
 
   implicit val order  : Ordering[Double] = Ordering.Double.TotalOrdering
   // eventually Ordering.Double.IeeeOrdering
@@ -59,7 +59,7 @@ object SingleAssignmentProblem{
     * @param m nodes
     * @param n tasks
     */
-  def randomProblem(m: Int, n: Int): SingleAssignmentProblem = {
+  def randomProblem(m: Int, n: Int): AssignmentProblem = {
     // Workers
     val workers: SortedSet[Worker] = collection.immutable.SortedSet[Worker]() ++
       (for (i <- 1 to m) yield new Worker(name = "w%02d".format(i)))
@@ -75,6 +75,6 @@ object SingleAssignmentProblem{
           cost =  cost.updated((worker, task), RandomUtils.random(1, MAX_COST))
       }
     }
-    new SingleAssignmentProblem(workers, tasks, cost)
+    new AssignmentProblem(workers, tasks, cost)
   }
 }
diff --git a/src/main/scala/org/scata/core/MultipleAssignment.scala b/src/main/scala/org/scata/core/MultipleAssignment.scala
new file mode 100644
index 0000000000000000000000000000000000000000..b267451ed8e53eb0d9f27018073bb42171406b8f
--- /dev/null
+++ b/src/main/scala/org/scata/core/MultipleAssignment.scala
@@ -0,0 +1,82 @@
+// Copyright (C) Maxime MORGE, 2024
+package org.scata.core
+
+import scala.math.Ordering.Implicits.seqOrdering
+
+/**
+ * Class representing an allocation as
+ * a multi-assignment of tasks to workers.
+ *
+ * @param pb is a multiple-assignment instance
+ */
+class MultipleAssignment(val pb: AssignmentProblem) {
+
+  var bundle: Map[Worker, List[Task]] = pb.workers.map(w => w -> List.empty[Task]).toMap
+
+  override def toString: String =
+    pb.workers.toList.map(worker => s"$worker: ${bundle(worker).mkString(", ")}").mkString("\n")
+
+  override def equals(that: Any): Boolean =
+    that match {
+      case that: MultipleAssignment => that.canEqual(this) && this.bundle == that.bundle
+      case _ => false
+    }
+
+  override def hashCode(): Int = this.bundle.hashCode()
+
+  def canEqual(a: Any): Boolean = a.isInstanceOf[MultipleAssignment]
+
+  /**
+   * Returns a deep copy
+   */
+  @throws(classOf[RuntimeException])
+  private def copy(): MultipleAssignment = {
+    val assignment = new MultipleAssignment(pb)
+    this.bundle.foreach {
+      case (worker: Worker, tasks: List[Task]) =>
+        assignment.bundle = assignment.bundle.updated(worker, tasks)
+      case _ => throw new RuntimeException("Not able to copy bundle")
+    }
+    assignment
+  }
+
+  /**
+   * Adds a task to the list assigned to a worker
+   */
+  def update(worker: Worker, task: Task): MultipleAssignment = {
+    val allocation = this.copy()
+    val updatedTasks = allocation.bundle(worker) :+ task
+    allocation.bundle = allocation.bundle.updated(worker, updatedTasks)
+    allocation
+  }
+
+  /**
+   * Returns true if each task is assigned to no more than one worker
+   */
+  private def isPartition: Boolean = {
+    val allTasks = bundle.values.flatten.toList
+    allTasks.distinct.size == allTasks.size
+  }
+
+  /**
+   * Returns true if the assignment is complete, i.e., every task is assigned to a worker
+   */
+  private def isComplete: Boolean = {
+    val assignedTasks = bundle.values.flatten.toSet
+    pb.tasks.forall(assignedTasks.contains)
+  }
+
+  /**
+   * Returns true if the allocation is sound, i.e., a complete partition of the tasks
+   */
+  def isSound: Boolean = isPartition && isComplete
+
+  /**
+   * Returns the worker which has the task in its bundle
+   */
+  def worker(task: Task): Option[Worker] = {
+    bundle.find {
+      case (_, tasks) => tasks.contains(task)
+    }.map(_._1)
+  }
+}
\ No newline at end of file
diff --git a/src/main/scala/org/scata/core/SingleAssignment.scala b/src/main/scala/org/scata/core/SingleAssignment.scala
index 4133822b67793f214f8fca9146bf5d4694f7c271..d6eedaa631cabbfafcc8408e7de4ccc679773519 100644
--- a/src/main/scala/org/scata/core/SingleAssignment.scala
+++ b/src/main/scala/org/scata/core/SingleAssignment.scala
@@ -8,7 +8,7 @@ import scala.collection.SortedSet
  * a single-assignment of the tasks to some workers.
  * @param pb is a single-assignment instance
  */
-class SingleAssignment(val pb: SingleAssignmentProblem) {
+class SingleAssignment(val pb: AssignmentProblem) {
 
   var bundle: Map[Worker, Task] = Map[Worker, Task]()
 
diff --git a/src/main/scala/org/scata/patrol/Environment.scala b/src/main/scala/org/scata/patrol/Environment.scala
index 2efab9f5ba19cc4aa5ecbf5f9c737795fcfdc49d..2d1fe6d285e0e92e67a237a321b1c5c7ebb66916 100644
--- a/src/main/scala/org/scata/patrol/Environment.scala
+++ b/src/main/scala/org/scata/patrol/Environment.scala
@@ -1,8 +1,8 @@
 // Copyright (C) Maxime MORGE, 2024
 package org.scata.patrol
 
-import org.scata.core.{SingleAssignmentProblem, Task, Worker}
-import org.scata.core.SingleAssignmentProblem.MAX_COST
+import org.scata.core.{AssignmentProblem, Task, Worker}
+import org.scata.core.AssignmentProblem.MAX_COST
 
 import scala.collection.SortedSet
 import org.scata.utils.RandomUtils
@@ -39,7 +39,7 @@ class Environment(val robots: SortedSet[Robot],
   /**
    * Generate a single assignment problem
    */
-  def toSAP(): SingleAssignmentProblem = {
+  def toSAP(): AssignmentProblem = {
     if (n > m)
       throw new RuntimeException("Cannot generate a single assignment problem since there are more targets than robots")
 
@@ -62,7 +62,7 @@ class Environment(val robots: SortedSet[Robot],
       ((worker, task), distance.toInt)
     }).toMap
 
-    new SingleAssignmentProblem(workers, tasks, cost)
+    new AssignmentProblem(workers, tasks, cost)
   }
 }
 
@@ -74,8 +74,8 @@ object Environment{
   implicit val order  : Ordering[Double] = Ordering.Double.TotalOrdering
   // eventually Ordering.Double.IeeeOrdering
 
-  private val MAX_X : Int = 10 // Maximum width
-  private val MAX_Y : Int = 10 // Maximum height
+  private val MAX_X : Int = 20 // Maximum width
+  private val MAX_Y : Int = 20 // Maximum height
 
   /**
    * Returns a random single-assignment problem  instance with