Files
TermNSI/Recursivité/evaluation_recursivité.ipynb
T
2024-10-07 08:08:10 +02:00

13 KiB

Exercices sur la récursivité et la programmation dynamique

Évolution des abonnements

Un youtuber constate chaque mois qu'il recrute 3000 nouveaux abonnés mais dans le même temps la lassitude pousse 2% de ses fidèles à se désabonner. Ce mois-ci (n=0) il compte 100000 abonnés et il souhaite pouvoir calculer les prédictions de ses abonnés pour démarcher des sponsors dans les mois à venir. Écrivez récursivement la fonction prev_abo qui calcule le nombre d'abonné au mois n.

Utilisez votre fonction prev_abo pour calculer le nombre d'abonnés dans les mois à venir.

D'après vous, le youtuber peut-il prétendre à ces sponsors qu'il va bientôt atteindre les 200000 abonnés ? Utilisez votre programme pour constater l'évolution des abonnés à long terme.

In [ ]:

Sans maitrise la puissance n'est rien

Écrivez par récurence une function puissance(x: float, n: int) -> float qui calcule x^n pour n >= 0 en utilisant uniquement des multiplications et la valeur de x^0.

In [ ]:
def puissance(x:float, n:int) -> float:
    """ n must be >= 0
    >>> puissance(0,0)
    1
    >>> puissance(1,0)
    1
    >>> puissance(1,1)
    1
    >>> puissance(1,3)
    1
    >>> puissance(2,3)
    8
    >>> puissance(10,5)
    100000
    """

import doctest
doctest.testmod(optionflags=doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE, verbose = False)

Un PGCD pour l'examen ?

Écrivez une fonction pgcd qui calcule le PGCD entre 2 nombres entiers avec l'algorithme d'Euclide qui dit ceci :

  • Le pgcd de a (a > 0) et b est le même que le pgcd de b et c avec c qui est le reste de la division entière (en python le modulo :%) de a par b.
  • Le pgcd de a par 0 est a
In [ ]:

Réduction de fraction

Utiliser le pgcd précédent pour écrire une fonction reduc qui réduit les fractions entières comme ceci : reduc(12,8) retourne (3,2) car \dfrac{12}{8} = \dfrac{3}{2}.

In [ ]:

Suite de Syracuse

Programmez par récursivité la fonction syracuse(u0: int, n: int) qui calcule la suite de Syracuse définie pour des entiers comme ceci :

u{n+1} = \begin{cases} U_n / 2 & \text{si } U_n \text{ est pair} \ 3U_n + 1 & \text{sinon} \end{cases}

💡 Si vous appelez récursivement plusieurs fois la fonction avec les mêmes paramètres, il vaut mieux faire 1 seul appel et mettre le résultat dans une variable. Vous limiterez ainsi considérablement le nombre d'appels.

In [ ]:
def syracuse(u0: int, n: int) -> int:
    """ Return Syracuse suite value
    u0 must be > 0 ; n must be >= 0
    >>> syracuse(1, 0)
    1
    >>> syracuse(15, 7)
    160
    >>> syracuse(15, 4)
    35
    >>> syracuse(15, 17)
    1
    """

import doctest
doctest.testmod(optionflags=doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE, verbose = False)

Pour l'appel de la fonction syracuse(15, 3) comptez au brouillon combien de fois la fonction syracuse se lance en écrivant l'arbre des appels.

Vérifiez cela en modifiant votre programme, puis trouvez le nombre d'appel pour calculer syracuse(15, 5), syracuse(15, 10), syracuse(15, 15) et syracuse(15, 20). Que pensez vous de ces derniers résultats ?

In [ ]:

Une conjecture encore non démontrée en mathématique affirme que pour tout N entier > 0 il existe toujours un terme de la suite pour lequel $u_n = 1$.

Écrivez un programme qui montre cette conjecture pour les valeurs de N allant jusqu'à 30.

In [ ]:

A ce stade, suivant le code de votre fonction syracuse, tout se passe bien (1 seul appel récursif par fonction), ou vous avez dû interrompre le programme car sa compléxité est telle qu'il ne peut pas répondre dans un temps raisonnable (3 appels récursifs par fonction) !

Écrivez maintenant votre programme de recherche du rang pour lequel u_n=1 pour un N donné en utilisant un algorithme itératif (avec une boucle). Testez votre code en trouvant les rangs recherchés pour N jusqu'à 100.

In [ ]:

Recherche dichotomique récursive !

Écrivez maintenant une fonction find_dicho_recur de recherche dichotomique dans une liste triée qui répond True ou False en fonction du résultat de la recherche. Vous pouvez utiliser les slices pour accéder à une sous-liste.

💡 La technique qui consiste à résoudre un problème en résolvant plusieurs parties plus petite du problème s'appelle : diviser pour régner.

