Échauffement

Table of Contents

1. Algorithmique sur une structure séquentielle

1.1. Recherche linéaire

def recherche_lineaire(l, m) :
    """
    Entrées : l une liste de chaînes de caractère, m une chaîne de caractère
    Sortie : indice de m dans l si m est dans l, -1 sinon
    """
    for i in range(n-1,-1,-1) :
        if m == l[i] :
            return i
    return -1

1.2. Calcul de la somme maximale :

def somme_max(l) :
    """
    Entrées : l une liste d'entiers
    Sortie : max(e1 + e2, (e1,e2) dans l), 0 si l est vide
    """
    if not l :
        return 0
    m = l[0] * 2
    for e1 in l :
        for e2 in l :
            if e1 + e2 > m :
                m = e1 + e2
    return m
def somme_max_lineaire(l) :
    """
    Entrées : l une liste d'entiers
    Sortie : max(e1 + e2, (e1,e2) dans l), 0 si l est vide.
    On calcule le maximum et le second maximum de la liste, puis on les somme.
    """
    if not l :
        return 0
    if len(l) == 1 :
        return l[0] * 2
    # Initialisation de im0 et im1 à l'indice du maximum et second maximum
    # des 2 premiers élèments, respsectivement.
    im0 = 0
    im1 = 1
    if l[0] < l[1] :
        im0 = 1
        im1 = 0
    for i in range(2,len(l)) :
        if l[i] < l[im0] :
            im1 = im0
            im0 = i
        elif l[i] < l[im1] :
            im1 = i
    return l[im0] + l[im1]

1.3. Aplatissement

def aplatissement(l) :
    """
    Entrées : Une liste de listes d'entiers
    Sortie : La concaténation de l'ensemble des listes de l
    """
    s = []
    for x in l :
        s.extend(x)
    return s

L’extension d’une liste \(l_1\) par une liste \(l_2\) est en \(\mathrm O(|l_2|)\) où \(|l_2|\) est la longueur de la liste \(l_2\).

Donc aplatissement est en \(\mathrm O(\sum_{x \in l} |x|)\), proportionnel donc au nombre d’éléments total contenu dans la liste de liste.

1.4. Aplatissement de dictionnaires

def aplatissement_dict(l) :
    """
    Entrées : Une liste de distionnaires str -> listes d'entiers
    Sortie : D obtenu en aplatissant la liste...
    """
    d = {}
    for e in l :
        for k in e :
            if k not in d :
                d[k] = []
            d[k].extend(e[k])
    return d

1.5. Recherche dichotomique

def recherche_dichotomique(l,x) :
    """
    Entrées :
    - l : liste de chaînes de caractères triée par ordre alphabétique,
    - x : une chaîne de caractère
    Sortie : l'indice de x dans l si x est dans l, -1 sinon
    """
    d, f = 0, len(l)
    while d < f :
        m = ( d + f ) // 2
        if l[m] == x :
            return m
        elif l[m] < x :
            d = m+1
        else : # l[m] > x
            f = m
    return -1

Complexité : Sur une entrée \(l\) de longueur \(n\), le pire cas est atteint lorsque \(x \not\in l\). Auquel cas la boucle while tourne tant que d < f. On montre par récurrence qu’au tour de boucle \(i\), \(|d - f| \leq \frac n {2^i}\).

  • C’est vrai au tour de boucle \(0\) car \(|d-f| = n\).
  • Supposons la propriété vrai au tour de boucle \(i\). Après l’exécution de la boucle, il y a deux possibilités puisque \(x \not\in l\) :

    • \(| d - f | \gets | d - \lfloor \frac {d + f} 2 \rfloor | \leq | \frac {f - d} 2 | \leq \frac n {2^{i+1}}\)
    • \(| d - f | \gets | \lfloor \frac {d + f} 2 \rfloor - f | \leq | \frac {d - f} 2 | \leq \frac n {2^{i+1}}\)

    Dans les deux cas, la propriété est vérifiée au rang \(i+1\)

  • La propriété est donc vrai, et donc pour \(i = \lceil \log_2(n) \rceil\) on a \(|d - f| = 0\) (ce sont des entiers). La boucle tourne donc au plus \(\lceil \log_2(n) \rceil\) fois.

    La complexité finale est donc en \(\mathrm O(\log_2(n))\).

Correction :

On rappelle :

  • Pré-conditions : \(l\) est triée par ordre lexicographique croissant
  • Post-conditions : l’algorithme renvoie un entier \(i\) tel que :
    1. \(i = -1\) et \(x \not\in l\), ou,
    2. \(0 \leq i < |l|\) et \(l[i] = x\).

On montre l’invariant suivant pour la boucle while :

\[ 0 \leq d \leq f \leq |l| \text{ et } \begin{cases} \forall 0 \leq i < d, & l[i] < x \\ \forall f \leq i < |l|, & l[i] > x \end{cases} \]

  • C’est vrai avant d’entrer dans la boucle car \(d = 0\) et \(f = |l|\).
  • Si la propriété est vrai à l’entrée de la boucle et que la sortie de la boucle est atteinte, alors \(l[m] \neq x\).
    • Si \(l[m] < x\) alors

      1. comme \(d \leq m < f\), \(0 \leq m + 1 \leq f \leq |l|\).
      2. comme \(l\) est triée, \(\forall 0 \leq i < m + 1, l[i] < x\).

      Donc à la sortie de la boucle, comme \(d \gets m + 1\) on retrouve l’invariant.

    • Si \(l[m] > x\) alors

      1. comme \(d \leq m < f\), \(0 \leq d \leq m \leq |l|\).
      2. comme \(l\) est triée, \(\forall m \leq i < |l|, l[i] > x\).

      Donc à la sortie de la boucle, comme \(f \gets m\) on retrouve l’invariant.

L’invariant est donc vrai quelque soit le tour de boucle.

