from collections import deque
from itertools import product

class CrossSumsCSP:
    def __init__(self, grid, rows, cols):
        self.grid = grid
        self.rows = rows
        self.cols = cols
        self.n_rows = len(grid)
        self.n_cols = len(grid[0])
        
        # Variables: chaque cellule (i,j) peut prendre la valeur 0 ou grid[i][j]
        self.variables = [(i, j) for i in range(self.n_rows) for j in range(self.n_cols)]
        
        # Domaines: {0, valeur_originale} pour chaque variable
        self.domains = {}
        for i in range(self.n_rows):
            for j in range(self.n_cols):
                self.domains[(i, j)] = {0, self.grid[i][j]}
        
        # Contraintes: une par ligne et une par colonne
        self.constraints = []
        
        # Contraintes de lignes
        for i in range(self.n_rows):
            variables_in_row = [(i, j) for j in range(self.n_cols)]
            self.constraints.append(('row', i, variables_in_row, self.rows[i]))
        
        # Contraintes de colonnes
        for j in range(self.n_cols):
            variables_in_col = [(i, j) for i in range(self.n_rows)]
            self.constraints.append(('col', j, variables_in_col, self.cols[j]))
        print(self.constraints)
    def is_consistent(self, assignment, constraint):
        """Vérifie si une assignation partielle est consistante avec une contrainte"""
        constraint_type, index, variables, target_sum = constraint
        
        current_sum = 0
        unassigned_count = 0
        max_possible = 0
        
        for var in variables:
            if var in assignment:
                current_sum += assignment[var]
            else:
                unassigned_count += 1
                # Contribution maximale possible de cette variable
                max_possible += max(self.domains[var])
        
        # Vérifier si c'est encore possible d'atteindre la somme cible
        if current_sum > target_sum:
            return False
        if current_sum + max_possible < target_sum:
            return False
        
        return True
    
    def revise(self, var1, var2, constraint):
        """Révise le domaine de var1 en fonction de var2 et de la contrainte"""
        revised = False
        constraint_type, index, variables, target_sum = constraint
        
        if var1 not in variables or var2 not in variables:
            return False
        
        to_remove = []
        
        for value1 in self.domains[var1]:
            # Vérifier si cette valeur pour var1 est compatible avec au moins une valeur de var2
            compatible = False
            
            for value2 in self.domains[var2]:
                # Créer une assignation partielle pour tester
                partial_assignment = {var1: value1, var2: value2}
                
                if self.is_consistent(partial_assignment, constraint):
                    compatible = True
                    break
            
            if not compatible:
                to_remove.append(value1)
        
        for value in to_remove:
            self.domains[var1].remove(value)
            revised = True
        
        return revised
    
    def get_related_constraints(self, var):
        """Retourne les contraintes qui impliquent cette variable"""
        related = []
        for constraint in self.constraints:
            if var in constraint[2]:  # constraint[2] sont les variables de la contrainte
                related.append(constraint)
        return related
    
    def ac3(self):
        """Algorithme AC-3 pour la consistance d'arc"""
        # Initialiser la queue avec tous les arcs
        queue = deque()
        
        for constraint in self.constraints:
            variables_in_constraint = constraint[2]
            # Ajouter tous les arcs entre les variables de cette contrainte
            for i in range(len(variables_in_constraint)):
                for j in range(i+1, len(variables_in_constraint)):
                    var1, var2 = variables_in_constraint[i], variables_in_constraint[j]
                    queue.append((var1, var2, constraint))
                    queue.append((var2, var1, constraint))
        
        while queue:
            var1, var2, constraint = queue.popleft()
            
            if self.revise(var1, var2, constraint):
                if len(self.domains[var1]) == 0:
                    return False  # Pas de solution
                
                # Ajouter les arcs affectés à la queue
                for other_constraint in self.get_related_constraints(var1):
                    if other_constraint != constraint:
                        for var in other_constraint[2]:
                            if var != var1 and var != var2:
                                queue.append((var, var1, other_constraint))
        
        return True
    
    def propagate_constraint(self, constraint):
        """Propage une contrainte spécifique pour réduire les domaines"""
        constraint_type, index, variables, target_sum = constraint
        
        # Calculer les valeurs min et max possibles
        min_sum = sum(min(self.domains[var]) for var in variables)
        max_sum = sum(max(self.domains[var]) for var in variables)
        
        # Si impossible, domaines vides
        if min_sum > target_sum or max_sum < target_sum:
            for var in variables:
                self.domains[var] = set()
            return False
        
        # Propagation plus fine
        for var in variables:
            to_remove = []
            for value in self.domains[var]:
                # Calculer la somme des autres variables
                other_min = sum(min(self.domains[other_var]) for other_var in variables if other_var != var)
                other_max = sum(max(self.domains[other_var]) for other_var in variables if other_var != var)
                
                # Vérifier si cette valeur peut mener à la somme cible
                if other_min + value > target_sum or other_max + value < target_sum:
                    to_remove.append(value)
            
            for value in to_remove:
                self.domains[var].remove(value)
        
        return True
    
    def constraint_propagation(self):
        """Applique la propagation de contraintes"""
        changed = True
        while changed:
            changed = False
            for constraint in self.constraints:
                if not self.propagate_constraint(constraint):
                    return False
                # Vérifier si des domaines ont été modifiés
                for var in self.variables:
                    if len(self.domains[var]) == 0:
                        return False
        return True
    
    def is_complete(self, assignment):
        """Vérifie si l'assignation est complète"""
        return len(assignment) == len(self.variables)
    
    def is_consistent_assignment(self, assignment):
        """Vérifie si l'assignation complète respecte toutes les contraintes"""
        for constraint in self.constraints:
            if not self.is_consistent(assignment, constraint):
                return False
        return True
    
    def select_unassigned_variable(self, assignment):
        """Sélectionne la prochaine variable à assigner (heuristique MRV)"""
        unassigned = [var for var in self.variables if var not in assignment]
        if not unassigned:
            return None
        
        # Heuristique MRV (Minimum Remaining Values)
        return min(unassigned, key=lambda var: len(self.domains[var]))
    
    def backtrack(self, assignment):
        """Recherche par backtracking avec propagation de contraintes"""
        if self.is_complete(assignment):
            return assignment
        
        var = self.select_unassigned_variable(assignment)
        if var is None:
            return None
        
        for value in sorted(self.domains[var]):
            assignment[var] = value
            
            # Sauvegarder les domaines avant la propagation
            saved_domains = {v: domain.copy() for v, domain in self.domains.items()}
            
            # Vérifier la consistance avec les contraintes
            consistent = True
            for constraint in self.get_related_constraints(var):
                if not self.is_consistent(assignment, constraint):
                    consistent = False
                    break
            
            if consistent:
                # Propager les contraintes
                if self.constraint_propagation():
                    result = self.backtrack(assignment)
                    if result is not None:
                        return result
            
            # Restaurer les domaines
            self.domains = saved_domains
            del assignment[var]
        
        return None
    
    def solve(self):
        """Résout le CSP en utilisant AC-3 et backtracking"""
        print("Application de AC-3...")
        
        # Appliquer AC-3 pour la consistance d'arc
        if not self.ac3():
            print("Aucune solution trouvée par AC-3")
            return None
        
        print("AC-3 terminé. Domaines après AC-3:")
        for var in self.variables:
            if len(self.domains[var]) > 1:
                print(f"Variable {var}: {self.domains[var]}")
        
        # Propagation de contraintes initiale
        if not self.constraint_propagation():
            print("Aucune solution trouvée après propagation")
            return None
        
        print("Démarrage du backtracking...")
        solution = self.backtrack({})
        
        if solution:
            # Convertir en grille
            result_grid = [[0 for _ in range(self.n_cols)] for _ in range(self.n_rows)]
            for (i, j), value in solution.items():
                result_grid[i][j] = value
            return result_grid
        
        return None

