Homework on Dijkstra

What does (1, 7) mean?


g=[[(1,7),(2,13)],[(3,5),(5,2)],[(0,13),(4,6)],[(0,11)],[(5,5)],[(2,8)],[]]
#On utilise des graphes implémenté sous forme de liste d'adjacence
#Le premier nombre d'un couple est le sommet vers lequel l'arête pointe et le second est le poid de celle-ci

What is the complexity of this update?

def update(self, c):
        """Méthode de mise à jour
           Pour c = (a, b), remplace la première occurrence de a rencontrée lors d'un parcours en largeur du tas par b.

        Args:
            c (couple d'élément du tas): Le premier élément est l'élément à remplacer. Le deuxième élément est l'élément qui le remplace
        """
        a, b = c
        assert(a in self), "L'élément qu'on cherche à remplacer n'existe pas dans le tas."
        indice = self.data.index(a)
        self.data[indice] = b
        if b > a:
            while self.key(self.data[leftChild(indice)], self.data[indice]) or self.data[rightChild(indice)].key(self.data[indice]):
                echange = leftChild(indice) if self.key(self.data[leftChild(indice)], self.data(rightChild)) else rightChild(indice)
                self.__swap(indice, echange)
                indice = echange
        else:
            while self.key(self.data[indice], self.data[parent(indice)]):
                self.__swap(indice, parent(indice))
                indice = parent(indice)

What do you think about the variable path?

    def path(self, start, end, exclude_nodes=None):
        exclude_nodes = exclude_nodes or []
        if (start, end) in self.memoized_paths:
            return self.memoized_paths[(start, end)]

        # Initialisation de la file de priorité
        heap = HeapQueue(key=lambda x: x[0])  # Trie par coût
        heap.push((0, start, []))  # (coût, nœud courant, chemin)

        visited = set()

        while heap.heap:
            cost, current, path = heap.pop()

            if current in visited or current in exclude_nodes:
                continue
            visited.add(current)

            # Mise à jour du chemin
            path = path + [current]

            if current == end:
                self.memoized_paths[(start, end)] = path
                return path

            # Exploration des voisins (chaque connexion a un poids de 1)
            for neighbor in self.graph.get(current, []):
                if neighbor not in visited and neighbor not in exclude_nodes:
                    heap.push((cost + 1, neighbor, path))

        return None  # Pas de chemin trouvé

What about the complexity of this function?

def update(self, x: T, prev_key_x: float) -> None:
        """Updates an element's priority (aka `key`).
        This should be called AFTER updating the `key` function.

        Args:
            x (T): The element
            prev_key_x (float): The previous key for the element
        """
        assert x in self.items
        i = self.items.index(x)

        key_x = self.key(x)

        if key_x == prev_key_x: return

        if key_x < prev_key_x: self._sift_down(i)
        else: self._sift_up(i)

Do you like integers? What is a and b?

class Graph():
    """Une class qui represente un graph orienté"""
    def __init__(self,n):
        self.mat=[[-1 for i in range(n)] for i in range(n)]

    def len(self):
        """Renvoie le nombre de sommet du graph"""
        return len(self.mat)

    ...
def disjtra(graph:Graph,a,b):
    """Disjtra sur graph avec a,b: Renvoie la distance min entre a,b et le chemin qu'il faut prendre"""
    assert(graph.len()>a)
    assert(graph.len()>b)
    queue=HeapQueue()
    l=[-1 for i in range(graph.len())]
    for enfant in graph.enfant(a):
        queue.diminuer_prio(enfant,l,a)
    if queue.vide() :
        return -1,[]
    i,d=queue.pop()
    while i!=b :
        for enfant in graph.enfant_avec_offset(i,d) :
            queue.diminuer_prio(enfant,l,i)
        if queue.vide() :
            return -1,[]
        i,d=queue.pop()
    return d,l

Clever!

    @abstractmethod
    def update(self, k: K, p: float):
        """
        Modifies the priority of the given key.
        Behaves the same as removing and adding back and should be more efficient.
        """
        self.remove(k)
        self.add(k, p)

I prefer a good old-style concrete implementation!

from PriorityQueue2 import PriorityQueue
from typing import List, Dict, Callable

def reconstruct_path(origine: Dict[int, int], depart: int, arrivee: int) -> List[int]:
    """
    Reconstruit le chemin du départ à l'objectif.

    origine: Un dictionnaire représentant le chemin inverse.
    depart: Le noeud de départ.
    arrivee: Le noeud d'arrivée.
    Renvoie une liste représentant le chemin du départ à l'arrivée.
    """
    chemin:List[int] = []
    noeud_actuel:int = arrivee # On commence par l'arrivée et on va remonter jusqu'au début
    while noeud_actuel != depart: # Tant qu'on n'est pas arrivé au début du chemin
        chemin.append(noeud_actuel)
        noeud_actuel = origine[noeud_actuel] # On remonte dans le dictionnaire
    chemin.append(depart)
    return chemin[::-1] # On inverse finalement la liste pour la remettre dans l'ordre

