Neuvième exercice en Python, JavaScript et APL

Résumé en français : Des lapins naissent et deviennent matures au bout de 1 mois, âge auquel ils pourront se reproduire.
Créez une fonction qui détermine le nombre de paires de lapins matures après n mois en commençant par un unique couple de lapins immatures et qui se reproduisent à raison de b paires à la fin de chaque mois. Voir le tableau ci-dessus dans le cas de n = 5 mois avec un taux de reproduction de b = 3 pour bien comprendre le déroulement. Quelques autres exemples :

>> lapins(0, 4)
0                     # Après 0 mois, il n'y a pas de paire adultes
>> lapins(1, 4)
1                     # Après 1 mois, une seule paire d'adultes
>>lapins(4, 0)
1                     # Lapins stériles (taux = 0), on reste à 1
>> lapins(6, 3)       
40 
>> lapins(8, 12)
8425
>> lapins(7, 4)
181 

# (1 0) > (0 1) > (4 1) > (4 5) > (20 9) > (36 29) > (116 65) > 181

Cet exercice étant assez facile, proposons différentes versions :

Une simple boucle

On part de 0 adulte et d’une paire de lapins immatures. Chaque mois, le nouveau nombre d’immatures est égal au nombre d’adultes qui se reproduisent avec un taux b (c’est-à-dire la multiplication du nombre d’adultes par le coefficient b). Et le nouveau nombre d’adultes est égal au nombre d’adultes précédents + les immatures précédents qui deviennent adultes.

Il faut faire attention à l’écriture du processus, voici une version INCORRECTE :

def lapins(n, b):
    immatures, adultes = 1, 0
    for i in range(n):
        immatures = b * adultes         # on écrase immatures trop tôt
        adultes =  adultes + immatures
    return adultes

En effet, on commence par mettre à jour la variable immatures puis on utilise cette valeur à la ligne suivante, or nous avions besoin de la valeur précédente de immatures et non pas de la valeur actualisée. Une technique classique est d’utiliser une variable temporaire :

def lapins(n, b):
    immatures, adultes = 1, 0
    for i in range(n):
        temp = immatures            # On mémorise valeur précédente
        immatures = b * adultes
        adultes =  adultes + temp   # que l'on utilise ici
    return adultes

Mais en Python ou en JavaScript, on peut également utiliser cette écriture CORRECTE :

python

def lapins(n, b):
    immatures, adultes = 1, 0
    for i in range(n):
        immatures, adultes = b * adultes, adultes + immatures
    return adultes

>> lapins(8,12)
8425
>> lapins(0,4)
0
>> lapins(4,0)
1

javascript

La traduction est quasi identique en JavaScript :

 const lapins = (n, b) => {
  let [immatures, adultes] = [1, 0];
  for (let i = 0; i < n; i++) {
    [immatures, adultes] = [adultes * b, immatures + adultes];
  }
  return adultes;
}

Avec d’infinies variations, par exemple en utilisant un objet et une boucle Tant que :

const lapins = (n, b) => {
  const tous = {immatures: 1, adultes: 0};    // Tous les lapins
  mois = 0;
  while (mois < n) {
    let temp = tous.adultes;
    tous.adultes += tous.immatures;
    tous.immatures = temp * b;
    mois++;
  }  
  return tous.adultes;
}

Version récursive

Reprenons l’exemple proposé dans l’énoncé avec n = 5 et b = 3. Le nombre de paires d’adultes matures est successivement : 0 → 1 → 1 → 4 → 7 → 19

Si on note u la suite donnant le nombre d’adultes au fil des mois, on a u(0) = 0, u(1) = 1 etc.

Cette suite peut être définie par une relation de récurrence :

Ce qui signifie que le nombre de paires d’adultes au mois n+1 est 3 fois le nombre de paires d’adultes 2 mois avant (reproduction) + les immatures du mois précédent qui deviennent adultes.

Essayez en effet de vous convaincre que 0 → 1 → 14719 correspond à :

u(2) = 1 = 3 * 0 + 1, u(3) = 4 = 3 * 1 + 1, u(4) = 7 = 3 * 1 + 4, u(5) = 19 = 3 * 4 + 7 🤔

Les traductions en Python et JavaScript sont immédiates :

Python

def lapins(n, b):
  if n <= 1: return n
  return b * lapins(n - 2, b) + lapins(n - 1, b)

JavaScript

const lapins = (n, b) => n <= 1 ? 
                         n : 
                         b * lapins(n - 2, b) + lapins(n - 1, b)

>> lapins(5,3)
19
>> lapins(8,12)
8425

Version matricielle

Autre vision en utilisant cette fois-ci un calcul matriciel. En effet, observons que :

On peut donc calculer u(n+1) et u(n) à l’aide des puissances d’une unique matrice 2 x 2 :

De façon plus générale, le nombre de paires d’adultes pour un taux de reproduction b peut être calculé par :

Python

Pour calculer la puissance d’une matrice 2 x 2, nous pouvons créer une fonction de multiplication ou utiliser une bibliothèque comme numpy :

import numpy as np                 # On importe numpy

m = np.array([[1, 3], [1, 0]])     # Création de la matrice m

for k in range(5):                 # Calcul de m^0, m^1... m^4
  print('m^{} = {}'.format(k, np.linalg.matrix_power(m, k)))

Résultats

m^0 = [[1 0], [0 1]]
m^1 = [[1 3], [1 0]]
m^2 = [[4 3], [1 3]]
m^3 = [[ 7 12], [ 4  3]]
m^4 = [[19 21], [ 7 12]]

Attention cependant, la bibliothèque numpy ne gère pas (par défaut) les entiers arbitrairement grands et peut donner des résultats FAUX :

