diff --git a/TP_2_3/exo3.py b/TP_2_3/exo3.py
new file mode 100644
index 0000000000000000000000000000000000000000..ac8198d78040442b8e53943e0abda423730701cd
--- /dev/null
+++ b/TP_2_3/exo3.py
@@ -0,0 +1,348 @@
+import random
+import itertools
+
+class Robot:
+    def __init__(self, id, capacity):
+        self.id = id
+        self.capacity = capacity
+    
+    def __repr__(self):
+        return f"Robot({self.id})"
+        
+
+class Zone:
+    def __init__(self, id, dirt):
+        self.id = id
+        self.dirt = dirt
+    
+    def __repr__(self):
+        return f"Zone({self.id})"
+
+class TaskAllocation:
+    def __init__(self, robotliste, zoneliste):
+        self.robotliste = robotliste
+        self.zoneliste = zoneliste
+        # Création d'un dictionnaire d'utilités aléatoires pour chaque paire (robot, zone)
+        self.dictio = {(robot, zone): random.randint(1, 10) for robot, zone in itertools.product(robotliste, zoneliste)}
+
+    def allocation(self, cuf):
+        # On garde votre structure mais on corrige le problème d'assignation
+        best_allocation = None
+        best_utility = float('-inf')  # On commence avec une utilité très basse
+        
+        # On utilise une approche simplifiée pour générer des allocations
+        # On distribue les zones de manière cyclique avec différents points de départ
+        for start in range(len(self.robotliste)):
+            current_allocation = {robot: [] for robot in self.robotliste}
+            for i, zone in enumerate(self.zoneliste):
+                robot_index = (start + i) % len(self.robotliste)
+                current_allocation[self.robotliste[robot_index]].append(zone)
+            
+            utility = cuf.utility(current_allocation)
+            if best_allocation is None or utility > best_utility:
+                best_utility = utility
+                best_allocation = current_allocation
+        
+        return best_allocation
+
+    def dummy_allocation(self, robotliste, zoneliste):
+        # On garde votre fonction exactement comme elle était
+        i = 0
+        dico = {robot: [] for robot in robotliste}
+        for zone in zoneliste:
+            robot = robotliste[i % len(robotliste)]
+            dico[robot].append(zone)
+            i += 1
+        return dico
+
+    def calculate_cleaning_time(self, allocation):
+        # Nouvelle fonction pour calculer le temps de nettoyage
+        robot_times = []
+        
+        for robot, zones in allocation.items():
+            total_dirt = sum(zone.dirt for zone in zones)
+            cleaning_time = total_dirt / robot.capacity
+            robot_times.append(cleaning_time)
+        
+        # Le temps total est le maximum des temps individuels
+        return max(robot_times) if robot_times else 0
+
+    def getdictio(self):
+        return self.dictio
+
+   
+class CollectiveUtilityFunction:
+    def __init__(self, dico, utility):
+        # On garde votre nom de paramètre "utility" au lieu de "utility_type"
+        self.utility = getattr(self, utility)
+        self.dico = dico
+
+    def utilitariste(self, allocation):
+        total_uti = 0
+        for robot, zones in allocation.items():
+            for zone in zones:
+                total_uti += self.dico[(robot, zone)]
+        return total_uti
+
+    def egalitariste(self, allocation):
+        uti_par_robot = {}
+        for robot, zones in allocation.items():
+            robot_utility = 0
+            for zone in zones:
+                robot_utility += self.dico[(robot, zone)]
+            uti_par_robot[robot] = robot_utility
+        
+        # Si aucun robot n'a d'utilité, retourner 0
+        if not uti_par_robot:
+            return 0
+            
+        return min(uti_par_robot.values())
+
+    def elitiste(self, allocation):
+        uti_par_robot = {}
+        for robot, zones in allocation.items():
+            robot_utility = 0
+            for zone in zones:
+                robot_utility += self.dico[(robot, zone)]
+            uti_par_robot[robot] = robot_utility
+            
+        # Si aucun robot n'a d'utilité, retourner 0
+        if not uti_par_robot:
+            return 0
+            
+        return max(uti_par_robot.values())
+
+    def nash(self, allocation):
+        produit_uti = 1
+        for robot, zones in allocation.items():
+            robot_uti = 0
+            for zone in zones:
+                robot_uti += self.dico[(robot, zone)]
+            
+            # Pour éviter de multiplier par 0
+            if robot_uti > 0:
+                produit_uti *= robot_uti
+        
+        return produit_uti
+
+
+# Nouvelle classe pour l'allocation distribuée
+class DistributedAllocation(TaskAllocation):
+    def __init__(self, robotliste, zoneliste):
+        super().__init__(robotliste, zoneliste)
+        # Dictionnaire pour stocker les préférences de chaque robot
+        self.preferences = {}
+        # Dictionnaire pour stocker le type de préférence de chaque robot
+        self.robot_types = {}
+        
+        # Assigner aléatoirement un type de préférence à chaque robot
+        types = ["utilitariste", "egalitariste", "elitiste", "egoiste"]
+        for robot in robotliste:
+            self.robot_types[robot] = random.choice(types)
+    
+    def generate_all_allocations(self):
+        """
+        Génère toutes les allocations possibles où chaque robot a au moins une zone
+        et le nombre de zones est égal au nombre de robots.
+        Pour simplifier, on suppose que chaque robot reçoit exactement une zone.
+        """
+        # Pour simplifier, on ne considère que les allocations où chaque robot a exactement une zone
+        # et on suppose qu'il y a autant de zones que de robots
+        if len(self.robotliste) != len(self.zoneliste):
+            print("Pour simplifier, on suppose qu'il y a autant de zones que de robots.")
+            # On prend les n premières zones où n est le nombre de robots
+            zones_to_use = self.zoneliste[:len(self.robotliste)]
+        else:
+            zones_to_use = self.zoneliste
+        
+        # Générer toutes les permutations possibles des zones
+        all_permutations = list(itertools.permutations(zones_to_use))
+        
+        # Créer les allocations correspondantes
+        all_allocations = []
+        for perm in all_permutations:
+            allocation = {}
+            for i, robot in enumerate(self.robotliste):
+                allocation[robot] = [perm[i]]
+            all_allocations.append(allocation)
+        
+        return all_allocations
+    
+    def calculate_preference(self, robot, allocation):
+        """
+        Calcule la préférence d'un robot pour une allocation donnée
+        selon son type de préférence.
+        """
+        robot_type = self.robot_types[robot]
+        
+        if robot_type == "utilitariste":
+            # Préfère l'allocation qui maximise l'utilité totale
+            total_utility = 0
+            for r, zones in allocation.items():
+                for zone in zones:
+                    total_utility += self.dictio[(r, zone)]
+            return total_utility
+        
+        elif robot_type == "egalitariste":
+            # Préfère l'allocation qui maximise l'utilité minimale
+            utilities = []
+            for r, zones in allocation.items():
+                robot_utility = sum(self.dictio[(r, zone)] for zone in zones)
+                utilities.append(robot_utility)
+            return min(utilities) if utilities else 0
+        
+        elif robot_type == "elitiste":
+            # Préfère l'allocation qui maximise l'utilité maximale
+            utilities = []
+            for r, zones in allocation.items():
+                robot_utility = sum(self.dictio[(r, zone)] for zone in zones)
+                utilities.append(robot_utility)
+            return max(utilities) if utilities else 0
+        
+        elif robot_type == "egoiste":
+            # Préfère l'allocation qui maximise sa propre utilité
+            if robot in allocation:
+                return sum(self.dictio[(robot, zone)] for zone in allocation[robot])
+            return 0
+        
+        return 0
+    
+    def generate_preferences(self):
+        """
+        Génère les préférences de chaque robot pour toutes les allocations possibles.
+        """
+        all_allocations = self.generate_all_allocations()
+        
+        # Pour chaque robot, calculer sa préférence pour chaque allocation
+        for robot in self.robotliste:
+            # Calculer les scores de préférence pour chaque allocation
+            preferences = []
+            for allocation in all_allocations:
+                score = self.calculate_preference(robot, allocation)
+                preferences.append((allocation, score))
+            
+            # Trier les allocations par score décroissant
+            preferences.sort(key=lambda x: x[1], reverse=True)
+            
+            # Stocker les préférences triées
+            self.preferences[robot] = preferences
+    
+    def allocation(self):
+        """
+        Implémente un algorithme distribué simple de type contract-net.
+        Les robots votent pour leurs allocations préférées et on élimine
+        progressivement les allocations les moins populaires.
+        """
+        # Générer les préférences des robots
+        self.generate_preferences()
+        
+        # Obtenir toutes les allocations possibles
+        all_allocations = self.generate_all_allocations()
+        
+        # Si aucune allocation n'est possible, retourner une allocation vide
+        if not all_allocations:
+            return {robot: [] for robot in self.robotliste}
+        
+        # Tant qu'il reste plus d'une allocation
+        while len(all_allocations) > 1:
+            # Compter les votes pour chaque allocation
+            votes = {i: 0 for i in range(len(all_allocations))}
+            
+            # Chaque robot vote pour sa préférence parmi les allocations restantes
+            for robot in self.robotliste:
+                # Trouver l'allocation préférée du robot parmi celles restantes
+                for pref, _ in self.preferences[robot]:
+                    if pref in all_allocations:
+                        # Trouver l'index de cette allocation
+                        idx = all_allocations.index(pref)
+                        votes[idx] += 1
+                        break
+            
+            # Trouver l'allocation avec le moins de votes
+            min_votes = min(votes.values())
+            min_indices = [i for i, v in votes.items() if v == min_votes]
+            
+            # En cas d'égalité, choisir aléatoirement
+            idx_to_remove = random.choice(min_indices)
+            
+            # Supprimer l'allocation la moins populaire
+            all_allocations.pop(idx_to_remove)
+        
+        # Retourner la dernière allocation restante
+        return all_allocations[0]
+    
+    def print_robot_types(self):
+        """
+        Affiche le type de préférence de chaque robot.
+        """
+        print("Types de préférence des robots:")
+        for robot, robot_type in self.robot_types.items():
+            print(f"{robot}: {robot_type}")
+
+
+# Test avec 3 robots et 3 zones pour l'allocation distribuée
+if __name__ == "__main__":
+    # Fixer la graine aléatoire pour des résultats reproductibles
+    random.seed(42)
+    
+    # Création des robots
+    robot1 = Robot("robot1", 10)
+    robot2 = Robot("robot2", 6)
+    robot3 = Robot("robot3", 4)
+    
+    # Création des zones (même nombre que de robots pour simplifier)
+    zone1 = Zone("zone1", 4)
+    zone2 = Zone("zone2", 6)
+    zone3 = Zone("zone3", 1)
+    
+    robotliste = [robot1, robot2, robot3]
+    zoneliste = [zone1, zone2, zone3]
+    
+    print("=== Test de l'allocation distribuée ===")
+    
+    # Création de l'allocation distribuée
+    dist_allocation = DistributedAllocation(robotliste, zoneliste)
+    
+    # Afficher les types de préférence des robots
+    dist_allocation.print_robot_types()
+    
+    # Afficher le dictionnaire d'utilités
+    print("\nDictionnaire d'utilités:")
+    for (robot, zone), utility in dist_allocation.getdictio().items():
+        print(f"{robot} - {zone}: {utility}")
+    
+    # Générer les préférences
+    dist_allocation.generate_preferences()
+    
+    # Afficher les préférences de chaque robot
+    print("\nPréférences des robots:")
+    for robot, preferences in dist_allocation.preferences.items():
+        print(f"\n{robot}:")
+        for i, (allocation, score) in enumerate(preferences[:3]):  # Afficher les 3 premières préférences
+            print(f"  {i+1}. Score: {score}")
+            for r, zones in allocation.items():
+                print(f"     {r}: {zones}")
+    
+    # Exécuter l'algorithme d'allocation distribuée
+    print("\nRésultat de l'allocation distribuée:")
+    result = dist_allocation.allocation()
+    for robot, zones in result.items():
+        print(f"{robot}: {zones}")
+    
+    # Calculer le temps de nettoyage
+    cleaning_time = dist_allocation.calculate_cleaning_time(result)
+    print(f"Temps de nettoyage: {cleaning_time:.2f} heures")
+    
+    # Comparer avec l'allocation centralisée
+    print("\n=== Comparaison avec l'allocation centralisée ===")
+    
+    # Allocation utilitariste
+    task_allocation = TaskAllocation(robotliste, zoneliste)
+    cuf_utilitariste = CollectiveUtilityFunction(task_allocation.getdictio(), "utilitariste")
+    alloc_utilitariste = task_allocation.allocation(cuf_utilitariste)
+    
+    print("\nAllocation centralisée (utilitariste):")
+    for robot, zones in alloc_utilitariste.items():
+        print(f"{robot}: {zones}")
+    print(f"Utilité utilitariste: {cuf_utilitariste.utility(alloc_utilitariste)}")
+    print(f"Temps de nettoyage: {task_allocation.calculate_cleaning_time(alloc_utilitariste):.2f} heures")
\ No newline at end of file