def a_star(G: List[List[float]], depart: int, arrivee: int, heuristic: Callable[[int, int], float]) -> tuple[Dict[int, float], List[int]]:
    """
    Implémentation de l'algorithme A* pour un graphe représenté par une matrice d'adjacence.

    G: La matrice d'adjacence du graphe, où G[i][j] représente le poids de l'arête
                   entre les noeuds i et j et vaut infini si aucune arête n'existe.
    depart: Le noeud de départ.
    arrivee: Le noeud d'arrivée.
    heuristic: Une fonction heuristique prenant deux noeuds et retournant une estimation
                      du coût pour aller du premier au second.
    Renvoie une paire contenant :
             - Un dictionnaire avec la distance minimale de chaque noeud depuis le départ.
             - Une liste représentant le chemin le plus court trouvé.
    """
    # Nombre total de noeuds dans le graphe
    n:int = len(G)

    # Initialisation des distances minimales connues (g) et des estimations totales (f)
    g:Dict[int, float] = {i: float('inf') for i in range(n)} # Initialisation de toutes les distances à inf
    g[depart] = 0
    f:Dict[int, float] = {i: float('inf') for i in range(n)} # Initialisation de toutes les distances à inf
    f[depart] = heuristic(depart, arrivee)

    # File de priorité pour explorer les noeuds avec les plus faibles coûts estimés
    pq = PriorityQueue()
    pq.push(depart, f[depart])

    # Dictionnaire pour suivre les origines des noeuds (reconstruction de chemin)
    origine:Dict[int,int] = {}

    # Boucle principale : exploration des noeuds jusqu'à épuisement ou atteinte de l'arrivée
    while not pq.is_empty():
        # Récupération du noeud avec le plus faible coût estimé
        noeud_actuel = pq.pop()

        if noeud_actuel == arrivee: # Si le noeud actuel est le noeud d'arrivée, le chemin optimal est trouvé
            return g, reconstruct_path(origine, depart, arrivee)

        # Exploration des voisins du noeud actuel
        for voisin in range(n):
            if G[noeud_actuel][voisin] != float('inf'):# S'il existe une arrête
                poids = G[noeud_actuel][voisin]
                g_score = g[noeud_actuel] + poids # Calcul du coût réel pour atteindre ce voisin

                if g_score < g[voisin]: # Si un chemin plus court vers ce voisin est trouvé
                    origine[voisin] = noeud_actuel # Mise à jour de l'origine du voisin
                    g[voisin] = g_score # Mise à jour du coût réel
                    f[voisin] = g[voisin] + heuristic(voisin, arrivee) # Mise à jour du coût estimé
                    pq.push(voisin, f[voisin]) # Ajout du voisin à la file de priorité

    # Si aucun chemin n'est trouvé jusqu'à l'arrivée
    return g, []

How to make the update?

# Implémentation avec un tas binaire
class BinaryHeapPriorityQ(PriorityQueue):
    def __init__(self):
        self.heap = []

    def push(self, item, priority):
        heapq.heappush(self.heap, (priority, item))

    def pop(self):
        return heapq.heappop(self.heap)[1]

    def is_empty(self):
        return len(self.heap) == 0

Concrete implementation

# Fonction Dijkstra utilisant la file de priorité
def dijkstra(graph : dict, start : int):
    """
    graph: Dictionnaire représentant un graphe pondéré (liste d'adjacence').
                  Par exemple: {0: [(1, 2), (2, 4)], 1: [(2, 1), (3, 7)], 2: [(3, 3)], 3: []}

                  Les couples sont de la forme : (voisin,poids de l'arrête vers ce voisin)
    start: Sommet de départ.

    Retourne un dictionnaire des distances minimales depuis le sommet de départ.
    """

    # Initialisation
    pq = BinaryHeapPriorityQ() # Notre file de priorité pour le traitement
    distances = {node: float('inf') for node in graph} # TOut le monde est à distance infinie au début
    distances[start] = 0
    pq.push(start, 0)

    while not pq.is_empty():
        current_node = pq.pop()

        # Exploration des voisins
        for neighbor, weight in graph[current_node]:
            new_distance = distances[current_node] + weight
            if new_distance < distances[neighbor]:
                distances[neighbor] = new_distance
                pq.push(neighbor, new_distance)

    return distances

