On dispose d’une liste de 5 éléments (par exemple) et on désire tester
une propriété particulière sur tous les sous-ensembles de taille 3 de
cette liste. Si les objets sont numérotés de 1 à 5, on doit tester la
propriété sur les objets 1,2,3 puis 1,2,4 puis 1,2,5 puis 1,3,4 puis
1,3,5 puis 1,4,5 puis 2,3,4 puis 2,3,5 puis 2,4,5 puis 3,4,5 (la
propriété pourrait être, par exemple : la somme des trois nombres donne
un nombre premier). Il est facile d’en dresser la liste à la main. Si en
revanche il y a 20 objets et qu’on doit tester les ensembles de 5 objets
exactement, il y aura alors $C_{20}^5=15 504$
séquences différentes
à tester… mais le module itertools
vient à la rescousse…
Énumérer avec itertools
Le module itertools de Python propose une fonction combinations
qui
permet d’énumérer les sous-ensembles de longueur n
d’un objet itérable
(et donc d’une liste) :
>>> import itertools
>>> l = [1, 2, 3, 4, 5]
>>> for p in itertools.combinations(l, 3):
...: print(p)
...:
(1, 2, 3)
(1, 2, 4)
(1, 2, 5)
(1, 3, 4)
(1, 3, 5)
(1, 4, 5)
(2, 3, 4)
(2, 3, 5)
(2, 4, 5)
(3, 4, 5)
On obtient ainsi les $C_5^3 = 10$
séquences possibles.
Tous les ensembles ayant au plus n valeurs
Si on doit maintenant énumérer les ensembles d’au plus 3 éléments, il suffit d’utiliser le même code pour chaque longueur :
import itertools
l = [1, 2, 3, 4, 5]
for long in (1, 2, 3):
for p in itertools.combinations(l, long):
print(p)
Si on veut obtenir un seul itérable qui permet d’énumérer ces
combinaisons, on peut utiliser itertools.chain
qui permet de…
chaîner plusieurs itérables:
import itertools
l = [1, 2, 3, 4, 5]
for p in itertools.chain(*(itertools.combinations(l, long) for long in range(1, 4))):
print(p)
(1,)
(2,)
(3,)
(4,)
(5,)
(1, 2)
(1, 3)
(1, 4)
(1, 5)
(2, 3)
(2, 4)
(2, 5)
(3, 4)
(3, 5)
(4, 5)
(1, 2, 3)
(1, 2, 4)
(1, 2, 5)
(1, 3, 4)
(1, 3, 5)
(1, 4, 5)
(2, 3, 4)
(2, 3, 5)
(2, 4, 5)
(3, 4, 5)
Et si l’ordre importe ?
On pourrait avoir besoin de tester les séquences dans tous les ordres
possibles : (1,2,3), puis (1,3,2) puis (2,3,1), (2,1,3), (3,1,2),
(3,2,1). Le module itertools
propose une fonction permutations
qui
fournit toutes les permutations d’un itérable particulier. On peut donc
générer les séquences avec combinations
puis modifier l’ordre avec
permutations
:
import itertools
l = [1, 2, 3, 4, 5]
for c in itertools.combinations(l, 3):
for p in itertools.permutations(c):
print(p)
(1, 2, 3)
(1, 3, 2)
(2, 1, 3)
(2, 3, 1)
(3, 1, 2)
(3, 2, 1)
(1, 2, 4)
(1, 4, 2)
(2, 1, 4)
(2, 4, 1)
(4, 1, 2)
(4, 2, 1)
(1, 2, 5)
(1, 5, 2)
(2, 1, 5)
(2, 5, 1)
(5, 1, 2)
...
Cette fois-ci, le nombre de séquences est donné par le nombre
d’arrangements de 3 objets parmi 5 : $A_5^3=60$
. Les 60 séquences
sont bien listées par le code précédent.
Et si on autorise les doublons, les triplets etc… ?
Jusqu’ici, chaque élément de notre ensemble de 5 ne pouvait être choisi qu’une fois. Mais est-il possible d’autoriser plusieurs apparitions du même élément (tirage avec remise) ? Comme si nous piochions des numéros dans un sac, et remettions le numéro tiré dans le sac à chaque fois ?
Le module itertools
propose la fonction
combinations_with_replacement
qui fait justement ce travail :
import itertools
l = [1, 2, 3, 4, 5]
for c in itertools.combinations_with_replacement(l, 3):
print(c)
On obtient :
(1, 1, 1)
(1, 1, 2)
(1, 1, 3)
(1, 1, 4)
(1, 1, 5)
(1, 2, 2)
(1, 2, 3)
(1, 2, 4)
(1, 2, 5)
(1, 3, 3)
(1, 3, 4)
(1, 3, 5)
(1, 4, 4)
(1, 4, 5)
(1, 5, 5)
(2, 2, 2)
(2, 2, 3)
(2, 2, 4)
(2, 2, 5)
(2, 3, 3)
(2, 3, 4)
(2, 3, 5)
(2, 4, 4)
(2, 4, 5)
(2, 5, 5)
(3, 3, 3)
(3, 3, 4)
(3, 3, 5)
(3, 4, 4)
(3, 4, 5)
(3, 5, 5)
(4, 4, 4)
(4, 4, 5)
(4, 5, 5)
(5, 5, 5)
Le nombre de séquences obtenues, pour 3 objets parmi 5 est donné par
$\Gamma_5^3 = C_{3+5-1}^3=35$
.
Des doublons, et de l’ordre ?
Dans la liste précédente figure le tirage (1,3,3), mais pas les tirage
(3,1,3) et (3,3,1). Comme tout à l’heure, nous pouvons forcer
l’apparition de chaque ordre. Mais attention à ne pas utiliser ici
permutations()
ce qui aurait pour effet de lister un peu trop de
triplets (le triplet (3,3,1) apparaîtrait par exemple 2 fois… car on
peut échanger l’ordre des 3…).
La liste des tirages ordonnés, avec remise, correpond à choisir un
élément parmi les 5, et cela 3 fois de suite. Il y a $5^3=125$
tirages possibles, et on peut les générer avec 3 boucles imbriquées :
l = [1, 2, 3, 4, 5]
for l1 in l:
for l2 in l:
for l3 in l:
print((l1, l2, l3))
On obtient bien ainsi les 125 tirages, mais ce n’est pas très joli… et obtenir 4 valeurs au lieu de 3 nécessite de changer la structure même du code (en ajoutant une boucle), ce qui n’est pas bon…
Le module itertools
propose une solution avec la fonction product
,
qui itère sur les éléments du produit cartésien entre plusieurs
itérables (ou éventuellement un seul plusieurs fois avec lui-même, ce
dont nous avons besoin ici) :
import itertools
l=[1, 2, 3, 4, 5]
for c in itertools.product(l, repeat = 3):
print(c)
Là aussi, nous obtenons bien les 125 séquences.
Énumérer sans itertools
Sans le module itertools, il est possible d’énumérer les sous-ensembles
d’un ensemble de $n$
éléments en considérant l’écriture binaire des
nombres de 0 à $2^n-1$
. Si $n$
vaut 3, on considère donc les
nombres de 0 à 7 qui s’écrivent en binaire : 000, 001, 010, 011, 100,
101, 110, 110, 111. En faisant correspondre chaque chiffre binaire à un
élément (il y en a 3), un 0 indique par exemple qu’on ne prend par
l’élément, et un 1 qu’on le prend. Ainsi, l’écriture 000 signifie qu’on
ne prend aucun élément, 100 qu’on prend uniquement le premier, 101 qu’on
prend le premier et le dernier. Énumérer les sous-ensembles revient donc
à compter et à convertir les nombres en binaire.
Ces différentes techniques peuvent être utilisées dans les challenges Pydéfis) suivants :
- Remplissage au mieux
- Somme limite
- …