def solve_cross_sums_csp(grid, rows, cols):
    """Interface principale pour résoudre le problème Cross Sums avec CSP"""
    csp = CrossSumsCSP(grid, rows, cols)
    return csp.solve()

def print_solution(solution):
    """Affiche une solution de manière lisible"""
    if solution is None:
        print("Aucune solution trouvée")
        return
    
    for row in solution:
        print(row)

def verify_solution(solution, rows, cols):
    """Vérifie qu'une solution respecte les contraintes"""
    if solution is None:
        return False
    
    # Vérifier les lignes
    for i, row in enumerate(solution):
        if sum(row) != rows[i]:
            print(f"Erreur ligne {i}: somme = {sum(row)}, attendu = {rows[i]}")
            return False
    
    # Vérifier les colonnes
    for j in range(len(solution[0])):
        col_sum = sum(solution[i][j] for i in range(len(solution)))
        if col_sum != cols[j]:
            print(f"Erreur colonne {j}: somme = {col_sum}, attendu = {cols[j]}")
            return False
    
    return True

# Test avec l'exemple fourni

grid = [[3, 7, 2, 4], [4, 1, 6, 2], [1, 4, 8, 6], [3, 1, 6, 2]]
rows = [2, 3, 5, 8]
cols = [1, 5, 8, 4]

print("Grille originale:")
for row in grid:
    print(row)
print(f"\nContraintes - Lignes: {rows}, Colonnes: {cols}")

solution = solve_cross_sums_csp(grid, rows, cols)

print("\nSolution trouvée:")
print_solution(solution)

if verify_solution(solution, rows, cols):
    print("\n✓ Solution vérifiée avec succès!")
else:
    print("\n✗ Erreur dans la solution")

Embed on website

To embed this program on your website, copy the following code and paste it into your website's HTML: