Sixième exercice en Python, JavaScript et APL

Résumé en français : Vous devez créer un programme qui à partir d’une phrase, met tous les mots distincts dans une liste et retourne une chaine donnant les positions des mots de la phrase initiale dans cette liste. On ne tiendra pas compte de la casse.

Voici un autre exemple pour mieux comprendre :

phrase = 'The number 0 is such a strange number Strangely it has zero meaning'

>> compress(phrase)
'012345617891011'

En effet, la liste des mots uniques est :

['the', 'number', '0', 'is', 'such', 'a', 'strange', 'strangely', 'it', 'has', 'zero', 'meaning']

Il faut donc bien voir que le mot ‘strangely’ est à la 8e place dans la phrase initiale mais à la 7e place dans la liste des mots uniques, c’est donc 7 qu’il faut récupérer en non 8.

Version classique avec boucles

On peut commencer par créer la liste des mots uniques trouvés dans la phrase et dans un second temps parcourir à nouveau tous les mots de la phrase pour trouver leurs positions dans cette liste. Première étape :

python

def compress(phrase):
    mots = []                          # Tableau des mots uniques
    for m in phrase.lower().split():   # On parcourt chaque mot
        if m not in mots:              # Nouveau mot ?
            mots.append(m)             # On l'ajoute à la liste
    return mots

>> compress('The number 0 is such a strange number Strangely it has zero meaning')
['the', 'number', '0', 'is', 'such', 'a', 'strange', 'strangely', 'it', 'has', 'zero', 'meaning']

Remarquez qu’en Python, split peut s’utiliser sans paramètre (par défaut ce sera espace) :

>> 'ab cd'.split()
['ab', 'cd']

On peut maintenant chercher les positions des mots dans la liste des mots uniques en utilisant index :

>> ['the', 'number', '0', 'is'].index('is')  # Position de 'is' ?
3

Ce qui donne cette version finale à tester ici :

def compress(phrase):
    decoup = phrase.lower().split()
    mots = []
    for m in decoup:
        if m not in mots:
            mots.append(m)
    return ''.join([ str(mots.index(m)) for m in decoup ])

>> compress('The number 0 is such a strange number Strangely it has zero meaning')
'012345617891011'

str permet de convertir un nombre en chaine.

javascript

La première partie pour rechercher les mots uniques peut être faite comme en Python ou en utilisant reduce (puisque l’on part d’un tableau vide [ ] et que l’on va ajouter au fur et à mesure les mots nouveaux). Ce qui donne :

var compress = phrase => {
  decoup = phrase.toLowerCase().split(' ')
  mots = decoup.reduce((a, m) => a.includes(m) ? a : a.concat(m), [])
  return mots
}

>> compress('The number 0 is such a strange number Strangely it has zero meaning')
['the', 'number', '0', 'is', 'such', 'a', 'strange', 'strangely', 'it', 'has', 'zero', 'meaning']

Ensuite, pour la seconde partie, il s’agit de transformer (map) chaque mot en sa position dans la liste des mots uniques. Voici 2 exemples d’utilisation de map :

>> [1, 5, 10].map(v => v * v)     // On met chaque élément au carré
[1, 25, 100]

>> ['abra','cada','bra'].map(m => m[0].toUpperCase())
['A', 'C', 'B']     // Première lettre de chaque mot en majuscule

D’où cette version finale à tester ici :

var compress = phrase => {
  decoup = phrase.toLowerCase().split(' ')
  mots = decoup.reduce((a, m) => a.includes(m) ? a : a.concat(m), [])
  return decoup.map(m => '' + mots.indexOf(m)).join('') 
}

Le  » + permet de convertir un nombre en chaine. Par exemple  » + 2 donne ‘2’, on peut aussi utiliser (2).toString().

Autres pistes : dictionnaires, ensembles…

Nous avons déjà vu l’utilisation de set pour créer des ensembles en Python/JavaScript et APL (). La première partie peut alors se simplifier en :

javascript

var compress = phrase => {
  decoup = phrase.toLowerCase().split(' ')
  mots = [...new Set(decoup)]     // Liste des mots uniques
  return decoup.map(m => '' + mots.indexOf(m)).join('') 
}

>> compress('The number 0 is such a strange number Strangely it has zero meaning')
'012345617891011'

python

Nous aurions également pu utiliser un dictionnaire, voici une écriture en Python :

>> dict.fromkeys('a b B A C b a'.lower().split())
{'a': None, 'b': None, 'c': None}

>> list(dict.fromkeys('a b B A C b a'.lower().split()))
['a', 'b', 'c']

Ce qui donne ce programme final :

def compress(phrase):
  decoup = phrase.lower().split()  
  mots = list(dict.fromkeys(decoup))     # Liste des mots uniques
  return ''.join(str(mots.index(m)) for m in decoup)

APL

Nous avons déjà vu dans l’exercice précédent comment séparer les mots en utilisant ⊆ :

      {(' '≠⍵) ⊆ ⎕C ⍵} 'LisTe dE MotS'
┌─────┬──┬────┐
│liste│de│mots│
└─────┴──┴────┘

Et ⍳ (iota) permet (en version dyadique, c’est-à-dire avec 2 arguments) de trouver la position d’une valeur dans un vecteur :

      4 8 7 9 ⍳ 7    ⍝ Quelle est la position du 7 dans le vecteur ?
3

Pour récupérer les mots uniques, on peut utiliser ou key (⌸) plus général qui permet de synthétiser des informations :

      {⍺,⍵}⌸ 'a' 'b' 'b' 'a' 'c' 'b' 'a'     ⍝ Lettres et positions
a 1 4 7
b 2 3 6
c 5    
      {⍺}⌸ 'a' 'b' 'b' 'a' 'c' 'b' 'a'       ⍝ Que les lettres
abc

      ∪ 'a' 'b' 'b' 'a' 'c' 'b' 'a'          ⍝ Plus simple avec ∪
abc

