Algorithmes Classiques

Table of Contents

1. Numériques

1.1. Suite de Fibonacci

La suite de Fibonacci est définie par récurrence double :

\[ F_0 = 0 ,\, F_1 = 1 ,\, F_{n+2} = F_{n+1} + F_{n} \]

Entrées : un entier \(n \geq 0\)
Sortie : \(F_n\)

def fibonacci(n) :
    f = 0
    if n > 0 : # Si n = 0, il n'y a aucun calcul à faire
        # Rang 1
        f = 1
        f_prev = 0
        rang = 1
        # Calcul du rang n en calculant chaque rang i successivement
        while rang != n :
            f_suivant = f + f_prev
            f_prev = f
            f = f_suivant
            rang = rang + 1
    return f

1.2. Suite de Syracuse

La suite de Syracuse est définie par récurrence :

\[S_{{n+1}}={\begin{cases}{\dfrac {S_{n}}{2}}&{\mbox{si }}S_{n}{\mbox{ est pair,}}\\3S_{n}+1&{\mbox{si }}S_{n}{\mbox{ est impair.}}\end{cases}} \]

Entrées : un entier \(s \geq 1\)
Sortie : Le premier entier \(n \geq 0\) tel que \(S_n = 1\)

def syracuse(s0) :
    # Rang 0
    s = s0
    rang = 0
    # Calcul du rang suivant dans s, jusqu'à ce que s == 1
    while s != 1 :
        rang = rang + 1
        if s % 2 == 0 :
            s = s // 2
        else :
            s = 3 * s + 1
    return rang

2. Séquences

2.1. Somme des éléments d’une liste

Entrées : une liste d’entiers relatifs.
Sortie : la somme des entiers de la liste.

def somme_liste(l) :
    somme = 0
    for x in l  :
        somme = somme + x
    return somme

Complexité : \(\O(\symtt{len}(l))\).
Invariant : On note \(i\) l’indice de \(x\) dans \(l\). \[I : \symtt{somme} = \symtt{sum(L[:i])} = \sum_{k = 0}^{i-1} \symtt{L[k]} \]

2.2. Maximum

Entrées : une liste d’entiers L
Sortie : l’indice du maximum de la liste

def imax(l) :
    if len(l) == 0 :
        return -1
    i_max = 0
    maxi = l[0]
    for i in range(len(l)) :
        if l[i] > maxi :
            maxi = l[i]
            i_max = i
    return i_max

Complexité : \(\O(\symtt{len}(l))\).
Invariant : \(\symtt{maxi} = \symtt{max(L[:i])}\) et \(\symtt{L[i\_max]} = \symtt{maxi}\)

2.3. Recherche linéaire - version while

Entrées : Liste l et un élément x
Sortie : L’indice de la première occurrence de x dans l, ou -1 si x n’est pas dans l

def recherche_lineaire(l, x) :
    i_x = -1
    i = 0
    n = len(l)
    while i < n and i_x == -1 :
        if l[i] == x :
            i_x = i
        i = i + 1
    return i_x

Complexité : \(\O(\symtt{len}(l))\).
Invariant : En posant \(\min(\emptyset) = -1\), \[\symtt{i\_x} = \min\{ 0 \leq k < i \, | \, \symtt{L[k] = x}\}\]

2.4. Recherche linéaire - version for

Entrées : Liste l et un élément x
Sortie : L’indice de la première occurrence de x dans l, ou -1 si x n’est pas dans l

def recherche_lineaire(l, x) :
    for i in range(len(l)) :
        if l[i] == x :
            return i
    return -1

Complexité : \(\O(\symtt{len}(l))\).
Invariant : \(\symtt{x} \not\in \symtt{l[:i]}\)

2.5. Second Minimum

Entrées : Liste d’entiers positifs, disjoints, l de taille supérieure à 2
Sortie : Le second minimum de l

def deuxieme_minimum(l) :
    if l[0] < l[1] :
        m = l[0]
        second_m = l[1]
    else :
        m = l[1]
        second_m = l[0]
    for x in l[2:] :
        if x < m :
            second_m = m
            m = x
        elif x < second_m :
            second_m = x
    return second_m

