Problème codage python algorithme A*

PangolinRobuste42 Messages postés 8 Date d'inscription vendredi 29 mars 2024 Statut Membre Dernière intervention 5 avril 2024 - Modifié le 4 avril 2024 à 12:29
mamiemando Messages postés 33093 Date d'inscription jeudi 12 mai 2005 Statut Modérateur Dernière intervention 4 mai 2024 - 6 avril 2024 à 14:23

Bonjour,

Dans le cadre de mes études, je dois coder un programme qui résout un jeu du taquin. Mon choix c'est porté sur un algorithme A*.

Cependant, mon programme n'est pas efficace, et prend beaucoup de coup pour résoudre un taquin. Je ne comprends pas pourquoi mon algorithme prend autant de coup pour résoudre un taquin, ce qui le rend très lent pour des taquins  4*4

Ci-joint mon code :

# Renvoie toutes les positions du taquin
# après une actions licites
def f_pose_k(M):
    """
    Renvoie la position de la case qui bouge.
    """
    return [
        k
        for k in range(len(M))
        if M[k] == len(M)
    ][0]
    
def deplacement_possible(taille_M, k):
    """
    Renvoie les coordonnées des déplacements possibles si la
    case 16 est à la position k
    """
    liste = []
    # Disjonctions classiques des cas pour
    # sélectionner les déplacements licites
    
    if k % taille_M != 1:
        # Colonne gauche
        liste.append(k - 1)
    if k > taille_M:
        # Ligne haute
        liste.append(k - taille_M)
    if k % taille_M != 0:
        # Colonne droite
        liste.append(k+1)
    if k <= (taille_M ** 2 - taille_M):
        # ligne basse
        liste.append(k+taille_M)
        
    return liste

def permutation_taquin(M,pose_k,cible):
    """
    Permute deux cases.
    Cette fonction sert pour une compréhension de texte.
    """
    # Pour ne pas modifier M avec les permutations
    N = [M[k] for k in range(len(M))]
    # Permute les deux cases cibles
    N[pose_k] , N[cible] = N[cible] , N[pose_k]
    return N
    
def possibilite(M):
    """
    Renvoie toutes les positions du taquin après
    une actions licite.
    """
    # Position de la case guidant le mouvement
    pose_k = f_pose_k(M)
    # Liste des déplacements licites de la matrice
    dep_licite = deplacement_possible(
        int(len(M) ** 0.5),
        pose_k + 1
    )
    return [
        permutation_taquin(M,pose_k,cible-1)
        for cible in dep_licite
    ]

# Heuristique
matrice_de_poids = [
   5, 5, 4, 4, # ici taille 4
   4, 4, 3, 3,
   3, 2, 2, 2,
   2, 1, 1, 1
]

# Programme pour la fonction heuristique_1
def ligne(taille_M, case):
    """
    Ligne du jeu du taquin
    """
    for k in range(1, taille_M + 1):
        if case <= k * taille_M:
            return k
    return taille_M

def heuristique_Manhattan(M):
    """
    Fonction heuristique dite "Manhattan", distance idéale.
    """
    # taille_M provisoire
    # len sera calculé au début de la fonction total
    # et la variable len M déjà établie.
    taille_M = int(len(M)**0.5)
    
    distance = 0
    for case in range(taille_M ** 2 - 1):
        # print(case + 1)
        # Différence de hauteur(en terme de ligne)
        # entre la case rangée et la case du sudoku
        difference_hauteur = abs(
            ligne(taille_M, M[case]) - 
            ligne(taille_M,case+1)
        )
        # Compte les déplacements verticaux  
        distance += difference_hauteur
        
        # Note : on peut pondérer la distance en
        # la multipliant par matrice_de_poids[case]
        
        distance += abs(
            (max(case + 1, M[case])) -
            (min(case + 1, M[case]) +
            taille_M*difference_hauteur)
        )
        # Compte les déplacements horizontaux
        # Principe: on ramène les cases sur une même ligne
        # (après les déplacements verticaux, puis soustraction)

    return distance

