É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 :
- \(i = -1\) et \(x \not\in l\), ou,
- \(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
- comme \(d \leq m < f\), \(0 \leq m + 1 \leq f \leq |l|\).
- 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
- comme \(d \leq m < f\), \(0 \leq d \leq m \leq |l|\).
- 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\).
- \(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|\).
- 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 à :
- La génération de toutes les listes de booléens possible de taille fixée et
commençant par
True
. - 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\).
- 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 taillen
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} \} \] 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)\) etsum2
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 del[:i]
représentées parr[:i]
.
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
querep
contient bien toutes les répartitions possibles. On prouve l’invariant qui montre querep_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.- 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)
image_nb = np.array(img) composantes = etiquettage_composante(image_nb) img_comp = composantes_en_couleur(composantes) display(Image.fromarray(img_comp))
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)
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))
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))
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))
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))
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))
GB = np.array([ [0, 0 , 0], [ 0, 1, 0], [0, 0, 1] ]) img_GB = filtrage(img_rgb, GB) display(Image.fromarray(img_GB))
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)
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))
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