diff --git a/TP_2_3/exo4.py b/TP_2_3/exo4.py
new file mode 100644
index 0000000000000000000000000000000000000000..4da879ae71aefcfb1c86dbb5d9ab1218f0503a3b
--- /dev/null
+++ b/TP_2_3/exo4.py
@@ -0,0 +1,403 @@
+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 avec contraintes de précédence
+class PrecedenceAllocation(TaskAllocation):
+    def __init__(self, robotliste, zoneliste, precedences=None):
+        super().__init__(robotliste, zoneliste)
+        
+        # Si aucune précédence n'est fournie, on en crée aléatoirement
+        if precedences is None:
+            self.precedences = self.generate_random_precedences()
+        else:
+            self.precedences = precedences
+        
+        # Vérifier la consistance des précédences
+        if not self.check_precedence_consistency():
+            print("Attention: Les contraintes de précédence contiennent des cycles!")
+    
+    def generate_random_precedences(self):
+        """
+        Génère aléatoirement des contraintes de précédence entre les zones.
+        Une contrainte (zone1, zone2) signifie que zone1 doit être nettoyée avant zone2.
+        """
+        precedences = []
+        
+        # Pour éviter les cycles, on crée un ordre aléatoire des zones
+        # et on ajoute des précédences uniquement dans cet ordre
+        zones_order = list(self.zoneliste)
+        random.shuffle(zones_order)
+        
+        # Ajouter quelques précédences aléatoires (pas trop pour garder ça simple)
+        num_precedences = random.randint(1, len(zones_order) - 1)
+        
+        for _ in range(num_precedences):
+            # Choisir deux indices aléatoires i < j pour éviter les cycles
+            i = random.randint(0, len(zones_order) - 2)
+            j = random.randint(i + 1, len(zones_order) - 1)
+            
+            # Ajouter la précédence
+            precedences.append((zones_order[i], zones_order[j]))
+        
+        return precedences
+    
+    def check_precedence_consistency(self):
+        """
+        Vérifie s'il n'y a pas de cycles dans les contraintes de précédence.
+        Utilise un algorithme de détection de cycle dans un graphe orienté.
+        """
+        # Construire un graphe orienté à partir des précédences
+        graph = {zone: [] for zone in self.zoneliste}
+        for zone1, zone2 in self.precedences:
+            graph[zone1].append(zone2)
+        
+        # Fonction pour détecter un cycle à partir d'un nœud
+        def has_cycle(node, visited, rec_stack):
+            visited[node] = True
+            rec_stack[node] = True
+            
+            for neighbor in graph[node]:
+                if not visited[neighbor]:
+                    if has_cycle(neighbor, visited, rec_stack):
+                        return True
+                elif rec_stack[neighbor]:
+                    return True
+            
+            rec_stack[node] = False
+            return False
+        
+        # Vérifier pour chaque nœud s'il y a un cycle
+        visited = {zone: False for zone in self.zoneliste}
+        rec_stack = {zone: False for zone in self.zoneliste}
+        
+        for zone in self.zoneliste:
+            if not visited[zone]:
+                if has_cycle(zone, visited, rec_stack):
+                    return False  # Il y a un cycle
+        
+        return True  # Pas de cycle
+    
+    def check_precedence_constraints(self, allocation):
+        """
+        Vérifie si une allocation respecte toutes les contraintes de précédence.
+        Une allocation respecte les contraintes si pour chaque précédence (zone1, zone2),
+        soit zone1 et zone2 sont attribuées au même robot et zone1 apparaît avant zone2 dans la liste,
+        soit zone1 est attribuée à un robot différent de zone2.
+        """
+        # Créer un dictionnaire qui associe chaque zone à son robot
+        zone_to_robot = {}
+        for robot, zones in allocation.items():
+            for zone in zones:
+                zone_to_robot[zone] = robot
+        
+        # Créer un dictionnaire qui associe chaque zone à sa position dans la liste de son robot
+        zone_to_position = {}
+        for robot, zones in allocation.items():
+            for i, zone in enumerate(zones):
+                zone_to_position[zone] = i
+        
+        # Vérifier chaque contrainte de précédence
+        for zone1, zone2 in self.precedences:
+            # Si les deux zones sont attribuées au même robot
+            if zone_to_robot[zone1] == zone_to_robot[zone2]:
+                # Vérifier que zone1 apparaît avant zone2 dans la liste
+                if zone_to_position[zone1] >= zone_to_position[zone2]:
+                    return False
+        
+        return True
+    
+    def allocation(self, cuf):
+        """
+        Calcule une allocation qui respecte les contraintes de précédence
+        et maximise l'utilité collective.
+        """
+        best_allocation = None
+        best_utility = float('-inf')
+        
+        # Générer toutes les allocations possibles (approche simplifiée)
+        for start in range(len(self.robotliste)):
+            current_allocation = {robot: [] for robot in self.robotliste}
+            
+            # Trier les zones selon les contraintes de précédence
+            sorted_zones = self.topological_sort()
+            
+            # Distribuer les zones de manière cyclique
+            for i, zone in enumerate(sorted_zones):
+                robot_index = (start + i) % len(self.robotliste)
+                current_allocation[self.robotliste[robot_index]].append(zone)
+            
+            # Vérifier si l'allocation respecte les contraintes de précédence
+            if self.check_precedence_constraints(current_allocation):
+                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 topological_sort(self):
+        """
+        Trie les zones selon un ordre topologique respectant les contraintes de précédence.
+        """
+        # Construire un graphe orienté à partir des précédences
+        graph = {zone: [] for zone in self.zoneliste}
+        in_degree = {zone: 0 for zone in self.zoneliste}
+        
+        for zone1, zone2 in self.precedences:
+            graph[zone1].append(zone2)
+            in_degree[zone2] += 1
+        
+        # Trouver toutes les zones sans prédécesseur
+        queue = []
+        for zone, degree in in_degree.items():
+            if degree == 0:
+                queue.append(zone)
+        
+        # Tri topologique
+        result = []
+        while queue:
+            zone = queue.pop(0)
+            result.append(zone)
+            
+            for neighbor in graph[zone]:
+                in_degree[neighbor] -= 1
+                if in_degree[neighbor] == 0:
+                    queue.append(neighbor)
+        
+        # Si le résultat n'a pas toutes les zones, il y a un cycle
+        if len(result) != len(self.zoneliste):
+            # Retourner une liste par défaut
+            return list(self.zoneliste)
+        
+        return result
+    
+    def print_precedences(self):
+        """
+        Affiche les contraintes de précédence.
+        """
+        print("Contraintes de précédence:")
+        for zone1, zone2 in self.precedences:
+            print(f"{zone1} doit être nettoyée avant {zone2}")
+
+
+# Test avec l'allocation avec contraintes de précédence
+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
+    zone1 = Zone("zone1", 4)
+    zone2 = Zone("zone2", 6)
+    zone3 = Zone("zone3", 1)
+    zone4 = Zone("zone4", 5)
+    zone5 = Zone("zone5", 3)
+    zone6 = Zone("zone6", 2)
+    
+    robotliste = [robot1, robot2, robot3]
+    zoneliste = [zone1, zone2, zone3, zone4, zone5, zone6]
+    
+    print("=== Test de l'allocation avec contraintes de précédence ===")
+    
+    # Création de contraintes de précédence manuelles pour l'exemple
+    precedences = [
+        (zone1, zone3),  # zone1 doit être nettoyée avant zone3
+        (zone2, zone4),  # zone2 doit être nettoyée avant zone4
+        (zone3, zone5),  # zone3 doit être nettoyée avant zone5
+        (zone4, zone6)   # zone4 doit être nettoyée avant zone6
+    ]
+    
+    # Création de l'allocation avec précédences
+    prec_allocation = PrecedenceAllocation(robotliste, zoneliste, precedences)
+    
+    # Afficher les contraintes de précédence
+    prec_allocation.print_precedences()
+    
+    # Vérifier la consistance des précédences
+    if prec_allocation.check_precedence_consistency():
+        print("\nLes contraintes de précédence sont consistantes (pas de cycles).")
+    else:
+        print("\nAttention: Les contraintes de précédence contiennent des cycles!")
+    
+    # Afficher le dictionnaire d'utilités
+    print("\nDictionnaire d'utilités:")
+    for (robot, zone), utility in prec_allocation.getdictio().items():
+        print(f"{robot} - {zone}: {utility}")
+    
+    # Créer une allocation naïve
+    dummy_alloc = prec_allocation.dummy_allocation(robotliste, zoneliste)
+    print("\nAllocation naïve (dummy):")
+    for robot, zones in dummy_alloc.items():
+        print(f"{robot}: {zones}")
+    
+    # Vérifier si l'allocation naïve respecte les contraintes
+    if prec_allocation.check_precedence_constraints(dummy_alloc):
+        print("L'allocation naïve respecte les contraintes de précédence.")
+    else:
+        print("L'allocation naïve ne respecte PAS les contraintes de précédence.")
+    
+    # Calculer une allocation qui respecte les contraintes
+    cuf_utilitariste = CollectiveUtilityFunction(prec_allocation.getdictio(), "utilitariste")
+    prec_alloc = prec_allocation.allocation(cuf_utilitariste)
+    
+    print("\nAllocation avec contraintes de précédence:")
+    for robot, zones in prec_alloc.items():
+        print(f"{robot}: {zones}")
+    
+    # Vérifier si l'allocation respecte les contraintes
+    if prec_allocation.check_precedence_constraints(prec_alloc):
+        print("L'allocation respecte les contraintes de précédence.")
+    else:
+        print("L'allocation ne respecte PAS les contraintes de précédence.")
+    
+    # Calculer le temps de nettoyage
+    cleaning_time = prec_allocation.calculate_cleaning_time(prec_alloc)
+    print(f"Temps de nettoyage: {cleaning_time:.2f} heures")
+    
+    # Comparer avec l'allocation sans contraintes
+    print("\n=== Comparaison avec l'allocation sans contraintes ===")
+    
+    # Allocation utilitariste sans contraintes
+    task_allocation = TaskAllocation(robotliste, zoneliste)
+    alloc_utilitariste = task_allocation.allocation(cuf_utilitariste)
+    
+    print("\nAllocation sans contraintes (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")
+    
+    # Vérifier si l'allocation sans contraintes respecte les contraintes de précédence
+    if prec_allocation.check_precedence_constraints(alloc_utilitariste):
+        print("L'allocation sans contraintes respecte les contraintes de précédence (par chance).")
+    else:
+        print("L'allocation sans contraintes ne respecte PAS les contraintes de précédence.")
\ No newline at end of file