Complexité : \(\O(\symtt{len}(l))\).
Invariant : On note \(i\) l’indice de \(x\). \[ \symtt{m} = \symtt{min(L[:i])} \text{ et } \symtt{second_m} = \symtt{min(L[:i]} \setminus \{ \symtt{m} \} ) \]

3. Double Boucles

3.1. Algorithme de la Fenêtre Glissante

Entrées : s, w deux chaînes de caractères non vides.
Sortie : i >= 0, le plus petit indice de s tel que w est une sous-chaîne de s à la position i.

def rechercher(s,w) :
    n = len(s)
    m = len(w)
    # Recherche linéaire (for) d'une fenetre qui contient w
    for i in range(0,n-m+1) :
        # Recherche linéaire (while) d'un caractère différent
        # dans la fenêtre.
        j = 0
        i_diff = -1
        while j < m :
            if w[j] != s[i+j] :
                i_diff = j
            j += 1
        # Si on n'a pas trouvé de caractère dans la fenêtre
        # c'est la bonne fenêtre.
        if i_diff == -1 :
            return i
    return -1

Complexité : \(\O(\symtt{len}(s)\,\symtt{len}(w))\).
Invariants : Ce sont les invariants des recherches linéaires.

3.2. Création d’une matrice \(n \times m\)

Entrées : n, m deux entiers positifs non nuls
Sortie : Une matrice de dimension \(n \times m\) remplie de None

def matrice(n,m) :
    M = []
    for i in range(n) :
        L = [ None ] * m
        M.append(L)
    return M

ou

def matrice(n,m) :
    return [ [ None ] * m for i in range(n) ]

Complexité : \(\O(nm)\).

3.3. Copie d’une matrice

Entrées : m une matrice \(p\times q\)
Sortie : Une copie de m

def copie(m) :
    m_copie = []
    for l in m : # pour chaque ligne de m
        l_copie = []
        for x in l : # pour chaque élément de la ligne
            l_copie.append(x)
        m_copie.append(l_copie)
    return m_copie

ou

def copie(M) :
    return [ [ x for x in L ] for L in M ]

Complexité : \(\O(pq)\).

3.4. Histogramme d’une image

Entrées : m une matrice \(p \times q\), d’entiers entre 0 et 255
Sortie : h, l’histogramme de m. i.e. une liste de taille 256 telle que h[i] soit le nombre d’occurrences de i dans m.

def histogramme(m) :
    h = [0]*256
    for l in m : # Pour chaque ligne de M
        for x in l : # Pour chaque élément de la ligne
            h[x] += 1
    return h

Complexité : \(\O(pq)\).

4. Algorithmes Glouton

4.1. Rendu de monnaie

On suppose que le système monétaire choisi permet toujours de rendre la monnaie.
Entrées : Un liste, L, de billets (entiers) dans l’ordre croissant et un montant, m, (entier).

Sortie : Une liste de billet de L dont la somme vaut m de taille minimale.

def rendu(L,m) :
    R = []
    reste = m
    i = len(L)-1 # i représente le plus gros billet éventuellement utilisable.
    while reste > 0 :
        if L[i] <= reste : # si on peux utiliser le plus gros billet, on l'utilise
            R.append(L[i])
            reste = reste - L[i]
        else : # sinon on considère le billet d'en dessous.
            i = i-1
    return R

Complexité : \(\O(\symtt{len(L) + reste}\)\).
Invariants : \(\symtt{reste + sum(R) = m}\).
Variant : \(\symtt{reste}+i\)

5. Algorithmes Dichotomiques

5.1. Exponentiation rapide

Entrées : \(a\) un entier et \(b\) un entier positifs.
Sortie : \(a^b\)

def exponentiation_rapide(a,b) :
    r = 1
    u = a
    v = b
    while v > 0 :
        if v % 2 == 1 :
            r = r * u
        u = u * u
        v = v // 2
    return r

Complexité : \(\O(\log_{2}(b))\).
Invariants : \(ru^{v} = a^{b}\).
Variant : \(\symtt{v}\)

5.2. Recherche dichotomique dans une liste triée

Entrées : Une liste d’entiers triés dans l’ordre croissant et un entier x.
Sortie : -1 si x n’est pas dans L ; l’indice de x dans L sinon.