Un exemple pour mieux comprendre l’utilisation de ⍳ :

      'un' 'deux' 'trois' ⍳ 'deux' 'deux' 'trois' 'un' 'un'
2 2 3 1 1

Ce qui signifie : quelles sont les positions des mots ‘deux’ ‘deux’ ‘trois’ ‘un’ ‘un’ dans le vecteur ‘un’ ‘deux’ ‘trois’ ?

Enfin, en APL, il est possible de simplifier des écritures du type :

(⍺ f ⍵) g (⍺ h ⍵)

C’est-à-dire calculer ⍺ f ⍵f est une fonction dyadique, calculer ⍺ h ⍵ avec h également dyadique et enfin appliquer g aux 2 résultats trouvés (g dyadique également). La version simplifiée est :

⍺ (f g h) ⍵

Exemple avec f = +, h = ⌈ et g = × :

      (2 + 3) × (2 ⌈ 3)   ⍝ 5 × 3 (= le plus grand entre 2 et 3)
15

      2 (+ × ⌈) 3         ⍝ Notation appelée "train" ou "fork"
15

De la même façon :


(f ⍵) g (h ⍵) peut s'écrire (f g h) ⍵

Exemple :

      (⌊/ × ⌈/) 3 7 2 9 5
18

Explications : On cherche la plus petite des valeurs (⌊/), c’est-à-dire 2 et la plus grande (⌈/) c’est-à-dire 9 puis on les multiplie, ce qui donne 18.

Programme final en APL :

      ⎕IO ← 0
      compress ← {,/⍕¨(∪ ⍳ ⊢)(' '≠⍵) ⊆ ⎕C ⍵}

      compress 'The number 0 is such a strange number Strangely it has zero meaning'
┌───────────────┐
│012345617891011│
└───────────────┘

Le ,/ sert à concaténer les termes et ⍕¨ à convertir en chaine chaque position numérique trouvée.

La traduction du programme est donc : Prendre le paramètre (), le convertir en minuscules (⎕C), séparer la phrase en utilisant les espaces (‘ ‘≠⍵) ⊆, prendre les éléments uniques () et cherchez les positions () des mots ( correspond à ce que l’on a à droite). Transformez toutes les positions en chaines (⍕¨) et en faire la concaténation (,/).

Cinquième exercice en Python, JavaScript et APL

Exemple 1 : ~O~O~O~OP
Tous les rats vont bien vers le joueur de flûte, donc aucun n’est sourd. Valeur attendue = 0

Exemple 2 : PO~O~~OO~
Le rat souligné va dans la mauvaise direction, il est donc sourd. Valeur attendue = 1

Exemple 3 : ~OO~~O~OP~OO~O~
Cette fois il y a 2 rats sourds. Valeur attendue = 2

Séparer les rats à gauche et à droite du joueur de flûte

Une première idée est de récupérer les rats qui sont à gauche et à droite du joueur de flûte. Pour cela on utilise split en Python/JavaScript ou en APL.

Python & Javascript

>> '~O~O~O~OP'.split('P')
['~O~O~O~O', '']

>> 'PO~O~~OO~'.split('P')
['', 'O~O~~OO~']

>> '~OO~~O~OP~OO~O~'.split('P')
['~OO~~O~O', '~OO~O~']

APL

      {('P'≠⍵) ⊆ ⍵} '~OO~~O~OP~OO~O~'
┌────────┬──────┐
│~OO~~O~O│~OO~O~│
└────────┴──────┘

Ensuite, un rat sera sourd dans la partie gauche s’il y a un ‘O’ à une position paire (puisque l’on est censé avoir déjà la queue du rat puis la tête ~O et pas l’inverse). De la même façon, dans la partie droite, un rat est sourd si on trouve un ‘O’ à une position impaire. On pourrait donc imaginer faire 2 boucles, chacune ressemblant à :

for i in range(len(gauche)):
  if gauche[i] == 'O' and i%2 == 0:   # un 'O' à une position paire
    sourds += 1     

Une boucle suffit si on concatène gauche et droite (et que l’on retourne une des 2 chaines). C’est-à-dire passer de [‘~OO~~O~O‘, ‘~OO~O~’] à l’unique chaine ‘~OO~~O~O~O~OO~’ (on a retourné tous les rats qui étaient dans la partie droite). Les rats sourds sont ceux où il y a un ‘O’ à une position paire (rappel, en Python et JavaScript, le premier élément d’une chaine ou d’un tableau est à la position 0).

retourner une chaine en Python

>> 'bonjour'[::-1]
'ruojnob'

Retourner une chaine en javascript

Javascript sait inverser un tableau mais pas une chaine. On va donc convertir la chaine en tableau, inverser ce tableau puis revenir à une chaine :

>>[...'bonjour']
['b', 'o', 'n', 'j', 'o', 'u', 'r']

>> [...'bonjour'].reverse()
['r', 'u', 'o', 'j', 'n', 'o', 'b']

>> [...'bonjour'].reverse().join('')
'ruojnob'

Si on a fréquemment besoin d’inverser une chaine en JavaScript, soit on trouve une bibliothèque (comme Esrever) contenant cette fonctionnalité, ou on crée un prototype :

>> String.prototype.reverse = function() {
  return this.split("").reverse().join("");
}

>> 'bonjour'.reverse()
'ruojnob'

en apl

      ⌽'bonjour'       ⍝ Admirez le symbole utilisé
ruojnob

version finale en python

Il faut donc parcourir toutes les lettres de la chaine contenant les rats et regarder s’il y a un ‘O’ à une position paire. Pour cela on utilise enumerate qui permettra de récupérer à la fois le caractère et son rang :

>> list(enumerate('~OO~~O~O~O~OO~'))
[(0, '~'), (1, 'O'), (2, 'O'), (3, '~'), (4, '~'), (5, 'O'), (6, '~'), (7, 'O'), (8, '~'), (9, 'O'), (10, '~'), (11, 'O'), (12, 'O'), (13, '~')]

Vous avez tout pour comprendre cette version à tester ici :