Il y a deux sortie possible de l’algorithme :

  • Lors d’un tour de boucle : il s’agit du cas où \(l[m] = x\), l’algorithme renvoi alors \(m = \lfloor \frac {d+f} 2 \rfloor\).

    1. \(0 \leq d \leq f \leq |l|\) d’après l’invariant de boucle et comme \(d \leq m < f\) on a bien \(0 \leq m < |l|\).
    2. Par hypothèse \(l[m] = x\)

    La post-condition 2 est vérifiée.

  • A la fin de la boucle : l’algorithme renvoie \(-1\). La condition de fin de boucle est fausse donc \(d \geq f\). L’invariant de boucle est vérifié donc \(\forall 0 \leq i < d, l[i] < x\) et \(\forall f \leq i < |l|, l[i] > x\). On conclut de ces deux propositions que \(\forall 0 \leq i < |l|, l[i] \neq x\), i.e \(x \not\in l\).

    La post-condition 1 est vérifiée.

1.6. Montagne

On va utiliser une recherche dichotomique dans la montagne pour trouver l’indice du pic. Cela est possible car, en regardant un élément et ses deux voisins on peut déduire la position du pic : à droite si on est sur la pente croissant, à gauche si on est sur la pente décroissante.

def recherche_pic(l) :
    """
    Entrées : l une montagne telle que décrite dans l'énoncé.
    Sortie : le pic de la montagne tel que défini dans l'énoncé
    """
    if len(l) == 1 : # l est non vide, on élimine le cas d'un singleton
        return 0
    # len(l) > 1
    d, f = 0, len(l)
    while d < f :
        m = ( d + f ) // 2
        if ( m == 0 or
             ( m == len(l) - 1 and l[m-1] < l[m]) or
             ( l[m-1] < l[m] and l[m] > l[m + 1] ) ) : # cas du pic
            return (m,l[m])
        if l[m-1] < l[m]  : # pente croissante
            d = m + 1
        else : # pente décroissante
            f = m
tests = ([1,2,3,2,1], [1,2,3,4,5,6,7,6,4,3], [1,2,3], [3,2,1], [1], [1,2], [2,1], [0,1,2,3,4,1])
for t in tests :
    print(t," : ", recherche_pic(t))
[1, 2, 3, 2, 1]  :  (2, 3)
[1, 2, 3, 4, 5, 6, 7, 6, 4, 3]  :  (6, 7)
[1, 2, 3]  :  (2, 3)
[3, 2, 1]  :  (0, 3)
[1]  :  0
[1, 2]  :  (1, 2)
[2, 1]  :  (0, 2)
[0, 1, 2, 3, 4, 1]  :  (4, 4)

Complexité : La boucle while tourne tant que d < f. On montre par récurrence qu’au tour de boucle \(i\), \(|d - f| \leq \frac n {2^i}\).

  • C’est vrai au tour de boucle \(0\) car \(|d-f| = n\).
  • Supposons la propriété vrai au tour de boucle \(i\). Et supposons que la boucle s’exécute une fois de plus. Après l’exécution de la boucle, il y a deux possibilités menant à l’exécution d’un tour de boucle supplémentaire :

    • \(| d - f | \gets | d - \lfloor \frac {d + f} 2 \rfloor | \leq | \frac {f - d} 2 | \leq \frac n {2^{i+1}}\)
    • \(| d - f | \gets | \lfloor \frac {d + f} 2 \rfloor - f | \leq | \frac {d - f} 2 | \leq \frac n {2^{i+1}}\)

    Dans les deux cas, la propriété est vérifiée au rang \(i+1\)

  • La propriété est donc vrai, et donc pour \(i = \lceil \log_2(n) \rceil\) on a \(|d - f| = 0\) (ce sont des entiers). La boucle tourne donc au plus \(\lceil \log_2(n) \rceil\) fois.

    La complexité finale est donc en \(\mathrm O(\log_2(n))\).

Correction :

On rappelle :

  • Pré-conditions : \(l\) est une montagne
  • Post-conditions : l’algorithme renvoie un entier \(i\) tel que \(0 \leq i < |l|\) et \(l[i] = \max(l)\)

On traite le cas \(|l| \geq 2\).

On montre l’invariant suivant pour la boucle while :

\[ 0 \leq d \leq f \leq |l| \text{ et } \exists d \leq k < f, \max(l) = l[k] \]

  • C’est vrai avant d’entrer dans la boucle car \(d = 0\) et \(f = |l|\) et que \(l\) est un ensemble fini et contient donc un maximum.
  • Si la propriété est vrai à l’entrée de la boucle et que la sortie de la boucle est atteinte, alors \(m > 0\) et

    • \(m < |l| - 1\) et \(l[m] \neq \max(l)\) par définition d’une montagne, ou
    • \(m = |l| - 1\) et $l[m-1] ≥ l[m], on est sur la pente décroissante de la montagne et \(l[m] \neq \max(l)\).

    Dans les deux cas, \(l[m] \neq \max(l)\). On note que \(m-1 \geq 0\) est qu’il est correct de parler donc de \(l[m-1]\).

    • Si \(l[m-1] < l[m]\) alors, comme \(l[m]\) n’est pas le maximum de la liste, \(m\) est un indice situé sur la pente croissante. On en déduit que l’indice \(k\) du maximum vérifie \(k > m\). On sait déjà que \(k < f\). Donc à la sortie de la boucle, comme \(d \gets m + 1\) on retrouve l’invariant.
    • Si \(l[m-1] \geq l[m]\) alors, comme \(l[m]\) n’est pas le maximum de la liste, \(m\) est un indice situé sur la pente décroissante. On en déduit que l’indice \(k\) du maximum vérifie \(k < m\). On sait déjà que \(k \geq d\). Donc à la sortie de la boucle, comme \(f \gets m\) on retrouve l’invariant.

L’invariant est donc vrai quelque soit le tour de boucle.