Never comment the code

class HeapQueue(PriorityQueue):
    def __init__(self):
        self.h = []
        self.pos = {}
    def swap(self,i:int,j:int):
        self.h[i],self.h[j] = self.h[j],self.h[i]
        self.pos[self.h[i]],self.pos[self.h[j]]=i,j
    def push(self,a):
        (self.h).append(a)
        elt = a
        n = len(self.h)-1
        while n > 0 and self.h[n] < self.h[parent(n)]:
            self.swap(n,parent(n))
            n = parent(n)
    def is_empty(self):
        return (len(self.h)==0)
    def pop(self):
        assert (not self.is_empty())
        min = self.h[0]
        n = len(self.h)-1
        if n == 0:
            self.h = []
            return min
        self.h[0] = self.h[n]
        self.h.pop()
        i = 0
        while (leftChild(i)<n and self.h[i] > self.h[leftChild(i)]) or(rightChild(i)<n and self.h[i] > self.h[rightChild(i)]):
            if rightChild(i) < n and self.h[leftChild(i)] > self.h[rightChild(i)]:
                self.swap(i,rightChild(i))
                i = rightChild(i)
            else:
                self.swap(i,leftChild(i))
                i = leftChild(i)
        return min
    def update(self,a1,a2):
        if a1 in self.pos.keys():
            i = self.pos[a1]
            del self.pos[a1]
            self.h[i] = a2
            self.pos[a2] = i
            n = len(self.h)
            while (leftChild(i)<n and self.h[i] > self.h[leftChild(i)]) or(rightChild(i)<n and self.h[i] > self.h[rightChild(i)]):
                if rightChild(i) < n and self.h[leftChild(i)] > self.h[rightChild(i)]:
                    self.swap(i,rightChild(i))
                    i = rightChild(i)
                else:
                    self.swap(i,leftChild(i))
                    i = leftChild(i)
            while i > 0 and self.h[i] < self.h[parent(i)]:
                self.swap(i,parent(i))
                i = parent(i)

Concrete implementation

'''
    applique l'algorithme de dijkstra au graphe G représenté par sa matrice de poids M

    M: matrice des poids des éléments de G, M[i][j] est le poids de l'arête (i,j)
    a: sommet de départ
    b: sommet d'arrivée
'''
#je pense que ce programme devrait marcher, mais le problème vient de remove
def dijkstra(M, a, b):
    n=len(M)
    Q=HeapQueue() #sommets non explorés sous forme de tas binaire
    Q.elems=[k for k in range(n)]
    Q.swap(0,a) #on commence par le sommet a
    Q.key=[math.inf for k in range(n)] #liste des distances
    Q.key[a]=0
    while Q.elems!=[]: #tous les sommets ne sont pas encore explorés
        i=Q.elems[0] #sommet de poids le plus faible
        for j in range(n):
            update_distances(i,j,M,Q.key)
        Q.remove(0) #on a exploré ce sommet
    return(Q.key[b]) #distance de a à b


Do not create magic functions

    def __swap__(self,i : int,j : int) -> None:
        self.tas[i],self.tas[j] = self.tas[j],self.tas[i]

Let us depend on the implementation

g2 = WeightedGraph(10, directed=True)
    g2.adj_matrix = [
        [0, 4, 0, 0, 0, 0, 0, 0, 8, 5],
        [0, 0, 5, 0, 0, 0, 0, 0, 6, 0],
        [0, 0, 0, 6, 0, 0, 0, 0, 4, 0],
        [0, 0, 0, 0, 4, 0, 0, 5, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 3, 0, 0],
        [0, 0, 0, 0, 2, 0, 3, 3, 0, 0],
        [0, 0, 0, 0, 0, 2, 0, 0, 5, 4],
        [0, 0, 0, 4, 0, 0, 0, 0, 0, 0],
        [0, 0, 4, 0, 0, 0, 5, 0, 0, 3],
        [0, 0, 0, 0, 0, 0, 3, 0, 0, 0],
    ]

Let us be very concrete