def rats_sourds(town):
    [gauche, droite] = town.split('P')
    rats = gauche + droite[::-1]
    return sum([c == 'O' and i % 2 == 0 for i,c in enumerate(rats)])

>> rats_sourds('~O~O~O~OP')
0
>> rats_sourds('PO~O~~OO~')
1
>> rats_sourds('~OO~~O~OP~OO~O~')
2

Javascript

Nous avons besoin de parcourir une chaine et accumuler le nombre de rats sourds, cela fait penser à reduce que nous avons utilisé au premier exercice. Notre accumulateur a sera le nombre de rats sourds (initialisé à 0) qui incrémentera de 1 au fur et à mesure si besoin.

>> var rats_sourds = ville => {
    [gauche, droite] = ville.split('P');
    rats = gauche + [...droite].reverse().join('');
    return [...rats]
       .reduce((a, c, i) => i%2 == 0 && c == 'O' ? a + 1 : a, 0)}

>> rats_sourds('~O~O~O~OP')
0
>> rats_sourds('PO~O~~OO~')
1
>> rats_sourds('~OO~~O~OP~OO~O~')
2

Remarque : reduce existe aussi en Python, en utilisant la bibliothèque functools. Voici un exemple où l’on compte par accumulation le nombre de ‘e’ dans une liste de mots :

>> from functools import reduce
>> mots = ['hello', 'yes', 'no', 'eleve']   # Liste de mots

>> reduce(lambda a, c : a + c.count('e'), mots, 0)
5

A-t-on vraiment besoin de séparer la gauche et la droite ?

En observant un peu plus attentivement la chaine représentant une ville, par exemple ‘~OO~~O~OP~OO~O~’, on se rend compte que peu importe l’emplacement du joueur de flûte, il y aura un rat sourd dès qu’un ‘O’ est observé à une position paire. Par exemple dans la ville ‘P~OO~O~’, la tête du rat sourd est bien à une position paire.

Finalement, il suffit de compter le nombre de ‘O’ qu’il y a dans la ville en n’ayant récupéré que les lettres qui sont aux positions paires.

Python

Il est très facile de récupérer une lettre sur 2 en Python :

>> 'UneLettreSurDeux'[::2]
'UeeteuDu'

Et le programme final, beaucoup plus court suite à notre analyse :

>> def rats_sourds(ville):
    return ville[::2].count('O')

javascript

>> var rats_sourds = ville => [...ville]
       .reduce((a, c, i) => i%2 == 0 && c == 'O' ? a + 1 : a, 0)

On peut aussi utiliser un filtre pour ne garder que les ‘O’ qui sont à des positions paires puis compter le nombre d’éléments (length) :

>> var rats_sourds = ville => [...ville]
       .filter((c, i) => i%2 == 0 && c == 'O').length

>> rats_sourds('~O~O~O~OP')
0
>> rats_sourds('PO~O~~OO~')
1
>> rats_sourds('~OO~~O~OP~OO~O~')
2

apl

Pour sélectionner, en APL, des éléments d’une chaine, d’un vecteur ou d’une matrice, on utilise / et un vecteur booléen (constitué de 1 ou de 0). Par exemple :

      2 | ⍳7               ⍝ Restes des divisions de 1, 2..., 7 par 2
1 0 1 0 1 0 1
      (2 | ⍳7) / 'bonjour' ⍝ qui sert à sélectionner une lettre sur 2
bnor

Au lieu de 7 comme dans l’exemple, on utilisera ⍴⍵ pour récupérer la taille de la ville. On peut finalement repérer les ‘O’ qui sont aux positions paires :

      rats_sourds_version_0 ← {'O' = (2|⍳⍴⍵) / ⍵}

      rats_sourds_version_0 '~OO~~O~OP~OO~O~'
0 1 0 0 0 1 0 0         ⍝ Les 1 correspondent aux têtes des rats sourds

Et ensuite les compter, c’est-à-dire faire une réduction par la somme :

      rats_sourds_version_1 ← {+/ 'O' = (2|⍳⍴⍵) / ⍵}

      rats_sourds_version_1 '~OO~~O~OP~OO~O~'
2

Observez que l’on a fait des tests ( ‘O’ = …) qui donne des 0 ou des 1 puis l’addition de ces valeurs. De façon analogue, la fonction SOMMEPROD d’Excel fait des produits et ensuite leur somme. C’est ce qu’on appelle un produit interne. En voici quelques exemples (⌈ permet de récupérer le plus grand entre 2 nombres) :

    +/  2 6 × 4 5    ⍝ (2 × 4) + (6 × 5) = 38 (somme des produits)
38

   2 6  +.× 4 5      ⍝ Même résultat en utilisant un produit interne
38

    ×/  2 6 ⌈ 4 5    ⍝ (2 ⌈ 4) × (6 ⌈ 5) = 4 × 6 = 24
24

   2 6 ×.⌈ 4 5       ⍝ Produit des plus grands termes
24

Version finale en APL :

Lien pour tester directement le code ci-dessous :

      rats_sourds ← {'O' +.= (2|⍳⍴⍵) / ⍵}

      rats_sourds '~O~O~O~OP'
0
      rats_sourds 'PO~O~~OO~'
1
      rats_sourds '~OO~~O~OP~OO~O~'
2

Quatrième exercice en Python, JavaScript et APL

Résumé en français : On vous donne une suite de nombres ainsi qu’une fonction booléenne. On cherche à récupérer le plus long préfixe (c’est-à-dire en commençant par le terme à gauche) d’éléments vérifiés par cette fonction. Par exemple, si la fonction booléenne est « être un nombre pair » (isEven en anglais) et que la suite de nombres est [2,4,6,8,1,2,5,4,3,2], le plus long préfixe est [2,4,6,8] puisqu’ensuite 1 n’est pas pair.

Cet exercice est plus abstrait que les précédents dans le sens où l’un des paramètres est une fonction.

A quel endroit faut-il s’arrêter ?

Une idée est de chercher le rang (s’il existe) où la fonction booléenne donnera faux. On devra alors garder les valeurs entre la position 0 et rang – 1. Pour l’exemple donné, la première valeur impaire est à la 4e position (le premier nombre est à la position 0), on garde donc les nombres entre les positions 0 et 4-1=3. En JavaScript c’est assez simple, voici par exemple comment trouver la position du premier nombre impair :

>> [2,4,6,8,1,2,5,4,3,2].findIndex(v => v % 2 != 0)
4

Remarquez que v % 2 != 0 peut être remplacé par v % 2 uniquement puisque si un nombre est impair, v % 2 sera égal à 1 qui correspond à VRAI en Python ou JavaScript.

Et lorsqu’aucune valeur ne remplit la condition, JavaScript retourne -1 :

>> [2,4,6,8,1,2,5,4,3,2].findIndex(v => v > 10)  // Nb plus grand que 10 ?
-1

On peut donc déjà écrire cette version finale en JavaScript à tester ici :

const pair = n => n % 2 == 0     // true si n est pair, false sinon

takeWhile = (a, f) => {
  let fin = a.findIndex(v => !f(v));       // On cherche où s'arrêter
  return fin == -1 ? a : a.slice(0, fin);  // Tableau complet ou découpage
};

>> takeWhile([2,4,6,8,1,2,5,4,3,2], pair)
[2, 4, 6, 8]

Le if…else peut être remplacé par l’opérateur ternaire :

condition ? exprSiVrai : exprSiFaux

Dans notre cas, si fin vaut -1, renvoyer le tableau complet sinon faire le découpage. On peut utiliser notre programme avec des fonctions quelconques, par exemple pour trouver le plus long préfixe de carrés (nombres de la forme n * n) dans un tableau :

const carre = n => n == (Math.sqrt(n) | 0) ** 2

>> takeWhile([4,9,36,48,100,121], carre)
[4, 9, 36]

La notation n | 0 est équivalente à un Math.floor(n)

En Python, on peut également rechercher l’index de la première valeur ne vérifiant pas une condition :

>> next(i for i,v in enumerate([2,4,6,8,1,2,5,4,3,2]) if v%2 != 0)
4

Et qui renvoie un message d’erreur lorsque la condition n’est jamais vérifiée :