def recherche_dichotomique(L,x) :
    debut = 0
    fin = len(L)
    while fin - debut > 0 :
        milieu = (debut + fin) // 2
        if L[milieu] == x :
            return milieu
        elif L[milieu] < x :
            debut = milieu + 1
        else :
            fin = milieu
    return -1

Complexité : \(\O(\log_{2}(\symtt{len}(L)))\).
Invariants : \(x \not\in \symtt{L[:debut]}\) et \(x \not\in \symtt{L[fin:]}\).
Variant : \(\symtt{fin-debut}\)

6. Algorithmes récursifs

6.1. Somme

Entrées : une liste d’entiers.
Sortie : la somme des éléments de la liste.

On défini \(\symtt{somme\_rec(L,i)} = \sum_{\symtt{k}=0}^{\symtt{i}-1} \symtt{L[k]}\) par la formule de récurrence suivante : \[ \symtt{somme\_rec(L,i)} = \begin{cases} 0 & \text{si } \symtt{i} = 0 \\ \symtt{somme\_rec(L,i-1)} + \symtt{L[i-1]} & sinon \end{cases} \]

def somme_rec(L, i) :
    """
    Entrées : L une liste d'entier, $0 \leq i \leq \symtt{len}(L)$.
    Sorties : \symtt{sum(L[:i])
    """
    if i == 0 :
        return 0
    s = somme_rec(L,i-1)
    return s + L[i-1]
 
def somme(L) :
    return somme_rec(L,len(L))

6.2. Maximum

Entrées : une liste d’entiers relatifs.
Sortie : le maximum de la liste

On défini \(\symtt{maximum\_rec(L,i)} = \symtt{max(L[:i]})\) par la formule de récurrence suivante : \[ \symtt{maximum\_rec(L,i)} = \begin{cases} -\infty & \text{si } \symtt{i} = 0 \\ max(\symtt{maximum\_rec(L,i-1)}, \symtt{L[i-1]}) & sinon \end{cases} \]

def maximum_rec(L, i) :
    """
    Entrées : L une liste d'entier, $0 \leq i \leq \symtt{len}(L)$.
    Sorties : \symtt{max(L[:i])
    """
    if i == 0 :
        return -float('inf')
    m = maximum_rec(L,i-1)
    if m > L[i-1] :
        return m
    return L[i-1]
 
def maximum(L) :
    return maximum_rec(L,len(L))

6.3. Exponentiation rapide

Entrées : \(a\) un entier et \(b\) un entiers positifs.
Sortie : \(a^b\)

\[ a^{b} = \begin{cases} 1 & \text{si } b = 0 \\ (a^{2})^{k} & \text{sinon si } b = 2\,k \\ a\,(a^{2})^{k} & \text{sinon, (i.e. } b = 2\,k+1) \end{cases}\]