`

def djikstra(s :int, l):
    """given a start s
    and a weighted graph in the form of an adjacency list,

    returns the list of the lower distance to s, with
    the path as the form of a predecessor list

    Args:
        s (int): s < len(l), a node of the graph G
        l ( list[ list[(int, float)] ]): adjacency list of G

    Returns:
        list[float], list[int]: lower distance to a, predecessor list
    """
    file_prio = tasFile()
    d = [float('inf')] * len(l)
    pred = [-1] * len(l)

    file_prio.empile(0,s)
    while not file_prio.estVide() :
        dist,v = file_prio.depile()
        d[v] = dist
        print(dist,v)
        for (u, p) in l[v]:
            if d[u] > d[v] + p :
                d[u] = p + d[v]
                pred[u] =
                # Given tasFile.actualise is in O(n), this is clearly suboptimal.
                # We could note use the actualise method, but admettons que j'ai
                # implémenté un dictionnaire pour que ce soit en O(logn)
                # On pourrait ne faire qu'empiler, quitte à empiler plusieurs fois le même état, mais je suis fatigué
                file_prio.actualise(d[u], u)

    return d,pred

Let us have multiple responsabilities

@dataclass
class Element:
    val : int #valeur de l'élément
    prio : int #priorité de l'élément
    precedent : int #sommet d'où l'on vient, pour reconstruire le chemin
#Ni Dijkstra ni A_star n'utilise update
    def update(self, x : Element):
        index = self.find_index(x)
        if not index: #on ne percole que si x est dans self.A
            raise IndexError("udpate sur un élément inexistant")
        self.A[index] = x
        self.percole_bas(index)
        self.percole_haut(index)
#renvoie le couple (chemin, distances)
def dijkstra(source : int, destination : int, gphe : Graphe) -> (Tuple, List[int]):
    Pfile = HeapQueue(comp_fun = lambda x,y : x.prio - y.prio, key = lambda x : x.val)
    vus = [False for i in range(gphe.nb_sommets)]
    distances = [float('inf') for i in range(gphe.nb_sommets)]
    distances[source] = 0
    came_from = [None for i in range(gphe.nb_sommets)]
    chemin = ()

    Pfile.push(Element(source, 0, -1))
    while Pfile.A:
        x = Pfile.pop()
        if not vus[x.val]:
            vus[x.val] = True
            distances[x.val] = x.prio
            came_from[x.val] = x.precedent
            if x.val == destination:
                break
            voisins = gphe.voisins(x.val)
            for (voisin,poids) in voisins:
                if not vus[voisin]:
                    Pfile.push(Element(voisin,poids + x.prio, x.val)) #il peut y avoir plusieurs fois la même valeur dans le tas

    if distances[destination] != float('inf'):
        chemin = reconstruct_chemin(came_from, source, destination)

    return (chemin, distances)

Very funny

def path_length[V](g: Graph[V], path: list[V]) -> float:
    """Compute the length of `path` in `g`
    This code makes me hate Python and imperative programming in general
    """
    l = 0
    for i in range(len(path) - 1):
        v = path[i]
        n = g.outgoing_neighbors(v)
        for u in filter(lambda u: u[0] == path[i + 1], n):
            l += u[1]
            break
    return l

Let us do not use object-programming features

graphe = {
    "A": [("B", 4), ("C", 2)],
    "B": [("A", 4), ("C", 5), ("D", 10)],
    "C": [("A", 2), ("B", 5), ("D", 3)],
    "D": [("B", 10), ("C", 3)]
}

distances, predecesseurs = dijkstra(graphe, "A")
print("Distances :", distances)
print("Prédécesseurs :", predecesseurs)

Let us do not use standard docstrings!

class PriorityQueue: #classe de file de priorité basique
    def __init__(self):
        self.elt=[]
        self.nb_elt=0


    def order(self,x,y)->bool: #l'ordre sur les éléments de la file
        return x<y


    def head(self):#fonction pour consulter la tête de file
        try:
            return self.elt[-1]

        except:
            print("La file est vide")


    def est_vide(self)->bool:#fonction pour tester si la file est vide
        return self.elt==[]


    def push(self,x)->None:#ajout d'un élément
        self.nb_elt+=1
        i=len(self.__elt)
        self.elt.append(x)
        while i>0 and not self.order(self.__elt[i],self.__elt[i-1]):
            self.elt[i],self.elt[i-1]=self.elt[i-1],self.elt[i]
            i-=1


    def pull(self):#suppression d'un élément
        self.nb_elt-=1
        return self.__elt.pop()

Projects

def copy(self):
        """
        Creates a deepcopy of the grid with the same objects but a different position in memory.
        """
        new_grid = [[set() for j in range (self.w)] for _ in range (self.h)]

        for i,j in self.grid_indices():
                for elt in self.get(i,j):
                    name = elt.name

                    # Copy the object with the same properties
                    new_elt = None
                    if type(elt) == Actionnable:
                        new_elt = Actionnable(elt.design, elt.position)
                    elif type(elt) == TextActionnable:
                        new_elt = TextActionnable(elt.design, elt.position)
                    elif type(elt) == TextAction:
                        new_elt = TextAction(elt.design, elt.position)
                    elif type(elt) == TextLink:
                        new_elt = TextLink(elt.design, elt.position)
                    new_grid[i][j].add(new_elt)

        return Gamegrid(new_grid)
if event.type == pygame.MOUSEBUTTONDOWN:
                    # If the cursor is in the 'rules' tile
                    if (self.__is_in_rectangle__(self.center_x_rules, self.center_y_rules)) :
                        rules.start_rules()
                    # If the cursor is in the 'lvl1' tile
                    elif (self.__is_in_rectangle__(self.center_x_lv1, self.center_y_lv1)) :
                        levels.Levels("level1").run()
                    # If the cursor is in the 'lvl2' tile
                    elif (self.__is_in_rectangle__(self.center_x_lv2, self.center_y_lv2)) :
                        levels.Levels("level2").run()
                    # If the cursor is in the 'lvl3' tile
                    elif (self.__is_in_rectangle__(self.center_x_lv3, self.center_y_lv3)) :
                        levels.Levels("level3").run()
                    # If the cursor is in the 'lvl4' tile
                    elif (self.__is_in_rectangle__(self.center_x_lv4, self.center_y_lv4)) :
                        levels.Levels("level4").run()
                    # If the cursor is in the 'lvl5' tile
                    elif (self.__is_in_rectangle__(self.center_x_lv5, self.center_y_lv5)) :
                        levels.Levels("level5").run()

if not has_bottom_water and not has_top_water and not has_left_water and not has_right_water:
    text = "h_d_b_g_"
    rotate = 0

elif not has_bottom_water and not has_top_water:
    if has_left_water and not has_right_water:
        text = "one_side_"
        rotate = 0
    elif not has_left_water and has_right_water:
        text = "one_side_"
        rotate = 180
    elif has_left_water and has_right_water:
        text = "two_side_op_"
        rotate = 0

elif not has_top_water and has_bottom_water:
    if not has_left_water and not has_right_water:
        text = "one_side_"
        rotate = 90
    elif has_left_water and not has_right_water:
        if has_bottom_left_water:
            text = "two_side_"
        else:
            text = "two_side_corner_"
        rotate = 180
    elif not has_left_water and has_right_water:
        if has_bottom_right_water:
            text = "two_side_"
        else:
            text = "two_side_corner_"
        rotate = 270
    elif has_left_water and has_right_water:
        text = "two_side_op_"
        rotate = 90

elif has_top_water and not has_bottom_water:
    if not has_left_water and not has_right_water:
        text = "one_side_"
        rotate = 270
    elif has_left_water and not has_right_water:
        if has_top_left_water:
            text = "two_side_"
        else:
            text = "two_side_corner_"
        rotate = 90
    elif not has_left_water and has_right_water:
        if has_top_right_water:
            text = "two_side_"
        else:
            text = "two_side_corner_"
        rotate = 0
    elif has_left_water and has_right_water:
        text = "one_side_"
        rotate = 90

elif has_bottom_water and has_top_water and has_left_water and has_right_water:
    text = "one_side_"
    rotate = 90
else:
    print("Erreur, dans la gestion de l'eau: ", i, j)



def search(self, init_state, ite=1000):
    """@brief Runs the MCTS algorithm from the initial state.
    @param init_state The initial state of the game.
    @param ite The number of iterations to perform (default is 1000).
    @return The state corresponding to the best move found.
    """
    root = MCTSNode(state=init_state)
    for _ in range(ite):
        node = root
        # Selection
        while not node.u_actions and node.childs:
            node = node.best_child(self.exp)
        # Expansion
        if node.u_actions:
            action = random.choice(node.u_actions)
            new_state = node.state.do_action(action)
            node = node.add_child(action, new_state)
        # Simulation
        result = self.simulate(node.state)
        # Backpropagation
        while node is not None:
            node.update(result)
            node = node.parent
    return root.best_child(0).state

def check_horizontal(data):
    """Vérifie si il existe un vainqueur diagonal, renvoie son numéro dans ce cas, et renvoie 0 sinon"""
    counter_1 = 0
    counter_2 = 0
    for i in range(n):
        counter_1 = 0
        counter_2 = 0
        for j in range(n):
            if(data[i][j] == 1):
                counter_1 += 1
                counter_2 = 0
                if(counter_1 == 4):
                    return 1
                if(counter_2 == 4):
                    return 2
            elif(data[i][j] == 2):
                counter_1 = 0
                counter_2 += 1
                if(counter_1 == 4):
                    return 1
                if(counter_2 == 4):
                    return 2
            else:
                counter_1 = 0
                counter_2 = 0
    return 0