>> next(i for i,v in enumerate([2,4,6,8]) if v%2 != 0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Pour contourner le problème, on peut utiliser try et except :

def cherche(a):
  try:
   return next(i for i,v in enumerate(a) if v%2 != 0)
  except:
   return -1

>> cherche([2,4,6,8,1,2,5,4,3,2])
4
>> cherche([2,4,6,8])
-1

Ce qui donne ce programme final :

def pair(n): return n % 2 == 0   
  
def takeWhile(a, f):
  try:
    fin = next(i for i,v in enumerate(a) if not(f(v)))
    return a[:fin]
  except:
    return a

>> takeWhile([2,4,6,8,1,2,5,4,3,2], pair)
[2, 4, 6, 8]

>> takeWhile([2,4,8,6], pair)
[2, 4, 8, 6]

Passons à APL en restant dans l’idée de chercher le rang où arrêter la sélection.

      a ← 2 4 6 8 1 2 5 4 3 2   ⍝ Vecteur initial
      0 = 2 | a                 ⍝ Le reste par 2 vaut-il 0 ?
1 1 1 1 0 1 0 1 0 1             ⍝ Vrai pour les 4 premiers puis Faux...
      ∧\ 0 = 2 | a              ⍝ On fait un scan avec la fonction ∧ (ET)
1 1 1 1 0 0 0 0 0 0

Le test 0 = 2 | a peut aussi être remplacé par ~ 2 | (négation) :

      ~ 2 | 2 4 6 8 1 2 5 4 3 2
1 1 1 1 0 1 0 1 0 1

Ensuite la technique est de scanner (de la gauche vers la droite en APL) en utilisant le ET. Le premier terme (celui à gauche) est toujours récupéré puis un ET est exécuté entre les 2 premiers termes : 1 ∧ 1 = 1, ensuite entre les 3 premiers termes etc. Comme il s’agit de la fonction ET, dès qu’un des nombres est nul, l’ensemble sera faux, d’où la suite de 0 à partir du premier nombre impair.

Ce vecteur booléen va servir à sélectionner (filtrer) les éléments à conserver :

      (∧\ 0 = 2 | a) / a
2 4 6 8

Voyons comment généraliser l’écriture à une fonction booléenne quelconque. Créons par exemple isEven qui teste si le reste de la division du nombre par 2 est bien 0 :

isEven ← ~ 2 | ⊢

      isEven 4 5 2 11
1 0 1 0

Pour mettre cette fonction en paramètre dans takeWhile, nous allons écrire son nom en tant que chaine puis utiliser ⍎ pour l’exécuter :

       takeWhile_essai ← {(⍎⍺) ⍵}
      'isEven' takeWhile_essai 4 5 2 11
1 0 1 0

On exécute le terme de gauche (⍎⍺) en l’appliquant au terme de droite ⍵. D’où cette version finale :

      takeWhile ← { ( ∧\ (⍎ ⍺) ⍵ ) / ⍵ }

      'isEven' takeWhile 2 4 6 8 1 2 5 4 3 2
2 4 6 8

Comme me le fait remarquer Conor Hoekstra, expert en APL, on peut aussi utiliser l’écriture :

Lien pour tester directement le code ci-dessous :

takeWhile ← {⍵ /⍨ ∧\ ⍺⍺ ⍵}

      isEven takeWhile 2 4 6 8 1 2 5 4 3 2
2 4 6 8

Le symbole permettant de commuter les arguments et ⍺⍺ pour récupérer la fonction (et donc inutile de la mettre sous forme de chaine)

Modules existants pour Python et JavaScript

On retrouve takeWhile dans certaines bibliothèques Python, comme itertools :

>> from itertools import takewhile

>> list(takewhile(lambda x: x%2 == 0, [2,4,6,8,1,2,5,4,3,2]))
[2,4,6,8]

Ce qui permet de définir notre takeWhile (que nous noterons _takeWhile) par :

from itertools import takewhile

def isEven(n):
  return 0 == n % 2

def _takeWhile(a, f):
  return list(takewhile(f, a))

>> _takeWhile([2,4,6,8,1,2,5,4,3,2], isEven)
[2, 4, 6, 8]

De même on retrouve takewhile en JavaScript, par exemple dans la bibliothèque collect.js.

>> a = collect([2,4,6,8,1,2,5,4,3,2])   // Création d'une collection
Object { items: (10) […] }

>> const isEven = n => 0 == n % 2       // notre fonction booléenne

>> a.takeWhile(isEven)
Object { items: (4) […] }

>> a.takeWhile(isEven).all()
Array(4) [ 2, 4, 6, 8 ]

Ou en une seule ligne :

>> collect([2,4,6,8,1,2,5,4,3,2]).takeWhile(isEven).all()
Array(4) [ 2, 4, 6, 8 ]

Ce qui permet de définir notre fonction _takeWhile :

>> _takeWhile = (a, f) => collect(a).takeWhile(f).all()

>> _takeWhile([2,4,6,8,1,2,5,4,3,2], isEven)
Array(4) [ 2, 4, 6, 8 ]

Troisième exercice Python / JavaScript / APL

Résumé en français : un isogram (En français on parle d’heterogramme) est un mot qui ne contient aucune lettre répétée. Ecrire une fonction qui renvoie vrai ou faux suivant que le mot est un heterogramme, sans tenir compte de la casse (majuscule/minuscule)

Les ensembles

Que ce soit en Python, JavaScript ou APL, on peut créer des ensembles.

Commençons par Python, en plus des listes [ ], tuples ( ) et dictionnaires { }, les ensembles sont des listes non ordonnées sans éléments répétés :

>> set({5, 5, 1, 5, 1})
{1, 5}

>> set('abracadabra')
{'d', 'a', 'b', 'c', 'r'}

En JavaScript, on définit un ensemble de façon assez semblable :

>> new Set([5, 5, 1, 5, 1])
{5, 1}

>> new Set('abracadabra')
{'a', 'b', 'r', 'c', 'd'}

Et en APL le symbole est celui de l’union comme en mathématique :

      ∪ 5 5 1 5 1
5 1
      ∪ 'abracadabra'
abrcd

L’idée principale : On aura un heterogramme si la taille de l’ensemble des lettres utilisées pour écrire le mot est égal à la taille du mot initial.

Python

def isogram(mot):
    return len(mot) == len(set(mot.lower()))

>> isogram('Dermatoglyphics')
True
>> isogram('aba')
False
>> isogram('moOse')
False

Remarquez que le mot a besoin d’être mis en minuscule (ou majuscule) uniquement pour créer l’ensemble des lettres, pas pour trouver sa taille initiale. len permet en Python de donner la taille d’une chaine, d’une liste ou d’un dictionnaire.

javascript

isogram = mot => mot.length == new Set(mot.toLowerCase()).size

length pour la taille d’une chaine ou d’un tableau, size pour les ensembles.

APL

isogram ← ⍴ = (⍴∪∘⎕C)

      isogram'Dermatoglyphics'
1
      isogram'aba'
0
      isogram'moOse'
0

⎕C permet de convertir une chaine en minuscule, on cherche ensuite avec ∪ l’ensemble des lettres utilisées et on calcule sa taille ⍴. Finalement, on teste si elle est égale à celle du mot initial.

Recherche d’au moins une lettre en double

Une autre idée peut être de rechercher s’il existe au moins une lettre en double, par exemple en utilisant 2 boucles, en voici une version générique écrite en JavaScript mais facilement traduisible en Python :

function isogram(mot) {
  mot = mot.toLowerCase();
  for (i = 0; i < mot.length - 1; i++) {
    for (j = i + 1; j < mot.length; j++) {
      if (mot[i] == mot[j]) return false;
    }
  }
  return true;
}

D’autres pistes…

Voici d’autres pistes pour aborder ce problème dans chacun des langages :

javascript

Ecrivons une expression régulière (Regex) qui va tester si une correspondance (match en anglais) existe entre un caractère (noté « . » en Regex) et ce même caractère un peu plus loin.

L’expression régulière est (exemple ici avec le mot couleur):

(.).*\1

le (.) permet de chercher un caractère et de le mémoriser dans un groupe (les parenthèses). Ensuite .* signifie 0 ou plusieurs caractères quelconques et le \1 rappelle le premier groupe, donc pour trouver un éventuel doublon. On ajoute le paramètre i pour que la recherche soit indépendante de la casse :

isogram = mot => !/(.).*\1/i.test(mot.toLowerCase())

>> isogram('Dermatoglyphics')
true

Python

Pour savoir si une lettre est en double, on peut compter (count) combien de fois elle apparait dans le mot. Si toutes (all) les lettres n’apparaissent qu’une seule fois, c’est un heterogramme.

>> 'abracadabra'.count('a')
5

def isogram(mot):
    mot = mot.lower()
    return all(mot.count(c) == 1 for c in mot)

Ou utiliser le module collections qui contient la classe Counter permettant de faire du dénombrement.

>> from collections import Counter
>> Counter('abracadabra')
Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
>> Counter('abracadabra').values()
dict_values([5, 2, 2, 1, 1])

On a alors l’alternative :

from collections import Counter

def isogram(mot):    
 return all(v == 1 for v in Counter(mot.lower()).values())

APL

Le symbole ⌸ (key) permet de créer des sortes de tableaux croisés dynamiques comme sur Excel. Par exemple :

      {⍺,⍵}⌸ 'abracadabra'
a 1  4 6 8 11
b 2  9       
r 3 10       
c 5          
d 7 

Pour chaque élément distinct ⍺ de la chaine (c’est-à-dire les lettres a, b, r, c et d), il affiche les positions trouvées ⍵. La lettre ‘a’ a été vue aux positions 1, 4, 6… On peut lui demander de n’afficher que le nombre de positions trouvées, avec ou sans préciser ⍺ :

      {⍺,≢⍵}⌸ 'abracadabra'
a 5
b 2
r 2
c 1
d 1

      {≢⍵}⌸ 'abracadabra'       ⍝ ou encore ⊢∘≢⌸ 'abracadabra'
5 2 2 1 1

Si c’est un heterogramme, toutes ces valeurs doivent être égales à 1 :

      {≢⍵}⌸ ⎕C 'Dermatoglyphics'
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1

On va comparer tous les éléments avec 1 puis faire une réduction avec un ET. Ce qui signifie : Est-ce que le premier élément vaut 1 ET est-ce que le second terme vaut 1 etc. Pour cela 2 notations :

      ^/ 1 = ∪{≢⍵}⌸ ⎕C 'aba'      ⍝ Test 1 = ... puis réduction ^/
0

      1 ^.= ∪{≢⍵}⌸ ⎕C 'aba'       ⍝ Ecriture équivalente avec 1 ^.=
0
      1 ^.= ∪{≢⍵}⌸ ⎕C 'Dermatoglyphics'
1

Ce qui donne finalement :

      isogram ← 1 ^.= ∪{≢⍵}⌸ ⎕C

      isogram 'aba'
0
      isogram 'Dermatoglyphics'
1

⍝ ou encore :

      isogram ← 1 ^.= (∪(⊢∘≢⌸ ⎕C))

Pour finir, autre version qui signifie littéralement « mettre le mot en minuscule, compter le nombre de d’occurrences de chaque lettre (sans répétition), prendre la plus grande de ces valeurs et tester si c’est 1« ), limpide diront certains 😅

isogram ← 1 = (⌈/ (⊢∘≢ ⌸ ⎕C))

Deuxième exercice Python / JavaScript / APL

Résumé en français : On vous donne 3 nombres différents dans un ordre quelconque. En sortie, donnez le rang du nombre qui est entre les 2 autres. Par exemple avec 2, 3, 1 c’est le chiffre 2 qui est entre 1 et 3, son rang dans 2, 3, 1 est 0.

Partons d’un tri

Une première idée est de trier les 3 éléments (par ordre croissant ou décroissant, peu importe), de chercher quelle valeur est au milieu et enfin de récupérer le rang de cette valeur dans le tableau initial. Par exemple avec 2, 3 et 1, le tri donne 1, 2, 3 ce qui permet de récupérer la valeur centrale « 2 » et donc son rang 0 dans le tableau initial.

PythonJavaScriptAPL
sorted et sortsort
Comment trier en…

En Python, la fonction sorted tri par défaut un tableau numérique par ordre croissant :

>> sorted([21, 3, 1])
[1, 3, 21]

On peut également utiliser la méthode sort :

>> a = [21, 3, 1]
>> a.sort()
>> a
[1, 3, 21]

En JavaScript, la méthode sort fait un tri alphabétique, si bien que :

>> [21, 3, 1].sort()
[1, 21, 3]

En effet, dans un dictionnaire, le « mot » 21 est avant le « mot » 3… C’est pourquoi on utilise très souvent une fonction de comparaison. Voici celle pour obtenir un tri croissant :

>> [21, 3, 1].sort((a, b) => a - b)
[1, 3, 21]

Lorsque la différence a – b est négative, a sera placé avant b, si la différence est positive a sera placé après b sinon les 2 éléments restent inchangés.

En APL, autre vision du tri !

      ⍋ 21 3 1
3 2 1

La réponse 3 2 1 signifie que l’élément qui doit être en premier est à la 3e position (c’est le 1 du vecteur initial) ensuite le second est à la 2e position (le 3 du vecteur initial) et enfin le dernier élément est à la 1ere position (le 21).

Remarquez que l’on a la réponse à l’exercice, le 2e élément de ⍋ est la position recherchée. On visualise le 0 en 2e position :

      ⎕IO ← 0          ⍝ On impose que l'origine des indices soit 0
      ⍋ 2 3 1
2 0 1

Voici des versions Python / JavaScript et APL en utilisant les tris :

Python

def milieu(triplet):
    return triplet.index(sorted(triplet)[1])

>> milieu([2, 3, 1])
0
>> milieu([5, 10, 14])
1

Javascript

milieu = triplet => triplet.indexOf([...triplet].sort((a, b) => a - b)[1])

>> milieu([2, 3, 1])
0
>> milieu([5, 10, 14])
1

[…triplet] permet de faire une copie du tableau initial.

APL

      ⎕IO ← 0
      milieu ← {(⍋⍵)[1]}

      milieu 2 3 1
0
      milieu 5 10 14
1

Autre écriture possible, le symbole ⊃ (pick) utilisé seul (monadique) permet de récupérer le 1er élément d’un vecteur (ou matrice) et sous sa forme dyadique le n-ième terme :

      ⊃ 'bonjour'          ⍝ Ecriture monadique = 1er élément
b
      3 ⊃ 'bonjour'        ⍝ Ecriture dyadique
n
      ⊃ 4 6 8              ⍝ 1er élément
4
      3 ⊃ 4 6 8            ⍝ 3e élément
8

Ce qui permet d’obtenir cette version :

      ⎕IO ← 0
      milieu ← 2 ⊃ ⍋

      milieu 2 3 1
0

Si certain.e.s parmi vous aiment les choses concises, APL ne devrait pas vous déplaire de ce point de vue !

Et si on cherchait un invariant ?

Remarquons que si l’on trouve les positions de la plus grande valeur (max) et de la plus petite valeur (min) du triplet, la position restante sera celle que l’on cherche.

Mais l’ensemble des positions sera toujours { 0, 1, 2 }. Par exemple si le max est en 2e et le min en 1er, alors le milieu est en position 0. L’invariant intéressant ici est la somme des positions qui vaut toujours 0+1+2=3. Le rang que l’on cherche est donc :
rang_cherché = 3 – rang(max) – rang(min)

Python

def milieu(triplet):
    return 3 - triplet.index(max(triplet)) \
             - triplet.index(min(triplet))