def exp(a,b) :
    if b == 0 :
        return 1
    elif b % 2 == 0 :
        return exp(a * a , b // 2)
    else :
        return a * exp( a * a, b // 2)

6.4. Tri Fusion

Entrées : Une liste d’entiers
Sortie : Une copie de cette liste triée dans l’ordre croissant.

On suppose qu’il existe une fonction ayant la spécification suivante :

def fusion(l1 : list[int], l2 : list[int]) -> list[int] :
    """
    Entrées : deux listes d'entiers triés.
    Sortie: une liste triée contenant les éléments des deux listes en entrée.
    """

\[ \symtt{tri\_fusion(L)} = \begin{cases} \symtt{[]} & \text{si } \symtt{L = []} \\ \symtt{fusion(tri\_fusion(L[:n//2]),tri\_fusion(L[n//2:]))} & \text{sinon} \end{cases} \]

def tri_fusion(l) :
    n = len(l)
    if n < 2 :
        return l[:]
    l1 = tri_fusion(l[:n//2])
    l2 = tri_fusion(l[n//2:])
    return fusion(l1,l2)

Complexité : \(\O(n\,\log_2(n))\)

6.5. Binaire

Entrées : Un entier n.
Sortie : Une chaîne donnant la représentation binaire de n.

\[ \symtt{bin(n)} = \begin{cases} \text{""} & \text{si } \symtt{n = 0} \\ \symtt{bin(n//2)} + \text{'0'} & \text{si n est pair} \\ \symtt{bin(n//2)} + \text{'1'} & \text{si n est impair} \end{cases} \]

def bin(n) :
    if n == 0 :
        return ""
    bin_quot = bin(n//2)
    if n % 2 == 0 :
        return bin_quot + "0"
    return bin_quot + "1"

Complexité : \(\O(\log_{2}(\symtt{n}))\).

6.6. Binaire complément à deux sur n bits

Entrées : Deux entiers n et k tel que \(\symtt{n} \in \ints{-2^{\symtt{k}-1}}{2^{\symtt{k}-1}-1}\).
Sortie : Une liste donnant la représentation binaire de n en complément à 2 sur k bits.

def bin2(n, k) :
    if n == 0 :
        return "0"*k
    if n < 0 :
        return bin2(2**k + n, k)
    bin_quot = bin2(n//2)
    if n % 2 == 0 :
        return bin_quot + "0"
    return bin_quot + "1"

Complexité : \(\O(\log_{2}(\symtt{n}))\).

7. Dictionnaires

7.1. Nombre d’occurrences

Entrées : Une chaîne de caractère s.
Sortie : Le nombre d’occurrence de chaque caractère dans un dictionnaire.

def nb_occ(s) :
    d = {}
    for c in s :
        if c  in d :
            d[c] += 1
        else :
            d[c] = 1
    return d

Complexité : \(\O(\symtt{len(s)})\).
Invariants : On note \(i\) l’indice de \(c\) dans \(s\). d est toujours le nombre d’occurrence de chaque caractère de s[:i].

8. Algorithmique des graphes

8.1. Degrés dans un graphe non orienté

Entrées : la liste d’adjacence d’un graphe orienté à \(n\) sommets et \(m\) arêtes.
Sortie : la liste des degrés de chaque sommets.

def degre(g) :
    deg = [ len(g[s]) for s in range(len(g)) ]
    for s in range(len(g)) :
        for v in g[s] :
            deg[v] += 1
    return deg

Complexité : \(\O(n+m)\)

8.2. Parcours en largeur

Entrées : la liste d’adjacence d’un graphe à \(n\) sommets et \(m\) arêtes, source un sommet de g.
Sortie : ?? dépend du problème
Complexité : \(\O(n+m)\)

from collections import deque

# Utilisation d'une file : fifo pour avoir la bonne complexité.

def parcours_largeur(g, source) :
    n = len(g)
    marquage = [False] * n
    fifo = deque()
    fifo.append(source)
    marquage[source] = True
    # Traiter source ...
    while len(fifo) > 0 :
        s = fifo.popleft()
        for v in g[s] :
            if not marquage[v] :
                fifo.append(v)
                marquage[v] = True
                # Traiter v ...
    return # Quelque chose peut-être

8.3. Parcours en profondeur

Entrées : la liste d’adjacence d’un graphe à \(n\) sommets et \(m\) arêtes, source un sommet de g.
Sortie : ?? dépend du problème
Complexité : \(\O(n+m)\)

# Utilisation d'une pile : lifo pour avoir la bonne complexité.
def parcours_profondeur(g, source) :
    n = len(g)
    marquage = [False] * n
    lifo = []
    lifo.append(source)
    while len(lifo) > 0 :
        s = lifo.pop()
        if not marquage[s] :
            marquage[s] = True
            # Traiter s ...
            for v in g[s] :
                if not marquage[v] :
                    lifo.append(v)
    return # Quelque chose peut-être

La version récursive est meilleure.

def visiter(g, s, marquage) :
    marquage[s] = True
    # Traitement de source
    for v in g[s] :
        if marquage[v] != True :
            visiter(g, v, marquage)
            # ou ; r = visiter(g, v, marquage)
    return # Quelque chose peut-être
 
def parcours_profondeur(g, source) :
    n = len(g)
    marquage = [False] * n
    visiter(g,source,marquage)
    # ou ; r = visiter(g, source, marquage)
    return # Quelque chose peut-être

8.4. Dijkstra

Entrées : la liste d’adjacence d’un graphe pondéré orienté à \(n\) sommets et \(m\) arêtes, source un sommet du graphe.
Sortie : la liste des distances minimales de source à chaque autre sommet du graphe et des relations de parentés qui permettent d’attendre ces distances.

On suppose qu’on dispose d’une structure de donnée file de priorité qui supporte les opérations suivantes :

  • creer() : renvoie une file de priorité vide.
  • est_vide(file) : renvoie vrai si la file est vide.
  • ajoute(file, x, prio) : ajoute l’élément x dans la file avec priorité prio s’il n’y est pas déjà. Si x est déjà dans la file on change seulement sa priorité.
  • enleve_min(file) : enleve de la file et renvoie l’élément de priorité minimale.
def dijkstra(g,source) :
    n = len(g)
    # Structures
    distances = [float(inf)]*n
    parents = [None]*n
    file_prio = creer()
    # Initialisation
    distances[source] = 0
    ajoute(file_prio, source, 0)
    # Algorithme
    while not est_vide(file_prio) :
        # On récupère le sommet non visité à distance minimale
        s = enleve_min(file_prio)
        # On relache tous les voisins
        for voisin, poids in g[s] :
            distance_par_s = distances[s] + poids
            if distance_par_s < distances[voisins] :
                distances[voisins] = distance_par_s
                parents[voisin] = s
                ajoute(file_prio, voisin, distances[voisins])
    return distances, parents

On peut trouver une implémentation de la structure de file de priorité telle que les fonctions creer et est_vide soient en \(\O(1)\) et les fonctions enleve_min et ajoute soient en \(\O(\log(n))\). La complexité finale est alors en \(\O(n\log(n)+m)\).

Voici une autre implémentation utilisant un marquage par dictionnaire avec les outils du programme, et la fonction d.pop(key) qui permet d’enlever une clé d’un dictionnaire :

def creer() : # $\O(1)$
    return {}
 
def est_vide(file_prio) : # $\O(1)$
    return len(file_prio) == 0
 
def ajoute(file_prio, x, prio) : # $\O(1)$
    file_prio[x] = prio
 
def enleve_min(file_prio) : # $\O(\symtt{len(file_prio)})$
    min_key, min_prio = None, float('inf')
    for x, prio in file_prio.items() :
        if prio < min_prio :
            min_key, min_prio = x, prio
    file_prio.pop(min_key)
    return min_key

La complexité finale est alors en \(\O(n^2+m) = \O(n^{2})\) car \(m \leq n^{2}\).

8.5. \(A^*\)

Entrées :

  • la liste d’adjacence d’un graphe pondéré orienté à \(n\) sommets et \(m\) arêtes,
  • source un sommet du graphe,
  • destination un sommet du graphe,
  • heuristique une fonction qui prend en entrée g, source, destination et un sommet s de g et qui renvoie une valeur entière.

Sortie : une liste de distances de source à chaque autre sommet du graphe et des relations de parentés qui permettent d’attendre ces distances.
Si h ne surestime jamais la distance de s à destination, les distances sont les plus petites. Si h = 0, c’est l’algorithme de Dijkstra. Complexité : La même que Dijkstra si h est en \(\O(1)\). Mais en pratique on atteindra la destination plus rapidement.

def astar(g, heuristique, source, destination) :
    n = len(g)
    # Structures
    distances = [float(inf)]*n
    parents = [None]*n
    file_prio = creer()
    # Initialisation
    distances[source] = 0
    ajoute(file_prio, source, 0)
    # Algorithme
    while not est_vide(file_prio) :
        # On récupère le sommet non visité à distance minimale
        s = enleve_min(file_prio)
        # ***** Une différence est ici *****
        if s == destination :
            break
        # On relache tous les voisins
        for voisin, poids in g[s] :
            distance_par_s = distances[s] + poids
            if distance_par_s < distances[voisins] :
                distances[voisins] = distance_par_s
                parents[voisin] = s
                ajoute(file_prio, voisin,
                       # ***** Une autre différence est ici *****
                       distances[voisins]+h(g,source,destination,voisin))
    return distances, parents

9. Programmation Dynamique

9.1. Ordonnancement

Entrées : Une liste L d’entiers positifs de longueur n.
Sortie : La différence de charge minimale entre deux listes L1 et L2 se repartissant les éléments de L.
Complexité recherche exhaustive : \(\O(n\,2^{n})\).
Complexité programme dynamique : \(\O(n*\symtt{sum(L)})\).

Sous-problèmes : \(\symtt{ordo(L,i,\Delta)}\) correspond à la différence de charge minimale sachant que les éléments L[i:] sont déjà repartis avec une différence de \Delta. Pour \(0 \leq i \leq \symtt{len(L)}\) et \(0 \leq \Delta \leq \symtt{sum(L)}\).

Formule : \[ \symtt{ordo(L,i,}\Delta) = \begin{cases} \Delta & \text{si } i = 0 \\ \min(\symtt{ordo(L,i-1,}\Delta+\symtt{L[i-1]}), \symtt{ordo(L,i-1,}|\Delta-\symtt{L[i-1]}|) & \text{sinon} \end{cases} \] Le problème \(\symtt{ordo(L)}\) correspond au sous-problème \(\symtt{ordo(L,0,0)}\).

def ordo_memo(L,i,delta,memo) :
    if i == 0 :
        memo[(i,delta)] = delta
        return
    delta_1 = delta + L[i-1]
    delta_2 = abs(delta - L[i-1])
    if (i-1, delta_1) not in memo :
        ordo_memo(L,i-1,delta_1,memo)
    if (i-1, delta_2) not in memo :
        ordo_memo(L,i-1,delta_2,memo)
    memo[(i,delta)] = min(memo[(i-1,delta_1)], memo[(i-1,delta_2)])
    return
 
def ordo(L) :
    memo = {}
    ordo_memo(L,len(L),0,memo)
    return memo[(len(L),0)]

9.2. Floyd-Warshall

Entrées : Un graphe pondéré orienté g à n sommets et m arrêtes, représenté par sa matrice d’adjacence.
Sortie : La matrice des distances minimales entre chaque paire de sommets.
Complexité recherche exhaustive : \(\O(n\,n!)\).
Complexité programme dynamique : \(\O(n^{3})\).
Complexité Dijkstra entre chaque paire (s,d) : \(\O(n^{3}\log(n))\).

Sous-problèmes : \(\symtt{fw(g,k) = M^{k}}\) correspond aux distances minimales entre chaque paire de sommets. Sachant que les seuls sommets intermédiaires utilisés sont les sommets \(0 \leq s < k\). Formule : \[ M^{k+1}_{i,j} = \min(M^{k}_{i,j}, M^{k}_{i,k} + M^{k}_{k,j}) \] Le problème \(\symtt{fw(G)}\) correspond au sous-problème \(\symtt{fg(G,n)}\).

On peut utiliser une approche montante ici.

def fw(g) :
    n = len(g) :
    m_new = [ [ x for x in line ] for line in g ]
    for k in range(1,n+1) :
        m_prev = m_new
        m_new = [ [ float("inf") for x in line ] for line in g ]
        for i in range(n) :
            for j in range(n) :
                m_new[i][j] = min(m_prev[i][j], m_prev[i][k] + m_prev[k][j])
    return m_new

9.3. Levenstein

Entrées : Deux chaînes de caractère, s1 et s2 de longueur \(n_1\) et \(n_2\).
Sortie : Le nombre minimal d’insertion, suppression, substitution qu’il faut appliquer à s1 pour obtenir s2. Appelé distance d’édition.
Complexité recherche exhaustive : \(\O(3^{n_{1} + n_{2}})\).
Complexité programme dynamique : \(\O(n^{2})\).

Sous-problèmes : \(\symtt{lev(s1,s2,i,j)}\) correspond à la distance d’édition entre s[:i] et s[:j]. Pour \(0 \leq i \leq n_{1}\) et \(0 \leq j \leq n_{2}\).
Formule : \[ \symtt{lev(s1,s2,i,j)} = \begin{cases} j & \text{si } i = 0 \\ i & \text{sinon si } j = 0 \\ 1 + \min(symtt{lev(s1,s2,i-1,j)}, symtt{lev(s1,s2,i,j-1)}, symtt{lev(s1,s2,i-1,j-1)}) & \text{sinon si } \symtt{s1[i-1] != s2[j-1]} \\ 1 + symtt{lev(s1,s2,i-1,j-1)} & \text{sinon } \\ \end{cases} \]

Le problème \(\symtt{lev(s1,s2)}\) correspond au sous-problème \(\symtt{lev(s1,s2,}n_{1},n_{2})\).

def lev_memo(s1,s2,i,j,memo) :
    if i == 0 :
        memo[(i,j)] = j
        return
    elif j == 0 :
        memo[(i,j)] = i
        return
    elif s1[i-1] == s2[j-1] :
        if (i-1,j) not in memo :
            lev_memo(s1,s2,i-1,j,memo)
        if (i,j-1) not in memo :
            lev_memo(s1,s2,i,j-1,memo)
        if (i-1,j-1) not in memo :
            lev_memo(s1,s2,i-1,j-1,memo)
        memo[(i,j)] = 1 + min(memo[(i-1,j)], memo[(i,j-1)], memo[(i-1,j-1)])
        return
    else :
        if (i-1,j-1) not in memo :
            lev_memo(s1,s2,i-1,j-1,memo)
        memo[(i,j)] = 1 + memo[(i-1,j-1)]
        return
     
def lev(s1,s2) :
    memo = {}
    lev_memo(s1,s2,len(s1),len(s2),memo)
    return memo[(len(s1), len(s2))]

9.4. Held-Karp

Entrées : Un graphe pondéré orienté g = (S,A) à n sommets et m arrêtes.
Sortie : Le chemin le plus court permettant de passer par chaque sommet exactement une fois.
Complexité recherche exhaustive : \(\O(n!)\).
Complexité programme dynamique : \(\O(2^{n}\,n^{2})\).

Sous-problèmes : \(\symtt{hk(g,E,i,j)}\) correspond au poids du chemin le plus cours reliant i et j en passant exactement une fois par tous les sommets de E, ensemble de sommets de g tel que i et j ne sont pas dans E.
Formule : En notant \(d(u,v)\) le poids de l’arête \((u,v)\) (éventuellement \(+\infty\)). \[ \symtt{hk(g,E,i,j)} = \begin{cases} d(i,j) & \text{si } E = \emptyset \\ \min_{v \in E}(d(v,u) + \symtt{hk(g,E \setminus \{v\},v,j)}) & \text{sinon} \\ \end{cases} \]

Le problème \(\symtt{hk(g,i,j)}\) correspond au sous-problème \(\symtt{hk(g,S \setminus \{i,j\}, i,j)}\), avec S l’ensemble des sommets de g.

S doit être représenté par son indicatrice sous la forme de tuple pour pouvoir être mémoïsé. On représente ici g par sa matrice d’adjacence.

def hk_memo(g,E,i,j,memo) :
    vide = True
    for s_in_E in E :
        if s_in_E :
            vide = False
    if vide :
        memo[(E,i,j)] = g[i][j]
        return
    m = float("inf")
    for s in range(len(E)) :
        if E[s] :
            E_sans_s = E[:i] + (False,) + E[i+1:]
            if (E_sans_s,s,j) not in memo :
                hk_memo(g,E_sans_s,s,j,memo)
            val = g[i][s] + memo[(E_sans_s,s,j)]
            if val < m :
                m = val
    memo[(E,i,j)] = m
    return
 
def hk(g,i,j) :
    memo = {}
    S = (True,) * len(g)
    S = S[:i] + (False,) + S[i+1:]
    S = S[:j] + (False,) + S[j+1:]
    # ou S = tuple( [ x != i and x != j for x in range(len(g)) ] )
    hk_memo(g,S_sans_ij,i,j,memo)
    return memo[(len(s1), len(s2))]

10. Apprentissage

10.1. K-voisins

Entrées : Un ensemble de \(n\) données d’entraînements étiquetées, une distance entre les données, un entier \(k\), une donnée \(x\).
Sortie : L’étiquette majoritaire parmis les \(k\) plus proches voisins de \(x\).

L’ensemble des données d’entraînement est ici un dictionnaire dont la clé est la donnée et la valeur est l’étiquette.

def kvoisins(e,d,k,x) :
    pp = kplusproches(e,d,k,x)
    return majo(pp,e,d,k)

Il y a plusieurs algorithmes pour kplusproches. Parmis elles :

  • l’algorithme ci-après en \(\O(kn)\) si on suppose la complexité de \(d\) en \(\O(1)\).
  • trier la liste et récupérer les k premiers en \(\O(n\log(n))\)
  • Beaucoup plus compliqué : un variante du quickselect, linéaire en moyenne mais quadratique au pire cas. Qui peut devenir linéaire en le combinant avec une méthode de médiane des médianes.
def kplusproches(e,d,k,x) :
    L = []
    for data in e.keys() :
        if len(L) < k :
            L.append(data)
        elif d(data,x) < d(data,L[k-1]) :
            L.pop()
            L.append(data)
        j = k-1
        while j > 0 and L[j] < L[j-1] :
            L[j], L[j-1] = L[j-1], L[j]
            j += 1
    return L

Pour la fonction majo, cela dépend de l’heuristique utiliser pour décider de l’étiquette majoritaire. Dans ça version la plus simple :

def majo(pp,e,d,k) :
    etiquettes = {}
    for data in pp :
        if e[data] not in etiquettes :
            e[data] = 1
        else :
            e[data] += 1
    e_max = None
    m = -1
    for et in etiquettes :
        if etiquettes[et] > m :
            e_max = et
            m = etiquettes[et]
    return e_max

10.2. K-moyennes

Entrées : Un ensemble de \(n\) données d’entraînements, une distance entre les données, un entier \(k\), une fonction moyenne qui calcule la moyenne d’un ensemble de données.
Sortie : \(k\) catégories.

L’ensemble des données d’entraînement est ici une liste de données.

from random import sample
def kmoyenne(e,d,k,moyenne) :
    centroides = sample(e,k)
    old_centroides = None
    while old_centroides != centroides :
        categories = [ [] for i in range(k) ]
        for data in e :
            centroide_plus_proche = 0
            min_distance = d(data, centroides[0])
            for i in range(1,k) :
                distance = d(centroide[i], data)
                if distance < min_distance :
                    centroide_plus_proche = i
                    min_distance = distance
            categories[centroide_plus_proche].append(data)
            old_centroides = centroides
            centroides = [ moyenne(cat[i]) for i in range(k) ]
    return categories

Si les données sont des vecteurs, la distance et la moyennes peuvent être implémentées par :

def distance(x,y) :
    d = 0
    for i in range(len(x)) :
        d += (x[i]-y[i])**2
    return d ** 0.5
 
def moyenne(e) :
    m = [0.0] * len(e[0])
    for x in e :
        for i in range(len(x)) :
            m[i] += x[i]
    return [ x / len(m) for x in m ]

11. Théorie des jeux

11.1. Calcul des attracteurs

Entrées : Le graphe d’un jeux sous forme de liste d’adjacence (\(n\) sommets et \(m\) arêtes), les conditions de victoires d’un joueur sous la forme d’une liste de sommets du graphe, le joueur en question.
Sortie : L’ensemble des attracteurs (sommets gagnants) du joueur en question.
Complexité : \(\O(n + m)\)

Dans le graphe d’un jeux chaque sommet est contrôlé par un joueur. Il y a plusieurs manière de représenté cela. On supposera que chaque sommet est de la forme (j,s) avec j le numéro, 0 ou 1, du joueur contrôlant le sommet.

def transpose(g) :
    tg = [ [] for s in g ]
    for s in g :
        for v in g[s] :
            tg[v].append(s)
    return tg
 
def attracteurs(g,v,joueur) :
    tg = transpose(g)
    marquage = [ len(tg[s]) for s in tg ]
    a = [ False for i in range(len(tg)) ]
    for s in v :
        parcours(tg,s,a,marquage, joueur)
    return a
 
def parcours_dfs(g,s,a,marquage, joueur) :
    if a[s] :
        return
    a[s] = True
    for v in g[s] :
        if not a[v] :
            marquage[v] -= 1
            j,_ = v
            if j = joueur or marquave[v] == 0:
                parcours_dfs(v)

11.2. Algorithme Min-Max

Entrées : Un arbre de jeux à profondeur fixé, donné par sa liste d’adjacence, r la racine de l’arbre, une heuristique \(h\) permettant d’estimer le score des feuilles, le joueur (0 pour max, 1 pour min) qui joue à la racine.
Sortie : Le score du joueur à la racine de l’arbre, et le coup qui permet de l’attendre.

def minmax(a, r, h, j) :
    if len(a[r]) == 0 :
        return h(r), None
    L = []
    for v in a[r] :
        L.append((minmax(a,v,h,1-j), v))
    if i == 0 :
        return max(L)
    return min(L)

Author: Samy Jaziri

Created: 2025-02-14 ven. 16:55