# A*
# On fait le choix d'un tri par insertion car chaque élément
# est ajouté 1 par 1.
def tri_insertion_dichotomie(liste,noeuds):
    """
    Ajoute un élément dans une liste trié par dichotomie.
    """
    # Optimisation par récursive
    n = len(liste)

    if n == 1:
        if liste[0][3] > noeuds[3]:
            # evalue pour la valeur de l'heuristique,
            # ie pour l'indice 3
            return [noeuds,liste[0]]
        else:
            return [liste[0],noeuds]

    if noeuds[3] <= liste[n // 2][3]:
        # Si élément est dans la partie inférieure
        # de la liste trié
        return (
            tri_insertion_dichotomie(liste[: n//2], noeuds) +
            liste[n // 2:]
        )
    
    if noeuds[3] > liste[n // 2][3]:
        # Si élément est dans la partie supérieure
        # de la liste trié
        return (
            liste[: n // 2] +
            tri_insertion_dichotomie(liste[n//2:], noeuds)
        )

def algorithme_A_star(M, heuristique):
    """
    Cherche un chemin en étant guidée par un fonction heuristique
    """
    n = len(M)
    # Idée directrice de l'algorithme :
    # Nous allons créer un graphe au fur et à mesure.
    # Chaque nœud est obtenu par une action licite
    # du taquin qui y contient le graphe, est ordonné par
    # le nombre d'actions nécessaires pour y aller.
    # Le point clef est: nous allons naviguer dans le graphe
    # de manière ciblée (nous évaluerons prioritairement
    # les nœuds possédant une valeur heuristique faible)
    # ce que contient un nœud.
    # Si un nœud n'a jamais été testé : noeuds[0] = True
    # Réciproquement,  noeuds[0] = False,
    # De plus, un nœud possède:
    # - la matrice représentatrice du taquin
    # - le chemin réalisé pour y parvenir
    # - un approximation du nombre d'action nécessaire
    #   pour résoudre cette matrice.
    

    # Initialisation:
    # Cette liste va être triée au fur et à mesure,
    # afin de classer les potentiels candidats selon la valeur
    # de la fonction heuristique
    graphe = [
        [False, M, [], heuristique(M)]
    ]

    # La tête est la configuration du taquin qui est étudié,
    # et qui est changée à chaque tour de boucle.
    tete = [False, M, [], heuristique(M)]
    
    indice = 0
    while tete[1] != [k for k in range(1,n+1)]:
        # Tant que le taquin n'est pas résolu
        for noeuds in possibilite(tete[1]):
            # Ajoute les taquin obtenus depuis la tête au
            # graphe, en triant ce dernier par la fonction
            # heuristique associé aux taquins.
            # Trie le graphe en fonction de la
            # valeur de l'heuristique.
            graphe = tri_insertion_dichotomie(
                graphe,
                [True, noeuds, tete[2] + [tete[1]],
                heuristique(noeuds)]
            )
        
        indice = 0
        # print(graphe,"graphe")
        while (
            not graphe[indice][0]
            or graphe[indice][1] in [
                taquin[1]
                for taquin in graphe
                if not taquin[0]
            ]
        ):
            # Si le taquin n'a jamais été testé et
            # n'appartient pas aux taquins déjà testés
            # (possibilité de revenir sur un même taquin
            # après différents déplacements.
            # On cherche le candidat qui respecte les conditions
            # ci-dessus, le graphe étant trié par son heuristique.
            indice += 1 

            # print(
            #     memoire,
            #     not graphe[indice][0],
            #     graphe[indice][1] in memoire,
            #     "passe ou casse"
            # )
        
        graphe[indice][0] = False
        tete = graphe[indice]

    # Compile le chemin et le taquin final
    return tete[2] + [tete[1]]

# Fonction finale
def show(M):
    n = int(len(M) ** 0.5)
    for k in range(n):
        print([M[k * n + j] for j in range(n)])

def taquin(M):
    chemin = algorithme_A_star(M, heuristique_Manhattan)
    for taquin in chemin:
        print("to")
        show(taquin)
    return len(chemin)
A voir également:

5 réponses

mamiemando Messages postés 33093 Date d'inscription jeudi 12 mai 2005 Statut Modérateur Dernière intervention 4 mai 2024 7 752
4 avril 2024 à 12:53

Bonjour,

Si j'ai bien compris, tu veux utiliser l'algorithme A* dans le graphe des configuration du taquin :

  • Chaque nœud correspond à une disposition possible du taquin
  • On construit un lien (u, v) si en un mouvement, on peut passer de u à v.
  • Le but est alors de trouver un chemin entre la configuration de départ s et une configuration cible t (typiquement, celle où les nombres sont dans le bon ordre). L'existence d'un tel chemin n'est pas garantie (voir ici).
  • Comme ce graphe est très grand (voir ici), tu ne peux pas te permettre de le construire complètement, et c'est pourquoi tu vas vouloir le construire dynamiquement, en fonction des configurations qui te paraissent les plus prometteuses.

En admettant que j'ai bien compris il y a plusieurs aspects pour avoir un programme efficace :

  • Dans ton problème, il est clair qu'il existe dans ce graphe plusieurs chemins pour aller d'une configuration u à une configuration v (ne serait ce qu'en faisant des alternance de mouvement inutile). Cela signifie que tu as intérêt dans ton exploration à être prudent, et à ne pas traiter une configuration déjà traitée. On parle alors de programmation dynamique. Je suppose que c'est ce que tu appelles dans ton code "si un taquin a déjà été testé" ?

Dans ton implémentation :

  • Certaines fonctions sont fréquemment appelées et pas optimisées. Par exemple f_pose_k construit une liste en mémoire (dont seule la première valeur est utile). Il vaudrait mieux utiliser la méthode find.
  • Tu passes ton temps à recalculer la largeur/hauteur de ta grille (ici 4) avec len(M) ** 0.5. Ce n'est pas très stratégique pour plusieurs raisons.
    • De manière générale, un calcul de puissance est cher (plus qu'un appel à math.sqrt)
    • Ici, il suffirait de calculer une fois pour toute cette valeur (appelons la n) et passer n en paramètre à tes fonctions qui en ont besoin.
  • Tu mémorises dans chaque nœud le chemin utilisé pour l'atteindre. Ce chemin n'étant en général pas unique, cela veut dire qu'il faut élire le meilleur.
    • Ce n'est sans doute pas un problème dans A* (comparé à Dijkstra) mais ça soulève surtout un problème du point de vue de la mémoire.
    • Supposons que ton chemin soit s -> u -> v -> w -> t, cela signie que dans u : tu mémorises [s], puis dans v : [s, u], puis dans w [s, u, v], puis dans t [s, u, v, w]. Clairement c'est redondant donc ça prend plus de mémoire et surtout ça te force à construire toutes ces listes.
    • En général, on utilise plutôt un vecteur de prédécesseurs (donc on associe à un sommet depuis quel sommet on l'a atteint -- quel arc dans le cas général). Dans mon exemple, ce serait {u: s, v: u, w: v, t: w}. Pour reconstruire le chemin, on part de la cible et on "remonte" le vecteur pour reconstruire le chemin.

Peut-être aussi que ça vaudrait le coup que tu regardes des implémentations existantes, par exemple celle-ci.

2

Bonsoir,
 

Des fois mamiemando, je me demande pourquoi tu te décarcasses à faire des réponses aussi élaborées et complètes, alors qu'en majorité, le créateur du sujet ne prend jamais la peine de revenir au moins dire un petit merci. Suffit de regarder les sujets où tu es le dernier intervenant avec des reponses toujours détaillées. Tristesse, ccm se meurt...

2
georges97 Messages postés 11879 Date d'inscription lundi 31 janvier 2011 Statut Contributeur Dernière intervention 11 mai 2024 2 266 > goulu
Modifié le 4 avril 2024 à 22:02

Bonsoir à tous,
 

J'espère que mamiemando, que je salue, continuera longtemps à nous dispenser ce savoir qu'elle est une des seules à exposer avec tant de talent pédagogique et d'abnégation.
 

Si des visiteurs aussi éphémères qu'irrespectueux n'appliquent pas les règles élémentaires de politesse, qui devraient prévaloir même en dehors de la charte, ignorée ou contestée.
 

Au moins ces mises au point servent elles à celles et ceux qui essaient d'améliorer leurs connaissances pour mieux aider les autres membres, en exigeant au minimum les salutations usuelles et sans attendre un retour qui statistiquement est la portion congrue.
 

Alors, à quoi sert que mamiemando se décarcasse ? Au moins à recevoir parmi d'autres mes remerciements pour son dévouement.

1
PangolinRobuste42 Messages postés 8 Date d'inscription vendredi 29 mars 2024 Statut Membre Dernière intervention 5 avril 2024
5 avril 2024 à 16:20

je te remercie pour ta réponse très complète!, je vais tacher de résoudre tous les problèmes que tu as judicieusement relevés dès que mes concours se finissent.

merci encore !

0
mamiemando Messages postés 33093 Date d'inscription jeudi 12 mai 2005 Statut Modérateur Dernière intervention 4 mai 2024 7 752 > PangolinRobuste42 Messages postés 8 Date d'inscription vendredi 29 mars 2024 Statut Membre Dernière intervention 5 avril 2024
6 avril 2024 à 14:23

Je profite de cette suite de commentaires pour remercier toutes les personnes qui apprécient les réponses que je prends le temps d'écrire, car c'est quelque chose qui me permet de garder le feu sacré :-) J'espère que mes recommandations serviront, mais ça, ce n'est plus entre mes mains ;-)

0
yg_be Messages postés 22777 Date d'inscription lundi 9 juin 2008 Statut Contributeur Dernière intervention 10 mai 2024 1 481
29 mars 2024 à 18:08

bonjour,

tu n'expliques pas comment appeller ton code.

0

Salut.

Avec un simple array.

board = [7, 4, 2, 9, 1, 3, 8, 5, 6]
taquin(board)

Pour ma part j'aurais choisi le 0 pour la case vide, si on met un 0 dans l'array, on se prend un IndexError, ce qui ne me semble pas trop logique, mais j'ai pas trop cherché à comprendre le code =)

Il faut déjà savoir que la moitié des combinaisons sont insolubles, voir wikipédia.

Rien ne semble testé dans le code de PangolinRobuste si le taquin est soluble.

Puis de toute façon, python ni même aucun langage de script n'est adapté pour résoudre ce genre de problème si on cherche la rapidité, le même algo en c, c++ sera bien plus rapide, y a pas mal d'exemples sur internet de la résolution des taquins dans divers langages et plusieurs algos.

0
yg_be Messages postés 22777 Date d'inscription lundi 9 juin 2008 Statut Contributeur Dernière intervention 10 mai 2024 1 481 > huitou
3 avril 2024 à 12:57

Cela peut probablement être rapide en Python si on utilise une librairie pour gérer et trier la liste, plutôt que de le coder en Python.

0
PangolinRobuste42 Messages postés 8 Date d'inscription vendredi 29 mars 2024 Statut Membre Dernière intervention 5 avril 2024 > yg_be Messages postés 22777 Date d'inscription lundi 9 juin 2008 Statut Contributeur Dernière intervention 10 mai 2024
5 avril 2024 à 16:26

malheureusement dans le cadre de mon épreuve (tipe en prepa) je ne peux pas trop diverger du python "basique", mais merci de ton commentaire, déjà je vais essayer de remplacer certaine fonction avec des librairies pour comparer et repérer là ou mon programme peut être grandement optimiser

0
PangolinRobuste42 Messages postés 8 Date d'inscription vendredi 29 mars 2024 Statut Membre Dernière intervention 5 avril 2024 > huitou
5 avril 2024 à 16:23

j'ai fais un autre programme me permettant de savoir si une configuration est soluble ou non

j'aurais du le préciser

0
PangolinRobuste42 Messages postés 8 Date d'inscription vendredi 29 mars 2024 Statut Membre Dernière intervention 5 avril 2024
5 avril 2024 à 16:42

je l'appel avec la fonction "taquin", si on parle de la même chose

0

Bonjour,

J'ai déjà fait un Taquin 3x3 avec Tkinter en utilisant les distances Manhattan

Ca fonctionne, mais évidement, pour que le problème soit soluble, il faut

partir de la configuration à atteindre, et déplacer les chiffres pour mélanger

avant de lancer la résolution

Prêt à mélanger:

Mélangé: on lance la résolution:

Résolu:

0
PangolinRobuste42 Messages postés 8 Date d'inscription vendredi 29 mars 2024 Statut Membre Dernière intervention 5 avril 2024
5 avril 2024 à 16:37

bonjour,

premièrement, pour les taquin non solubles, j'ai créé une autre fonction afin de savoir si un taquin est soluble ou non.

même si nos notation des cases dans le taquin diffèrent, (moi, je bouge ta case "8")

Est ce possible de jeter un coup d'œil à ton code si c'est du python?

merci dans tout les cas, bonne soirée

0
yg_be Messages postés 22777 Date d'inscription lundi 9 juin 2008 Statut Contributeur Dernière intervention 10 mai 2024 1 481
31 mars 2024 à 20:22

Tes appels incessants à la fonction ligne() sont totalement inutiles.  Il est préférable de créer un tableau, et d'y enregistrer les réponses possibles à cette fonction.

Cette fonction ligne() est très mal écrite, mais cela devient accessoire si tu l'appelles beaucoup moins.

0
PangolinRobuste42 Messages postés 8 Date d'inscription vendredi 29 mars 2024 Statut Membre Dernière intervention 5 avril 2024
5 avril 2024 à 16:41

faire un tableau avec toutes les configurations des taquins et les réponses, c'est ça? 

après je préférais faire un programme qui marche pour chaque taille de taquin..

j'ai fais une autre version avec des modulos et des restes, ça dois etre un bon compromis pour enlever le programme ligne().

merci

0
yg_be Messages postés 22777 Date d'inscription lundi 9 juin 2008 Statut Contributeur Dernière intervention 10 mai 2024 1 481 > PangolinRobuste42 Messages postés 8 Date d'inscription vendredi 29 mars 2024 Statut Membre Dernière intervention 5 avril 2024
5 avril 2024 à 17:32

La fonction ligne() donne toujours la même réponse pour chaque case, elle ne dépend pas du taquin.  Même avec des modulos et des restes, il me semble inutile de calculer perpétuellement dans quelle ligne se trouve chaque case.

0

Vous n’avez pas trouvé la réponse que vous recherchez ?

Posez votre question
yg_be Messages postés 22777 Date d'inscription lundi 9 juin 2008 Statut Contributeur Dernière intervention 10 mai 2024 1 481
5 avril 2024 à 18:47

La fonction de distance ne me semble pas être adéquate pour évaluer la proximité d'un taquin par rapport à la solution finale.

Je pense donc qu'il est inutile de la calculer, et, surtout, de trier en fonction de cela.

Si j'étais toi, je ne testerais surement pas des taquins de 16 cases.  Je regarderais comment le programme se comporte avec des taquins compliqués de 9 cases.  Par exemple avec un taquin "impossible" de 9. 

0