Il y a deux sortie possible de l’algorithme :

  • Lors d’un tour de boucle, trois cas possibles :

    • \(m = 0\), alors \(d = 0\) et \(f = 1\). Par l’invariant de boucle, \(\max(l) = l[0]\).
    • Comme \(m \neq 0\), on peut parler de \(l[m-1]\). On suppose donc que \(m = |l| - 1\) et \(l[m-1] < l[m]\). La montagne croit jusqu’au dernier élément, donc le maximum est le dernier élément.
    • Montrons d’abord que le troisième cas est traité uniquement si \(m \neq |l| - 1\), ce qui impliquera qu’il est possible de parler de \(l[m+1]\). On conclura alors par la définition du maximum pour une montagne.

      Supposons \(m = |l| - 1\), alors \(l[m-1] \geq l[m]\), car la seconde condition est fausse. On rappelle que les disjonctions sont traitées une par une de gauche à droite, et que l’on s’arrête à la première qui est vrai. Grâce encore à cette observation, la troisième condition sera évaluée à faux avant même de tester la seconde partie du and faisant intervenir \(m+1\).

    Dans tous les cas l’algorithme renvoie un indice entre 0 et \(|l|\) qui correspond à l’indice du maximum.

  • A la fin de la boucle : l’algorithme renvoie None. Montrons que ce cas est absurde. Comme la boucle est terminée on a \(d \geq f\). L’invariant de boucle dit qu’il existe un indice \(d \leq k < f\) pour lequel le maximum est atteint. C’est absurde.

Le seul cas qui peut arriver est donc le premier, et la post-condition est vérifiée.

1.7. Montagne avec plateau

On va utiliser deux recherches dichotomiques dans la montagne pour trouver l’indice du début du plateau et l’indice de fin du plateau. Cela est possible car, en regardant un élément et ses deux voisins on peut déduire la position du début du plateau : à droite si on est sur la pente croissante, à gauche si on est sur la pente décroissante ou sur le plateau. Idem pour la fin du plateau.

La recherche du début du plateau est similaire à la recherche du pic d’une montagne sans plateau, pour laquelle on considère que le plateau fait partie de la pente descendante.

def recherche_debut_pic_plateau(l) :
    """
    Entrées : l une montagne avec plateau telle que décrite dans l'énoncé.
    Sortie : le début du plateau de la montagne tel que défini dans l'énoncé
    """
    if len(l) == 1 : # l est non vide, on élimine le cas d'un singleton
        return 0
    # len(l) > 1
    d, f = 0, len(l)
    while d < f :
        m = ( d + f ) // 2
        if ( m == 0 or
             ( m == len(l) - 1 and l[m-1] < l[m]) or
             ( l[m-1] < l[m] and l[m] >= l[m + 1] ) ) : # cas du début du plateau
            return m
        if l[m-1] < l[m]  : # pente croissante strictement
            d = m + 1
        else : # pente décroissante ou plateau
            f = m
def recherche_pic_plateau(l) :
    """
    Entrées : l une montagne avec plateau telle que décrite dans l'énoncé.
    Sortie : le pic du plateau de la montagne tel que défini dans l'énoncé
    """
    p_debut = recherche_debut_pic_plateau(l)
    p_fin = len(l) - recherche_debut_pic_plateau(l[::-1]) - 1
    pic = ( p_debut + p_fin ) // 2
    return pic, t[pic]
tests_montagne = ([1,2,3,2,1], [1,2,3,4,5,6,7,6,4,3], [1,2,3], [3,2,1], [1], [1,2], [2,1], [0,1,2,3,4,1])
tests_plateau = ([1,2,3,3,3,2,1], [1,2,3,4,5,6,6,6,4,3], [1,2,3,3,3], [3,3,2,1], [1,1,1], [2,2])
tests = tests_montagne + tests_plateau
for t in tests :
    print(t," : ", recherche_pic_plateau(t))
[1, 2, 3, 2, 1]  :  (2, 3)
[1, 2, 3, 4, 5, 6, 7, 6, 4, 3]  :  (6, 7)
[1, 2, 3]  :  (2, 3)
[3, 2, 1]  :  (0, 3)
[1]  :  (0, 1)
[1, 2]  :  (1, 2)
[2, 1]  :  (0, 2)
[0, 1, 2, 3, 4, 1]  :  (4, 4)
[1, 2, 3, 3, 3, 2, 1]  :  (3, 3)
[1, 2, 3, 4, 5, 6, 6, 6, 4, 3]  :  (6, 6)
[1, 2, 3, 3, 3]  :  (3, 3)
[3, 3, 2, 1]  :  (0, 3)
[1, 1, 1]  :  (1, 1)
[2, 2]  :  (0, 2)

Complexité : La complexité est celle de recherche_debut_pic_plateau sur une entrée de longueur \(n\). C’est la même complexité que la fonction recherche_pic de la question précédente. La complexité finale est donc en \(\mathrm O(\log_2(n))\).

Correction : La preuve de correction est basée sur celle de recherche_debut_pic_plateau. La preuve est identique à celle de la fonction recherche_pic, il suffit de remplacer par «pente décroissante ou plateau» partout où on lit «pente décroissante». On vérifie ensuite que \(l\) inversée est toujours une montagne avec plateau et que la fin du plateau de \(l\) correspond bien à \(|l|-1-k\) si \(k\) est le début du plateau de \(l\) inversée.

1.8. Ordonnancement

L’heuristique gloutonne consiste à choisir toujours la liste qui a la plus petite somme si l’entier et positif, la plus grande si l’entier est négatif. C’est le choix optimal localement (à une étape donnée de l’algorithme).

def ordonnancement(l) :
    """
    Entrées : l une liste d'entiers
    Sortie : l1 et l2 deux listes dans lesquelles sont répartis les entiers de l
    """
    l1, l2 = [], []
    sum1, sum2 = 0, 0
    for x in l :
        if x > 0 :
            if sum1 > sum2 :
                l2.append(x)
                sum2 += x
            else :
                l1.append(x)
                sum1 += x
        else :
            if sum1 > sum2 :
                l1.append(x)
                sum1 += x
            else :
                l2.append(x)
                sum2 += x
    return l1, l2

Plus concis :

def ordonnancement(l) :
    """
    Entrées : l une liste d'entiers
    Sortie : l1 et l2 deux listes dans lesquelles sont répartis les entiers de l
    """
    l1, l2 = [], [] # l1 sera la plus grande somme à chaque fois
    sum1, sum2 = 0, 0
    for x in l :
        if x < 0 :
            l1, l2, sum1, sum2 = l2, l1, sum2, sum1
        l2.append(x)
        sum2 += x
        if sum2 > sum1 :
            l1, l2, sum1, sum2 = l2, l1, sum2, sum1
    return l1, l2

Ordonnancement optimal de [100,80,40,40,50,50] : [100,80], [40,40,50,50].

ordonnancement([100,80,40,40,50,50])
100 40 50
80 40 50

1.9. Ordonnancement Optimal

Pour le moment, sans utiliser la programmation dynamique, on ne sait pas mieux faire qu’essayer toutes les répartitions possibles.

Une répartition de \(n\) éléments dans deux listes peut être représentée par une liste de \(n\) booléens. Si le booléen \(i\) est vrai, l’élément va dans la première liste, s’il est faut l’élément va dans la seconde.

On va générer toutes les listes de booléens possibles. Pour gagner un peu de performance, on va éviter de générer la négation de la liste (chaque booléen est remplacé par sa négation) car cela correspond à la répartition duale où les deux listes sont simplement inversée. La différence est alors identique.

Pour éviter de générer les répartitions duales il suffit de considérer que le premier élément est toujours assigné à la liste \(l_1\).

Le problème se réduit donc à :

  1. La génération de toutes les listes de booléens possible de taille fixée et commençant par True.
  2. Un calcul de maximum.

La génération des listes de booléens se fait bien de manière récursive…

def repartition(n) :
    """
    Entrées : un entier n positif
    Sortie : la listes des listes de booléens possible de taille n
    """
    if n == 0 :
        return [[]]
    rep = repartition(n-1)
    return [ [True] + l for l in rep ] + [ [False] + l for l in rep ]
def ordonnancement_optimal(l) :
    """
    Entrées : l une liste d'entiers
    Sortie : l1 et l2 deux listes dans lesquelles sont répartis les entiers de l
    """
    if not l :
        return [],[]
    diff_max = sum(l)
    rep_max = [True] * len(l)
    rep = [ [True] + l for l in repartition(len(l) - 1) ]
    for r in rep :
        sum1, sum2 = 0, 0
        for i,b in enumerate(r) :
            if b :
                sum1 += l[i]
            else :
                sum2 += l[i]
        if abs(sum1 - sum2) < diff_max :
            rep_max = r
            diff_max = abs(sum1-sum2)
    l1, l2 = [], []
    for i,b in enumerate(rep_max) :
        if b :
            l1.append(l[i])
        else :
            l2.append(l[i])
    return l1, l2

Ordonnancement optimal de [100,80,40,40,50,50] : [100,80], [40,40,50,50].

ordonnancement_optimal([100,80,40,40,50,50])
100 80    
40 40 50 50

Terminaison : Il faut prouver la terminaison de repartition. Dans ordonnancement_optimal seules des boucles for sont utilisées, donc les boucles terminent.

On prouve que la fonction repartition termine par récurrence sur n. La récurrence est évidente.

Correction : Étant donnée une liste de booléen \(r\), on note \(l_1^r\) et \(l_2^r\) les deux répartitions de \(l\) représentées par \(r\).

  1. On montre la correction de repartition par récurrence. La récurrence n’est pas bien compliquée : si \(\cal R_n\) est l’ensemble des listes de booléens de taille n on a bien \(\cal R_0 = \{ [~] \}\) et \[ {\cal R}_n = \{ [\texttt{True}] + \texttt{L}, \texttt{L} \in {\cal R}_{n-1} \} \cup \{ [\texttt{False}] + \texttt{L}, \texttt{L} \in {\cal R}_{n-1} \} \]
  2. On montre que pour \(r \in \texttt{rep}\), après l’exécution de cette partie du code

        sum1, sum2 = 0, 0
        for i,b in enumerate(r) :
            if b :
                sum1 += l[i]
            else :
                sum2 += l[i]
    

    on montre facilement que sum1 vaut \(\texttt{sum}(l_1^r)\) et sum2 vaut \(\texttt{sum}(l_2^r)\). L’invariant de boucle à montrer est que à l’étape \(i\), la propriété est vrai pour les répartitions intermédiaires de l[:i] représentées par r[:i].

  1. On montre alors que le code suivant calcule dans rep_max la repartition ayant la plus petite différente.

    for r in rep :
        sum1, sum2 = 0, 0
        for i,b in enumerate(r) :
            if b :
                sum1 += l[i]
            else :
                sum2 += l[i]
        if abs(sum1 - sum2) < diff_max :
            rep_max = r
            diff_max = abs(sum1-sum2)
    

    On sait par correction de repartition que rep contient bien toutes les répartitions possibles. On prouve l’invariant qui montre que rep_max représente bien à l’étape \(i\) la répartition ayant la différence de somme minimale parmis les \(i\) répartitions déjà vu. On peut alors conclure.

  2. Enfin on montre facilement que ce code calcule dans \(l_1\) et \(l_2\) les listes \(l_1^{\texttt{rep_max}}\) et \(l_2^{\texttt{rep_max}}\). On passe par un invariant qui montre que c’est le cas pour les répartitions intermédiaires à l’étape \(i\).

2. Tris

2.1. Tri par comptage

def tri_lineaire(l, m) :
    """
    Entrées : l une lisste d'entiers positifs et m une borne supérieure de l
    Sortie : une copie de l triée
    """
    cpt = [0]*(m+1)
    l_triee = []
    for x in l :
        cpt[x] += 1
    for i,n in enumerate(cpt) :
        l_triee.extend([i]*n)
    return l_triee
import random
l = [ random.randint(0,99) for i in range(50) ]
tri_lineaire(l,99)
0 0 1 6 7 12 14 16 20 21 21 21 24 24 24 27 30 31 33 36 37 37 39 40 41 42 42 43 47 47 57 61 65 66 67 68 69 69 74 75 77 77 79 84 86 90 91 96 98 99

2.2. Tri partition-fusion

def fusion(l1,l2) :
    """
    Entrées : l1 et l2 deux listes triées
    Sortie : une liste triée dont les éléments sont ceux de l1 et l2
    """
    l = []
    i1, i2 = 0, 0
    while i1 < len(l1) and i2 < len(l2) :
        if l1[i1] < l2[i2] :
            l.append(l1[i1])
            i1 += 1
        else :
            l.append(l2[i2])
            i2 += 1
    if i1 == len(l1) :
        l.extend(l2[i2:])
    else :
        l.extend(l1[i1:])
    return l
def tri_fusion(l) :
    """
    Entrées : l une liste d'entiers
    Sortie : une copie de l triée
    """
    if len(l) < 2 :
        return l
    return fusion(tri_fusion(l[:len(l)//2]), tri_fusion(l[len(l)//2:]))
import random
l = [ random.randint(0,99) for i in range(50) ]
tri_fusion(l)
2 5 5 5 6 6 13 14 16 18 19 22 25 28 32 33 37 37 37 37 39 39 39 39 39 48 49 52 53 54 59 60 60 60 61 65 69 69 71 74 75 75 77 79 82 83 84 86 88 98

2.3. Tri rapide

import random

def tri_rapide(l) :
    if len(l) < 2 :
        return l
    pivot = random.randint(0,len(l)-1)
    l_inf, l_sup = [], []
    nb_pivot = 0
    for x in l :
        if x < l[pivot] :
            l_inf.append(x)
        elif x > l[pivot] :
            l_sup.append(x)
        else :
            nb_pivot += 1
    return tri_rapide(l_inf) + [l[pivot]]*nb_pivot + tri_rapide(l_sup)
import random
l = [ random.randint(0,99) for i in range(50) ]
tri_rapide(l)
1 1 2 2 3 5 6 6 8 14 14 15 15 17 18 18 19 28 41 41 43 45 46 48 48 51 55 57 59 61 62 64 65 67 67 71 71 73 78 78 78 79 80 84 85 87 91 94 94 99

3. Récursivité

3.1. Fonction d’Ackermann

def ackermann(m,n) :
    """
    Entrées : m et n deux entiers positifs
    Sortie : A(m,n)
    """
    if m == 0 :
        return n + 1
    elif n == 0 :
        return ackermann(m-1,1)
    else :
        return ackermann(m-1,ackermann(m,n-1))

3.2. Exponentiation rapide

def puissance(a,b) :
    """
    Entrées : a un flottant et b un entiers
    Sortie : a^b
    """
    if b == 0 :
        return 1
    if b % 2 == 0 :
        return puissance(a*a,b // 2)
    else :
        return a * puissance(a*a, b // 2)

3.3. Conversion binaire

def binaire(n, nb_bits) :
    """
    Entrées : n un entier relatif, nb_bits le nombre de bits de l'écriture
    Sortie : l'écriture binaire de n sous forme de chaîne de caractères
    """
    if nb_bits == 0:
        return ""
    if n < 0 :
        n = 2 ** nb_bits + n # complément à 1 + 1
    return binaire(n // 2, nb_bits -1) + str(n % 2)

3.4. Fibonacci

Pour obtenir du linéaire, la fonction doit renvoyer non seulement la valeur de Fibonnacci mais aussi la valeur précédente ! Ce qui évite de la recalculer à chaque fois.

def fibonnacci(n) :
    """
    Entrées : n un entier positif
    Sortie : F_n
    """
    if n < 2 :
        return (n, n-1)
    f, f_prev = fibonnacci(n-1)
    return (f + f_prev, f)

3.5. Calcul approché de \(\sqrt 2\)

def u(n) :
    if n == 0 :
        return 1.0
    u_prev, v_prev = u(n-1), v(n-1)
    return ( 2 * u_prev * v_prev ) / (u_prev + v_prev)
def v(n) :
    if n == 0 :
        return 2.0
    u_prev, v_prev = u(n-1), v(n-1)
    return ( u_prev + v_prev ) / 2

3.6. Tours de Hanoï

Il s’agit d’une application du problème des tours de Hanoï.

On peut résoudre se problème de manière récursive :

  • Si il y a aucun carton, le problème est résolu…
  • Supposons que depuis A, on sache déplacer \(n\) carton vers C en utilisant B. Prenons \(n+1\) cartons arrivés en A. On déplace les \(n\) premiers cartons vers B en utilisant C comme zone de transit. Puis on déplace le carton \(n+1\) vers C. Enfin on déplace les \(n\) cartons de B vers C en utilisant A comme zone de transit.
def instructions(n, A, B, C) :
    """
    Entrées : n le nombre de cartons arrivés en A
    Sortie : la séquence d'instructions pour déplacer
        les cartons de A vers C en transitant par B
    """
    if n == 0 :
        return ""
    ins = instructions(n-1, A, C, B)
    ins += A + " -> " + C + "\n"
    ins += instructions(n-1, B, A, C)
    return ins
print(instructions(1,"A","B","C"))
print("---")
print(instructions(3,"A","B","C"))
print("---")
print(instructions(7,"A","B","C"))
A -> C

---
A -> C
A -> B
C -> B
A -> C
B -> A
B -> C
A -> C

---
A -> C
A -> B
C -> B
A -> C
B -> A
B -> C
A -> C
A -> B
C -> B
C -> A
B -> A
C -> B
A -> C
A -> B
C -> B
A -> C
B -> A
B -> C
A -> C
B -> A
C -> B
C -> A
B -> A
B -> C
A -> C
A -> B
C -> B
A -> C
B -> A
B -> C
A -> C
A -> B
C -> B
C -> A
B -> A
C -> B
A -> C
A -> B
C -> B
C -> A
B -> A
B -> C
A -> C
B -> A
C -> B
C -> A
B -> A
C -> B
A -> C
A -> B
C -> B
A -> C
B -> A
B -> C
A -> C
A -> B
C -> B
C -> A
B -> A
C -> B
A -> C
A -> B
C -> B
A -> C
B -> A
B -> C
A -> C
B -> A
C -> B
C -> A
B -> A
B -> C
A -> C
A -> B
C -> B
A -> C
B -> A
B -> C
A -> C
B -> A
C -> B
C -> A
B -> A
C -> B
A -> C
A -> B
C -> B
C -> A
B -> A
B -> C
A -> C
B -> A
C -> B
C -> A
B -> A
B -> C
A -> C
A -> B
C -> B
A -> C
B -> A
B -> C
A -> C
A -> B
C -> B
C -> A
B -> A
C -> B
A -> C
A -> B
C -> B
A -> C
B -> A
B -> C
A -> C
B -> A
C -> B
C -> A
B -> A
B -> C
A -> C
A -> B
C -> B
A -> C
B -> A
B -> C
A -> C

4. Traitement d’images

4.1. Identification des composantes connexes

On peut considérer notre image comme un graphe. Les noeuds sont les pixels. Deux noeuds sont voisins si et seulement si ils sont voisins dans l’image et ils sont noirs tous les deux. Deux pixels \(p_1 = (x_1,y_1)\) et \(p_2 = (x_2,y2)\) sont dits voisins dans l’image si \(|x_1 - x_2| \leq 1\) et \(|y_1-y2| \leq 1\), i.e \((-1,-1) \leq p_1 - p_2 \leq (1,1)\). Graphiquement, deux pixels sont voisins si il sont côte à côte, l’un sur l’autre, ou en diagonal l’un de l’autre.

On implémente un parcours en profondeur pour trouver les composantes connexes. On évite de représenter en mémoire la matrice d’adjacence qui prendrait beaucoup de place. On utilisera les propriétés suivantes pour une image de taille \(m \times n\) :

  • Les noeuds sont les pixels \(\{ (i,j), 0 \leq i \leq m-1, 0 \leq j \leq n -1\}\)
  • Un noeud blanc n’a pas de voisins
  • Les voisins du pixels \((i,j)\) sont, parmis les éléments \[ \{ (k,l), i-1 \leq k \leq i+1, j-1 \leq l \leq j + 1 \} \] ceux tels que \(0 \leq k \leq m-1\), \(0 \leq l \leq n -1\) et \((k,l)\) est noir.
from queue import LifoQueue as Pile

def etiquettage_une_composante(image_nb, composantes, depart, num_composante) :
    """
    Entrées :
    - image_nb : une matrice de booléens représentant une image noir et blanc
    - composantes : une matrice donnant la composante de chaque pixel,
        -1 si non attribuée.
    - depart : le premier pixel à considérer, noir et non étiquetté
    - num_composante : le numéro de la composante à étiquetter
    Sortie : Aucune.
    Effet de bord : composantes et mise à jour avec l'étiquettage
        de la composante connexe du pixel depart
    """
    m, n = len(image_nb[0]), len(image_nb)
    a_visiter = Pile()
    a_visiter.put(depart)
    # Parcours en profondeur et étiquettage
    while not a_visiter.empty() :
        i,j = a_visiter.get()
        composantes[i][j] = num_composante
        # Ajout de chaque voisin à visiter
        for k in (i-1,i,i+1) :
            for l in (j-1,j,j+1) :
                if ( 0 <= k < n and 0 <= l < m # Voisin valide
                        and composantes[k][l] == -1 # Voisin non étiquetté
                        and not image_nb[k][l] ) : # Voisin noir
                    a_visiter.put((k,l))
    return None
import numpy as np

def etiquettage_composante(image_nb) :
    """
    Entrées : une matrice de booléens représentant une image noir et blanc
    Sortie : une matrice d'entiers représentant la composante connexe de
        chaque pixel noir. 0 pour les pixels blancs
    """
    m, n = len(image_nb[0]), len(image_nb)
    composantes = np.array([[-1]*m]*n)
    num_composante = 1
    for i,row in enumerate(image_nb) :
        for j,pixel in enumerate(row) :
            if composantes[i][j] == -1 :
                if pixel : # Pixel Blanc
                    composantes[i][j] = 0
                else : # Pixel Noir
                    etiquettage_une_composante(image_nb, composantes,
                                               (i,j), num_composante)
                    num_composante += 1
    return composantes

Pour tester sur une image l’algorithme, on va donner une couleur à chaque composantes. On supposera pour l“exemple que le nombre de composantes de dépasse pas 12.

def composantes_en_couleur(composantes) :
    """
    Entrées : Un étiquettage des composantes d'une image noir et blanc
    Sortie : Une image en couleur, avec chaque composante d'une couleur
        identique
    """

    couleurs = [ (255,255,255), (255,0,0), (0,255,0), (0,0,255),
                 (255,255,0), (0,255,255), (255,0,255), (0,0,0),
                 (255,120,120), (120,255,120), (120,120,255), (120,120,120) ]
    m, n = len(composantes[0]), len(composantes)
    img = np.array([[(0,0,0)]*m]*n, dtype="uint8")
    for i,row in enumerate(composantes) :
        for j, c in enumerate(row) :
            img[i][j] = couleurs[c]
    return img
from PIL import Image
img = Image.open("img/noir_et_blanc.jpg")
img = img.convert("1")
display(img)

60a60ec5f6f225a305dcac300ee97a4c8fbde551.png

image_nb = np.array(img)
composantes = etiquettage_composante(image_nb)
img_comp = composantes_en_couleur(composantes)
display(Image.fromarray(img_comp))

9fe4964af4b05a6e75f5b5066d90a8ed21cee78e.png

4.2. Filtrage

def appliquer_filtre(filtre, vec) :
    """
    Entrées :
    - filtre : une matrice 3x3
    - vec : un vecteur de dimension 3
    Sortie : filtre * vec
    """
    r = [0,0,0]
    for i in range(3) :
        for j in range(3) :
            r[i] += filtre[i][j] * vec[j]
    return r
def filtrage(image, filtre) :
    """
    Entrées :
    - image : une matrice de triplés représentant une image
    - filtre : une matrice 3x3
    Sortie : l'image sur laquelle a été appliqué le filtre
    """
    m, n = len(image[0]), len(image)
    image_filtree = np.array([[[0,0,0]]*m]*n, dtype="uint8")
    for i,row in enumerate(image) :
        for j, pixel in enumerate(row) :
            image_filtree[i][j] = appliquer_filtre(filtre, pixel)
    return image_filtree
from PIL import Image
img = Image.open("img/rgb.jpg")
img_rgb = np.array(img)
display(img)

07e46779bda6847cf6087c20f27a4d390d6ec9b0.png

On test quelques filtres :

\begin{array}{lclclc} R &= \begin{bmatrix} 1 & 0 & 0 \\ 0 & 0 & 0 \\ 0 & 0 & 0 \end{bmatrix} & G &= \begin{bmatrix} 0 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 0 \end{bmatrix} & B &= \begin{bmatrix} 0 & 0 & 0 \\ 0 & 0 & 0 \\ 0 & 0 & 1 \end{bmatrix} \\ NG &= \begin{bmatrix} \frac 1 3 & \frac 1 3 & \frac 1 3 \\ \frac 1 3 & \frac 1 3 & \frac 1 3 \\ \frac 1 3 & \frac 1 3 & \frac 1 3 \end{bmatrix} & D &= \begin{bmatrix} 0.5 & 0 & 0 \\ 0 & 0.5 & 0 \\ 0 & 0 & 0.5 \end{bmatrix} & GB &= \begin{bmatrix} 0 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix} \end{array}
R = np.array([ [1, 0, 0], [ 0, 0, 0], [0, 0, 0] ], dtype="uint8")
img_R = filtrage(img_rgb, R)
display(Image.fromarray(img_R))

rgb_R.png

G = np.array([ [0, 0, 0], [ 0, 1, 0], [0, 0, 0] ], dtype="uint8")
img_G = filtrage(img_rgb, G)
display(Image.fromarray(img_G))

rgb_G.png

B = np.array([ [0, 0, 0], [ 0, 0, 0], [0, 0, 1] ], dtype="uint8")
img_B = filtrage(img_rgb, B)
display(Image.fromarray(img_B))

rgb_B.png

NG = np.array([ [1/3, 1/3 , 1/3], [ 1/3, 1/3, 1/3], [1/3, 1/3, 1/3] ])
img_NG = filtrage(img_rgb, NG)
display(Image.fromarray(img_NG))

rgb_NG.png

D = np.array([ [0.5, 0 , 0], [ 0, 0.5, 0], [0, 0, 0.5] ])
img_D = filtrage(img_rgb, D)
display(Image.fromarray(img_D))

rgb_D.png

GB = np.array([ [0, 0 , 0], [ 0, 1, 0], [0, 0, 1] ])
img_GB = filtrage(img_rgb, GB)
display(Image.fromarray(img_GB))

rgb_GB.png

4.3. Flou Gaussien

import numpy as np

def noyau_gaussien(r, sigma) :
    """
    Entrées : r un entier et sigma un réel
    Sortie : le noyau gaussien de rayon r et ecart type sigma
    """
    h = np.array([[0.0]*(2*r+1)]*(2*r+1))
    somme = 0.0
    for i in range(2*r+1) :
        for j in range(2*r+1) :
            h[i][j] = np.exp( - ( (i-r) ** 2 + (j-r) ** 2 )
                              / ( 2 * sigma ** 2 ) )
            somme += h[i][j]
    for i in range(2*r+1) :
        for j in range(2*r+1) :
            h[i][j] = h[i][j] / somme
    return h
def conv(image,noyau,pixel) :
    """
    Entrées :
    - image : une matrice de flottants entre 0 et 1 représentant une image
        en niveau de gris.
    - noyau : un noyau gaussien
    - pixel : le pixel surlequel sera appliqué le noyau
    Sortie : la nouvelle valeur du pixel après application du noyau
    """
    n = len(noyau)
    r = ( n - 1 ) // 2
    moy = 0
    i,j = pixel
    for k in range(n) :
        for l in range(n) :
            if ( 0 <= i + k - r < len(image) and
                 0 <= j + l - r < len(image[0]) ) :
                moy += noyau[k][l] * image[i + k - r][j + l - r]
    return moy
def flou_gaussien(image, r, sigma) :
    """
    Entrées :
    - image : une matrice de flottants entre 0 et 1 représentant une image
        en niveau de gris.
    - r un entier et sigma un réel
    Sortie : Une copie de l'image sur laquelle a été appliqué un flou gaussien
        de rayon r et écrat-type sigma
    """
    noyau = noyau_gaussien(r,sigma)
    m, n = len(image[0]), len(image)
    image_flou = np.array([[0.0]*m]*n)
    for i,row in enumerate(image) :
        for j in range(len(row)) :
            image_flou[i][j] = conv(image,noyau,(i,j))
    return image_flou
from PIL import Image
img = Image.open("img/niveau_gris.jpg")
img = img.convert("L")
display(img)

77f6a914d493e13f23904076e435756f76cb0b49.png

img_ng = np.array(img)
m, n = len(img_ng[0]), len(img_ng)
img_ng_f = np.array([ [0.0]*m ]*n)
for i in range(n) :
    for j in range(m) :
        img_ng_f[i][j] = img_ng[i][j] / 255
img_flou_f = flou_gaussien(img_ng_f, 4, 10)
img_flou = np.array([ [0]*m ]*n, dtype="uint8")
for i in range(n) :
    for j in range(m) :
        img_flou[i][j] = int(img_flou_f[i][j] * 255)
display(Image.fromarray(img_flou))

niveau_gris_flou.png

5. Algorithmique des Graphes

5.1. Connexité

Pour vérifier la connexité du graphe, on fait un parcours en profondeur pour compter le nombre de sommets appartenant à la composante connexe du sommet 0. S’il y en a autant que de sommets dans le graphe, le graphe est connexe.

from queue import LifoQueue as Pile

def connexite(graphe) :
    """
    Entrées : la liste d'adjacence d'un graphe
    Sortie : un booléen vrai ssi le graphe est connexe
    """
    n = len(graphe)
    if n == 0 :
        return True
    a_visiter = Pile()
    a_visiter.put(0)
    nb_connexe_0 = 0
    visite = [False] * n
    while not a_visiter.empty() :
        sommet = a_visiter.get()
        nb_connexe_0 += 1
        visite[sommet] = True
        for voisin in graphe[sommet] :
            if not visite[voisin] :
                a_visiter.put(voisin)
    return nb_connexe_0 == n

5.2. Détection de cycles

Pour détecter un cycle, on fait un parcours en profondeur de chaque composante connexe. Dès qu’on tombe sur un noeud déjà visité, il y a donc un cycle.

from queue import LifoQueue as Pile

def cycles(graphe) :
    """
    Entrées : la matrice d'adjacence d'un graphe
    Sortie : un booléen vrai ssi le graphe possède un cycle
    """
    n = len(graphe)
    visite = [False] * n
    a_visiter = Pile()
    for depart in range(n) :
        if not visite[depart] :
            a_visiter.put(depart)
            while not a_visiter.empty() :
                sommet = a_visiter.get()
                visite[sommet] = True
                for autre_sommet in range(n) :
                    if sommet != autre_sommet and graphe[sommet][autre_sommet] :
                        if visite[autre_sommet] :
                            return True
                        else :
                            a_visiter.put(autre_sommet)
    return False

5.3. 2-coloration

Pour vérifier si un graphe est 2 coloriable, il suffit d’essayer de le colorier avec 2 couleur. Si on y arrive, il est deux coloriable, sinon, il ne l’est pas.

Pour colorier un graphe avec deux couleur il suffit de choisir une couleur pour le premier sommet, puis de colorier tous ses voisins de l’autre couleur, et de refaire ce processus avec chacune de ses voisins. Si jamais on tombe sur un sommet qui a un voisin déjà colorié par la même couleur que lui, c’est que le graphe n’était pas 2-coloriable.

On réalise donc un parcours en largeur.

from queue import Queue as File

def est_2_coloriable(graphe) :
    """
    Entrées : la liste d'adjacence d'un graphe
    Sortie : un booléen vrai ssi le graphe est 2-coloriable
    """
    n = len(graphe)
    couleur = [-1] * n # -1 non visité, 0 et 1 deux couleurs
    a_visiter = File()
    for depart in range(n) :
        if visite[depart] == -1 :
            a_visiter.put(depart)
            couleur[depart] = 0
            while not a_visiter.empty() :
                sommet = a_visiter.get()
                for voisin in graphe[sommet] :
                    if couleur[voisin] == couleur[sommet] :
                        return False
                    elif couleur[voisin] == -1 :
                        a_visiter.put(voisin)
                        couleur[voisin] = 1 - couleur[sommet]
    return True

5.4. 3-coloration

Le principe est le même, on va essayer de 3-colorier le graphe. Toutefois pour 3 couleurs, il n’existe pas de méthode permettant de déterminer la couleur d’un sommet en fonction de la couleur de ses voisins lors d’un parcours…

On doit tester toutes les colorations possibles.

Une coloration consiste en une liste dont la taille est le nombre de sommet et les valeurs sont 0, 1 et 2. On génère toutes les colorations possible, puis on vérifie que la coloration vérifie : deux voisins n’ont pas la même couleur.

def liste_coloration(n) :
    """
    Entrées : n un entier
    Sortie : la liste des colorations possible avec 3 couleurs pour n sommets
    """
    if n == 0 :
        return [[]]
    colo_prec = liste_coloration(n-1)
    return [ [0] + l for l in colo_prec ] + [ [1] + l for l in colo_prec ] + [ [2] + l for l in colo_prec ]
def est_3_coloration_valide(graphe, col) :
    """
    Entrées :
    - graphe : la liste d'adjacence d'un graphe
    - col : une coloration des sommets
    Sortie : vrai ssi la coloration vérifie : 2 voisins n'ont pas la même
        couleur
    """
    n = len(graphe)
    for sommet in range(n) :
        for voisin in graphe[sommet] :
            if col[sommet] == col[voisin] :
                return False
    return True

def est_3_coloriable(graphe) :
    """
    Entrées : la liste d'adjacence d'un graphe
    Sortie : un booléen vrai ssi le graphe est 3-coloriable
    """
    n = len(graphe)
    liste_col = liste_coloration(n)
    for col in liste_col :
        if est_3_coloration_valide(graphe,col) :
            return True
    return False

Author: Samy Jaziri

Created: 2022-11-02 Wed 23:30