In [ ]:
def find_dicho_recur(liste: list, value: int) -> bool:
    """ liste must be sorted
    >>> find_dicho_recur([], 0)
    False
    >>> find_dicho_recur([1], 0)
    False
    >>> find_dicho_recur([1], 1)
    True
    >>> find_dicho_recur([1, 2], 1)
    True
    >>> find_dicho_recur([1, 2], 2)
    True
    >>> find_dicho_recur([1, 2], 3)
    False
    >>> find_dicho_recur([1, 2], -1)
    False
    >>> find_dicho_recur([1,2,3], 0)
    False
    >>> find_dicho_recur([1,2,3], 5)
    False
    >>> find_dicho_recur([1,2,3], 1)
    True
    >>> find_dicho_recur([1,2,3], 2)
    True
    >>> find_dicho_recur([1,2,3], 3)
    True
    >>> find_dicho_recur([1,2,3,4], 1)
    True
    >>> find_dicho_recur([1,2,3,4], 2)
    True
    >>> find_dicho_recur([1,2,3,4], 3)
    True
    >>> find_dicho_recur([1,2,3,4], 4)
    True
    >>> find_dicho_recur([1,2,3,4], -1)
    False
    >>> find_dicho_recur([1,2,3,4], 10)
    False
    """

import doctest
doctest.testmod(optionflags=doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE, verbose = False)

Rendu de monnaie optimal !

Nous avons programmé le rendu de monnaie avec un algorithme glouton qui est optimal pour les systèmes de monnaies cannoniques (comme l'euro).

Mais si nous disposons des pièces : 4, 3 et 1, pour un rendu de 6, l'algorithme glouton rendra 3 pièces (4+1+1) alors qu'une solution en 2 pièces existe (3+3).

Nous allons programmer une fonction rendu_monnaie récursive pour trouver la solution optimale et explorant toutes les possibilités.

L'idée est de lancer une recherche récursive en choisissant 1 pièce et de garder seulement la solution qui demande le moins de pièces pour la somme restante. Voici un exemple avec [4,3,1] et 8 :

  • Je vais utiliser 1 pièce à tour de rôle : 4 puis je lance récursivement ma fonction avec [4,3,1] et 8-4 = 4. Puis je fais pareil avec les pièces 3 puis 1.
  • Je sais ainsi que le nombre de pièces total pour chaque possibilité est le résultat de l'appel récursif + 1. Il me suffit donc de choisir la branche qui retourne le moins de pièces.

💡 Et comme le programme est récursif toutes les possibilités seront automatiquement explorée pour donner une solution optimale.

In [ ]:
def rendu_monnaie(pieces: list, somme: int) -> list:
    """ Return optimal coin's count for money back
        Return None if no solution exists
        pieces : list of int
        >>> rendu_monnaie([], 5) is None
        True
        >>> rendu_monnaie([4,3], 1) is None
        True
        >>> rendu_monnaie([4,3], -1) is None
        True
        >>> rendu_monnaie([4,3], 5) is None
        True
        >>> rendu_monnaie([4,3], 6)
        [3, 3]
        >>> rendu_monnaie([4,3], 8)
        [4, 4]
        >>> rendu_monnaie([4,3,1], 6)
        [3, 3]
        >>> rendu_monnaie([4,3,1], 5)
        [4, 1]        
    """

import doctest
doctest.testmod(optionflags=doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE, verbose = False)

Rendu de monnaie optimal et dynamique !

La solution ci-dessus qui explore toutes les possibilités récursivement est séduisante mais si vous lancez rendu_monnaie([4,3,1], 25) avec l'affichage des appels vous allez constater que le nombre d'appels est considérable. Ce qui explique pourquoi rendu_monnaie([4,3,1], 53) a du mal à répondre dans un temps raisonnable...

Nous allons donc améliorer notre algorithme en écrivant une fonction rendu_dyn avec la technique de la programmation dynamique en mémorisant les solutions déjà calculées pour les réutiliser au cours du calcul.

In [ ]:
def rendu_dyn(pieces: list, somme: int) -> list:
    """ Return optimal coin's count for money back
        Return [] if no solution exists
        pieces : list of POSITIVE int    
        >>> rendu_dyn([], 5)
        []
        >>> rendu_dyn([4,3], 1)
        []
        >>> rendu_dyn([4,3], 0)
        []
        >>> rendu_dyn([4,3], 5)
        []
        >>> rendu_dyn([4,3], 6)
        [3, 3]
        >>> rendu_dyn([4,3], 8)
        [4, 4]
        >>> rendu_dyn([4,3,1], 6)
        [3, 3]
        >>> rendu_dyn([4,3,1], 5)
        [4, 1]
    """

import doctest
doctest.testmod(optionflags=doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE, verbose = False)

Diviser pour régner : rotation d'une image d'un quart de tour en temps constant

👏👏👏 Bravo ! 👏👏👏