>> np.linalg.matrix_power(m, 50)      # m à la puissance 50

[[ 827677709047869124 1078278355240672323]
 [ 359426118413557441  468251590634311683]]    # Résultat EXACT

>> np.linalg.matrix_power(m, 70)      # m à la puissance 70
[[   23110438186405259  6654818438609454037]
 [-3930641878366699193  3953752316553104452]]  # Résultat FAUX !

On peut malgré tout préciser comment les données doivent être stockées (entier, flottant, objet) :

import numpy as np

m = np.array([[1, 3], [1, 0]], dtype=object)  # en tant qu'objet

>> np.linalg.matrix_power(m, 70)
[[14551031556125490725277067 18956729415189764489429973]
 [6318909805063254829809991 8232121751062235895467076]]   # EXACT !

Programme final en Python :

import numpy as np

def lapins(n, b):
    m = np.array([[1, b], [1, 0]], dtype=object)
    return np.linalg.matrix_power(m, n)[1][0]    # on récupère u(n)

>> lapins(8, 12)
8425
>> lapins(0, 4)
0
>> lapins(4, 0)
1

Javascript

Il existe des bibliothèques JavaScript pour faire du calcul matriciel, par exemple math.js :

>> m = [[1, 3], [1, 0]]

>> math.pow(m,4)
0: [19, 21]
1: [7, 12]

>> math.pow(m,70)
0: [1.455103155612549e+25, 1.8956729415189765e+25]
1: [6.318909805063255e+24, 8.232121751062236e+24]

Sinon, vous pouvez vous créer une petite fonction permettant de mettre une matrice 2 x 2 à une puissance quelconque, par exemple :

const power = (m, n) =>      // Matrice m à la puissance n
{
    [[a, b], [c, d]] = m;    // On récupère les coefficients
    r = [[1, 0], [0, 1]]     // Matrice unité
    for (i = 0; i < n; i++)  // n multiplications matricielles
    {
      [e, f] = [a * r[0][0] + c * r[0][1], b * r[0][0] + d * r[0][1]];
      [g, h] = [a * r[1][0] + c * r[1][1], b * r[1][0] + d * r[1][1]];
      r = [[e, f], [g, h]];
    };
    return r
}

>> power([[1, 3],[1, 0]], 4)
0: [19, 21]
1: [7, 12]

>> power([[1, 3],[1, 0]], 70)
0: [1.455103155612549e+25, 1.8956729415189765e+25]
1: [6.318909805063255e+24, 8.232121751062236e+24]

Programme final en JavaScript qui utilise la fonction power ci-dessus :

lapins = (n, b) =>  power([[1, b],[1, 0]], n)[1][0]

>> lapins(8, 12)
8425

Version APL

Reprenons l’idée du calcul matriciel. En APL, on définit une matrice en précisant ses dimensions suivi de ⍴ et des valeurs :

      m ← 2 2 ⍴ 1 3 1 0     ⍝  Matrice 2 x 2
1 3
1 0

Pour la mettre au carré on utilise le produit interne (ici somme des produits) :

      m +.× m
4 3
1 3

Et avec une réduction on peut obtenir par exemple m^4 :

      +.×/ m m m m
┌─────┐
│19 21│
│ 7 12│
└─────┘

Pour généraliser à une puissance quelconque r, il va falloir dupliquer la matrice r fois, exemple avec r = 4 :

      4 ⍴ ⊂ m
┌───┬───┬───┬───┐
│1 3│1 3│1 3│1 3│
│1 0│1 0│1 0│1 0│
└───┴───┴───┴───┘

D’où cette fonction pour mettre une matrice m à la puissance r :

      power ← {+.×/ ⍺ ⍴ ⊂ ⍵}

      4 power m
┌─────┐
│19 21│
│ 7 12│
└─────┘
      1 power m
┌───┐
│1 3│
│1 0│
└───┘
      0 power m
DOMAIN ERROR

Cette alternative suggérée par Dyalog APL sur Twitter a l’avantage de gérer le cas r = 0 :

      power ← {+.× ⍣ (⍺ - 1) ⍨ ⍵}

      4 power m
19 21
 7 12
      1 power m
1 3
1 0
      0 power m      ⍝ On obtient bien la matrice identité
1 0
0 1

⍣ permet de répéter une opération plusieurs fois et ⍨ d’inverser les paramètres :

      ×⍣3 ⍨ 2       ⍝ 2 → 2 × 2 = 4 → 4 × 4 = 16
16
      +⍣3 ⍨ 2       ⍝ 2 → 2 + 2 = 4 → 4 + 4 = 8
8
      +∘÷⍣ 20 ⍨1    ⍝ 1 + 1 / (1 + 1 / (1 + ...)) ≃ Nombre d'or
1.618033985

Une fois que l’on aura calculé la puissance de la matrice, il faut récupérer l’élément à la ligne 2 et colonne 1. Pour cela, on utilise ⊃ (pick) et on accole (laminate) les lignes de la matrice :

      m ← 2 2  ⍴ 7 9 5 8     ⍝ Matrice 2 x 2
      ,/ m                   ⍝ Laminage (on accole les lignes)
┌───┬───┐
│7 9│5 8│
└───┴───┘
      2 ⊃ ,/ m               ⍝ 2e terme
5 8
      2 1 ⊃ ,/ m             ⍝ 1er terme du 2e terme
5

⍝ On peut également utiliser ⌷ (index)

      2 1 ⌷ m
5

Programme final en APL :

      lapins ←  {2 1 ⌷ +.× ⍣ (⍺ - 1) ⍨ 2 2 ⍴ 1 ⍵ 1 0} 

      8 lapins 12
8425
      0 lapins 4
0
      4 lapins 0
1
      7 lapins 4
181