javascript

milieu = triplet => 3 - triplet.indexOf(Math.max(...triplet))
                      - triplet.indexOf(Math.min(...triplet))

Observez la différence en JavaScript entre :

>> Math.max(2,5,8,1,10,3)
10
>> Math.max([2,5,8,1,10,3])
NaN
>> Math.max(...[2,5,8,1,10,3])
10

APL

En APL, on trouve les sommes, max, min des éléments d’un vecteur (ou matrice) en utilisant une réduction :

      +/ 2 3 1        ⍝ Somme des termes
6
      ⌈/ 2 3 1        ⍝ maximum
3
      ⌊/ 2 3 1        ⍝ minimum
1

Et pour trouver le rang d’un élément dans un vecteur, on utilise ⍳ (iota) :

      ⎕IO ← 1
      5 7 3 1 ⍳ 7     ⍝ Position de 7 dans le vecteur 5 7 3 1 ?  
2

Toujours avec le principe de l’invariant, somme – max – min donne le nombre qui est au milieu, celui dont on doit chercher le rang. On obtient finalement ce programme que vous pouvez tester directement ici :

⎕IO ← 0
milieu ← ⊢ ⍳ +/ - ⌈/ + ⌊/

      milieu 2 3 1
0
      milieu 5 10 14
1

Le ⊢ permet de récupérer le paramètre qui est à droite (équivalent de {⍵}) et je rappelle qu’APL fait les calculs de la droite vers la gauche, donc le minimum ⌊/ puis max + min ⌈/+⌊/ la soustraction va correspondre à – max – min et on ajoute la somme.

Petits exercices de programmation (niveau Lycée) en Python, JavaScript ou… APL

Page Twitter où j’ai mis quelques challenges que je vous propose de résoudre en Python, Javascript ou APL.


Python est le langage officiel enseigné dans les lycées français, donc pas le choix 😅
Lien pour taper en Python

JavaScript est un langage permettant de faire des interactions avec une page web. Indispensable si vous voulez devenir développeur web 🤓

APL n’est rien de tout ça ! Peu ou pas connu des informaticiens, critiqué pour son aspect ésotérique, il existe et résiste depuis plus de 50 ans (60 ans de façon théorique) ! 🏆
Ce langage développe une certaine façon de penser et vous donnera souvent des idées que vous pourrez ensuite réinterpréter en Python ou JavaScript. Son apprentissage n’est pas aisé mais sa beauté compense tous ses vilains défauts 🥰
Lien pour taper en APL

Les énoncés et corrigés sont aussi disponibles en version Notebook

Premier exercice (tiré de codewars.com)

Résumé en français : 2 paramètres entiers vous sont donnés, par exemple 1 et 9, le premier étant toujours inférieur au second (mais ils peuvent être négatifs). Vous devez trouver combien de nombres sont entre les 2 sachant qu’ils ne doivent pas contenir de ‘5’.

Python & Javascript

Testez cette version en ligne ici :

def dont_give_me_five(debut, fin):
 compteur = 0
 for v in range(debut, fin+1):
  if '5' not in str(v):
   compteur += 1
 return compteur

Cette version a le mérite d’être simple à comprendre. On initialise un compteur à zéro et on parcourt la liste complète des nombres entre debut et fin (avec un +1 pour inclure la valeur fin).
Pour savoir si 5 est ou n’est pas dans le nombre, on transforme le nombre v en chaine de caractères (str) et on teste si le caractère ‘5’ n’est pas dans cette chaine. C’est beaucoup plus facile que de le faire avec des formules mathématiques !
Si c’est vrai que ‘5’ n’est pas dans la chaine, le compteur augmente de 1.

Son équivalent en JavaScript que vous pouvez tester ici :

function dont_give_me_five(debut, fin) {
    let compteur = 0;
    for (let i = debut; i <= fin; i++)
        if (!i.toString().includes("5"))
            compteur++;
    return compteur;
}

En JavaScript, le « ! » signifie « négation » et includes « contient ». On peut aussi utiliser une expression régulière qui testera (test) s’il y a un 5 et qui au passage convertira le paramètre (ici le nombre i) en chaine :

function dont_give_me_five(debut, fin) {
    let compteur = 0;
    for (let i = debut; i <= fin; i++)
        if (!/5/.test(i))
            compteur++;
    return compteur;
}

APL

Vous pourrez tester les commandes ci-dessous en allant sur le site https://tryapl.org/

Si vous n’avez jamais entendu parler d’APL, j’ai fait 3 petites vidéos d’introduction à son sujet, la première est ici :

APL permet de générer facilement la liste des nombres de 1 à N ≥ 0. Par exemple :

      ⍳6       ⍝ le symbole ⍳ se lit 'iota'
1 2 3 4 5 6

Pour que la liste démarre à 0 (ce qui est le cas des positions dans les tableaux en Python ou JavaScript), on peut l’imposer :

      ⎕IO ← 0
      ⍳6
0 1 2 3 4 5

Si maintenant on teste 17+4-1 et 17-1+4, on s’attend à obtenir la même chose… Et bien non !

      17+4-1
20
      17-1+4
12

La logique d’APL est de faire les calculs de la droite vers la gauche, donc pour la première écriture on aura -1 puis 4-1=3 puis +3=3 et enfin17+3=20.
Pour la seconde écriture, 4 puis 1+4=5 puis -5 puis 17-5=12.
Il ne faut donc pas confondre -1+4 et ¯1+4 (il y a un symbole spécial pour les nombres négatifs)

Pour avoir la liste des nombres entre 4 et 17 :

      4 + ⍳17+1-4
4 5 6 7 8 9 10 11 12 13 14 15 16 17
      4 {⍺ + ⍳⍵+1-⍺} 17
4 5 6 7 8 9 10 11 12 13 14 15 16 17

Le symbole ⍺ correspond au paramètre qui est à gauche et ⍵ à celui de droite.

Transformons chacun de ces nombres en chaines avec ⍕ et ¨ (pour chaque)

      ⍕¨4 {⍺ + ⍳⍵+1-⍺} 17             ⍝ Nombres transformés en chaines
┌─┬─┬─┬─┬─┬─┬──┬──┬──┬──┬──┬──┬──┬──┐
│4│5│6│7│8│9│10│11│12│13│14│15│16│17│
└─┴─┴─┴─┴─┴─┴──┴──┴──┴──┴──┴──┴──┴──┘
      ⍴∘⍕¨4 {⍺ + ⍳⍵+1-⍺} 17           ⍝ Longueurs des chaines
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│1│1│1│1│1│1│2│2│2│2│2│2│2│2│
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘

Cherchons maintenant si le caractère ‘5’ est dans ces chaines, pour cela on va composer ∊ (test d’appartenance) avec ⍕ :

      '5' ∊∘⍕¨ 4 + ⍳14
0 1 0 0 0 0 0 0 0 0 0 1 0 0

et en ajoutant la négation ~ :

     ~ '5' ∊∘⍕¨ 4 + ⍳14
1 0 1 1 1 1 1 1 1 1 1 0 1 1

Il ne reste plus qu’à compter le nombre de 1 en utilisant une réduction :

    +/ ~ '5' ∊∘⍕¨ 4 + ⍳14
12

Ce qui donne comme programme final que vous pouvez lancer directement ici :

      dont_give_me_five ← {+/ ~ '5' ∊∘⍕¨ ⍺ + ⍳⍵+1-⍺}

      
      4 dont_give_me_five 17
12
      ¯17 dont_give_me_five ¯4
12

Autres versions en python/javascript

Notre version APL propose de créer une liste de booléens suivant que ‘5’ est où non dans la chaine puis d’en faire la réduction par la somme. Or en Python :

> True + True        # 1 + 1
2
> True + False       # 1 + 0 
1

D’où cette seconde version :

def dont_give_me_five(debut, fin):
    return sum(['5' not in str(i) for i in range(debut, fin + 1)])

# qui peut même s'écrire sans les crochets :

def dont_give_me_five(debut, fin):
    return sum('5' not in str(i) for i in range(debut, fin + 1))

Pour terminer, voyons comment utiliser reduce en JavaScript afin d’ajouter +1 (ou + true) à chaque fois que le nombre convient. Pour cela il faut partir d’un tableau de taille fin – debut + 1, peut importe son contenu :

>> [...Array(17 - 4 + 1)]
(14) [undefined, undefined, ..., undefined]

reduce utilise un accumulateur ayant une valeur initiale (pour nous 0) et au fur et à mesure cet accumulateur va pouvoir changer. Ci-dessous 3 paramètres ont être ajoutés, l’accumulateur a, la valeur v et le rang r. Ainsi, en parcourant les éléments d’un tableau, on peut récupérer leurs valeurs et leurs rangs. Le 0 après la virgule est pour l’initialisation de l’accumulateur.

[...Array(17 - 4 + 1)].reduce((a,v,r) =>    , 0)

Vous devriez comprendre l’idée de cette version :

dontGiveMeFive = (debut,fin) => [...Array(fin - debut + 1)]
     .reduce(
       (a, _ ,r) => a + !/5/.test(r + debut)
     ,0)

Concours MPO105

C.Ret a organisé un petit MPO (Misez p’tit Optimisez) dont vous trouverez l’énoncé ici.

Il fallait déjà remarquer que :

eq1

La difficulté n’était donc pas tant au niveau de la programmation mais plutôt de trouver une machine ancienne, avec peu de pas de programme, suffisamment rapide et de créer un programme court avec un minimum de variables…

Voici les explications concernant le programme que j’ai proposé pour la calculatrice Radio Shack EC-4001 (Clone américain du Sinclair Cambridge Programmable de 1977) :

Le programme ci-dessous calcule la racine carrée du nombre voulu (4, 64, 169…) moins un.

1 F 3 1 -

Etapes en vidéo pour entrer le programme avec l’émulateur.

Signification :

  • 1 → Touche racine carrée (en mode programmation, les touches correspondent aux fonctions (7 → sin, 8 → cos etc.)
  • F → Moins (on voit le F en bleu sur le clavier)
  • 3 → # pour préciser que l’on va entrer un nombre
  • 1 → Le chiffre 1 (il faut donc 2 pas pour entrer ‘1’ )
  • – → « = » (le « – » en bleu, à ne pas confondre avec la touche « – »  !)

Erreur de 1/1000 pour la valeur 26553409

5152

J’ai également proposé une autre solution en utilisant une calculatrice financière non programmable en remarquant que la formule pour les intérêts composés est :

eq2

Donc, en prenant n = 2 et PV = 1, on obtient :

CodeCogsEqn

qui est exactement la formule que l’on cherche à programmer. Voici ce que cela donne par exemple sur une HP 10bII une fois que l’on a initialisé la machine par C ALL (Effacer les mémoires), 2 n, 1 +/- PV. Remarquez que les résultats sont multipliés par 100 car la machine affiche la valeur finale en %. Ainsi 18 100 correspond par exemple à 181.

Recherche de calculatrices

Bonjour à tous,

Comme vous avez pu le constater je fais régulièrement des vidéos sur des calculatrices anciennes (généralement des programmables). Si vous en avez à me proposer (en don ou à me vendre), n’hésitez pas à me contacter à cette adresse : schraf @ univ-angers . fr

Un petit aperçu de ma collection de Texas Instrument :

Collection

ti2

Mes énigmes Facebook

J’ai posté plusieurs énigmes sur le groupe Facebook Casse-tête, énigmes et paradoxes, suite aux remarques des membres, j’ai modifié quelques énoncés et vous en propose ici une version en PDF imprimable.

Vous trouverez une partie des solutions expliquées en vidéo ici :

Mes 6 énigmes proposées sur un groupe Facebook (Partie 1)
6 nouvelles énigmes proposées sur un groupe Facebook (Partie 2)
Petites énigmes mathématiques à programmer (avec solutions)