1. Rendu de monnaie

    1. Écrire une fonction nbr_pièces_dictmonnaie qui accepte un dictionnaire dont les clefs sont des entiers représentant des centimes et dont les valeurs sont des entiers représentant un nombre de pièce et qui retourne le nombre total de pièces. Ainsi l'appel nbr_pièces_dictmonnaie({1: 10, 4: 2}) doit retourner 12.
    2. Écrire une fonction total_dictmonnaie qui accepte un dictionnaire dont les clefs sont des entiers représentant des centimes et dont les valeurs sont des entiers représentant un nombre de pièce et qui retourne le montant total en centimes. Ainsi l'appel total_dictmonnaie({1: 10, 4: 2}) doit retourner 18.
    3. Écrire une fonction total_listmonnaie qui accepte une liste de couples d'entiers dont le premier élément représente des centimes et le second un nombre de pièce et qui retourne le montant total en centimes. Ainsi l'appel total_listmonnaie([(1, 10), (4, 2)]) doit retourner 18.
    4. Vaut-il mieux privilégier total_dictmonnaie ou total_listmonnaie?
  1. Écrire une fonction meme_rendus qui accepte deux dictionnaires dont les clefs sont des entiers représentant des centimes et dont les valeurs sont des entiers représentant un nombre de pièce et qui retourne True si les rendus associés sont les mêmes et False sinon. Ainsi
    • meme_rendus({1: 3}, {1: 3, 2: 0}) doit renvoyer True.
    • meme_rendus({1: 3}, {1: 4}) doit renvoyer False.
    • meme_rendus({1: 3}, {1: 3, 2: 1}) doit renvoyer False.
  2. On souhaite écrire une fonction rendu_monnaie_illimite

    • qui accepte deux arguments qui sont
      • un entier représentant un montant en centimes à rendre
      • et une liste d'entiers triés dans l'ordre strictement décroissant représentant les montants des pièces qui peuvent être utilisées pour le rendu (on supposera ici que l'on dispose d'autant de pièces de chaque type que nécessaire). À cette question on supposera que 1 fait partie de cette liste c'est à dire que l'on peut utiliser des pièces de 1 centimes pour le rendu de monnaie si bien que le rendu est toujours possible (quitte à n'utiliser que des pièces de 1 centimes)
    • et qui retourne un dictionnaire donnant pour chaque type de pièce (représenté par sa valeur) le nombre de pièces correspondantes à rendre ou bien lève une exception.

    Pour cela on utilise un algorithme dit glouton: on rend systématiquement la plus grosse pièce possible (c'est à dire inférieur à ce qu'il reste à rendre). Par exemple

    • rendu_monnai(6, [2, 1]) doit renvoyer {2: 3} mais ni {6: 1} ni {2: 4, 1:2}.
    • rendu_monnai(4, [3, 2]) échoue car il rend d'abord 3 puis ne peut plus rendre la monnaie.

    Écrire une telle fonction rendu_monnaie_illimite et vérifier qu'avec des pièces de valeurs 1, 3 et 4 l'algorithme glouton n'est pas toujours optimal en nombre de pièces utilisées pour le rendu.

  3. On souhaite écrire une fonction rendu_monnaie

    • qui accepte deux arguments qui sont
      • un entier représentant le montant en centimes à rendre;
      • et un dictionaire d'entiers qui à chaque montant de pièce existant associe le nombre (qui peut être nul) de pièces de ce type disponibles pour le rendu;
    • et qui retourne un dictionnaire donnant pour chaque type de pièce (toujours représenté par sa valeur) le nombre de pièces correspondantes à rendre si le rendu de monnaie est possible et None sinon.

    Pour cela on utilisera un algorithme récursif. Aide: pour le calcul de rendu_monnaie(S, dict) on pourra penser à rendre une pièce de monnaie d'un montant montant_piece puis voir si on peut maintenant rendre la monnaie sur S-montant_piece (avec une pièce de ce type en moins!)…

  4. Écrire une fonction rendu_monnaie_mini analogue à la précédente mais qui rend toujours la monnaie avec un nombre minimal de pièces…

2. Allocation de ressources

On dispose d'une salle de conférence qui est très demandé: beaucoup de demandes de créneaux sont faites la même journée, chacune étant représentée par un intervalle de temps \(\interfo{d_{i}}{f_{i}}\) avec \(d_{i}\lt{}f_{i}\). Malheureusement des demandes sont incompatibles c'est à dire qu'il est fort possible d'avoir \(\interfo{d_{i}}{f_{i}}\interfo{d_{j}}{f_{j}}\neq\emptyset\) même si \(i\neq{}j\). L'idée est de maximiser le nombre de conférences qui peuvent se tenir dans la journée. Pour cela on se propose d'étudier plusieurs algorithmes d'affectation (aussi appelés algorithmes d'allocations de ressources, la ressource à partager étant la salle de conférence).

Mathématiquement l'ensemble des demandes sera représenté par un ensemble \(\mathcal{D}=\set{\interfo{d_{1}}{f_{1}},\interfo{d_{2}}{f_{2}},\ldots,\interfo{d_{n}}{f_{n}}}\) qui sera en python représenté par une liste de couples de flottants (ou une liste de listes de longueur \(2\) de flottants).

  1. Un premier algorithme glouton auquel on peut penser est le suivant: on tri les demandes par heure de début de conférence (remarquons que cet ordre n'est pas unique car des demandes peuvent être distinctes et commencer à la même heure: la relation binaire utilisée n'est pas une relation d'ordre car elle n'est pas antisymétrique) puis on parcourt les demandes dans cet ordre

    • en commençant avec une liste de demandes retenues vide;
    • puis
      • si la demande courante n'est pas incompatible avec celles qui ont déjà été retenues alors on ajoute cette demande aux demandes retenues;
      • si la demande courante est incompatible avec une demande déjà retenue (c'est à dire avec cet algorithme avec la dernière demande retenue) alors on passe à la demande suivante.

    Remarquons qu'avec cet algorithme si une demande commence strictement avant toutes les autres alors elle sera nécessairement retenue (en premier). Ainsi avec les \(4\) demandes \(\interfo{0}{6}\), \(\interfo{1}{2}\), \(\interfo{3}{4}\), \(\interfo{4}{7}\) et \(\interfo{6}{7}\) représentées graphiquement ci-dessous

    demandes1.png

    l'algorithme

    • part d'une liste de demandes acceptées vide;
    • y ajoute \(\interfo{0}{6}\);
    • ignore dans l'ordre les demandes \(\interfo{1}{2}\), \(\interfo{3}{4}\), \(\interfo{4}{7}\) car elles sont incompatibles avec la demande acceptée \(\interfo{0}{6}\);
    • puis ajoute \(\interfo{6}{7}\);

    et on obtient donc l'ordonnancement suivant

    ordonnancement1.png

    qui n'est pas optimal car il est possible de planifier \(3>2\) conférences.

    1. Copier-coller le code ci-dessous et compléter la fonction teste_tri_liste_demandes_par_date_debut qui doit tester la fonction tri_liste_demandes_par_date_debut (qui doit évidemment avoir un fonctionnement conforme à sa documentation)

      def tri_liste_demandes_par_date_debut(L):
          """Fonction qui retourne une copie triée  la liste de demandes L triée
          par heure de début.
      
          Arguments:
            L: liste de couples (ou de listes de longueur 2) de valeurs
            numériques (entiers ou flottants)
          Retourne:
            une copie de L où les demandes sont triée par heure de début
          """
          if type(L) != tuple and type(L) != list:
              raise Exception("Les demandes doivent être représentées par un tuple/une liste")
          return sorted(L, key=lambda demande: demande[0])
      
      def teste_tri_liste_demandes_par_date_debut():
          assert ...
          assert ...
      
      teste_tri_liste_demandes_par_date_debut()
      
    2. Écrire une fonction deux_creneaux_sont_compat qui accepte deux couples de valeurs numériques et retourne True si elles sont compatibles et False sinon (sans hypotheses sur les dates de début ou de fin ou la longueur de ces demandes). Remarquons que deux créneaux sont compatible ssi l'un est tout entier avant ou tout entier après l'autre…
    3. En déduire une fonction liste_creneaux_tous_compat_avec_autre_creneau qui accepte une liste de creneaux et un creneau en arguments et qui retourne True si le creneau donné en argument est compatible avec tous les créneaux de la liste et False sinon.
    4. Écrire une fonction algo_glouton_min_heure_debut implémentant l'algorithme glouton d'affectation décrit ci-dessus (on utilisera les fonctions tri_liste_demandes_par_date_debut et deux_creneaux_sont_compat).
  2. On change d'algorithme d'affectation pour un autre algorithme glouton: au ileu de minimiser localement (c'est à dire à chaque étape) l'heure de début des demandes retenues on choisit de minimiser localement la longueur des demandes (càd parmi les demandes qui sont compatibles avec celles déjà retenues on choisit la demande \(\interfo{d_{i}}{f_{i}}\) minimisant \(f_{i}-d_{i}\)) tant que cela est possible. Ainsi avec les demandes

    demandes2.png

    l'algorithme

    • part d'une liste de demande acceptées vide;
    • y ajoute \(\interfo{0}{1}\);
    • y ajoute \(\interfo{3}{5}\).

    et on obtient donc l'ordonnancement non optimal suivant

    ordonnancement2.png
    1. Copier-coller le code ci-dessous et compléter la fonction teste_tri_liste_demandes_par_longueur qui doit tester la fonction tri_liste_demandes_par_longueur (qui doit évidemment avoir un fonctionnement conforme à sa documentation)

      def tri_liste_demandes_par_longueur(L):
          """Fonction qui retourne une copie triée  la liste de demandes L triée
          par longueurs croissantes.
      
          Arguments:
            L: liste de couples (ou de listes de longueur 2) de valeurs
            numériques (entiers ou flottants)
          Retourne:
            une copie de L où les demandes sont triée par heure de début
          """
          if type(L) != tuple and type(L) != list:
              raise Exception("Les demandes doivent être représentées par un tuple/une liste")
          return sorted(L, key=lambda demande: demande[1]-demande[0])
      
      def teste_tri_liste_demandes_par_longueur():
          L = [[2, 4], [1, 5], [0, 6]]
          res = ??
          assert tri_liste_demandes_par_longueur(L) == res
          L = [[2, 4], [1, 5], [0, 6]]
          res = ??
          assert tri_liste_demandes_par_longueur(L) == res
          L, res = [], []
          assert tri_liste_demandes_par_longueur(L) == res
      
      teste_tri_liste_demandes_par_longueur()
      
    2. Écrire une fonction algo_glouton_min_longueur_creneau implémentant l'algorithme glouton d'affectation décrit ci-dessus (on utilisera les fonctions tri_liste_demandes_par_longueur et deux_creneaux_sont_compat).
  3. De façon semblable, écrire maintenant un algorithme glouton algo_glouton_min_heure_fin qui minimise à chaque étape la date de fin de la conférence. Remarque importante: on peut montrer (mais ce n'est pas facile) que contrairement aux autres algorithmes gloutons ci-dessus cet algorithme glouton est toujours optimal en nombre de créneaux retenus!

3. Enveloppe convexe

Le but est d'écrire un algorithme qui accepte une liste L de (coordonnées de) points du plan (représentés à l'aide de couples de longueur 2) et retourne une liste de points de l'enveloppe convexe de ces points

4f96ad443f671d81d9ff84d3d8354a2a3cb4ce87.png

3.1. Vérification d'une enveloppe convexe

On souhaite vérifier qu'une liste de points (représentée par une liste de tuple de longueur 2) définit une enveloppe de points parcourue dans le sens trigonométrique. On admettra

  • que pour deux vecteurs \(\vcolnamed{\vec{u}}{u_{x},u_{y}}\) et \(\vcolnamed{\vec{v}}{v_{x},v_{y}}\) l'angle orienté \(\angle{\vec{u}}{\vec{v}}\) et le produit en croix \(u_{x}v_{y}-u_{y}v_{x}\) vérifient \(u_{x}v_{y}-u_{y}v_{x} =\norm{\vec{u}}\norm{\vec{v}}\sin{(\angle{\vec{u}}{\vec{v}})}\). En particulier si on choisit le représentant principal de l'angle c'est à dire \(\angle{\vec{u}}{\vec{v}}\in\interof{-\pi}{\pi}\) alors le signe de ce produit en croix permet de savoir si \(\angle{\vec{u}}{\vec{v}}\in\interof{0}{\pi}\) ou \(\angle{\vec{u}}{\vec{v}}\in\interof{-\pi}{0}\).
  • qu'une liste de points \((P_{1},\dots,P_{n})\) définit une enveloppe de points parcourue dans le sens trigonométrique si et seulement si pour tout \(1\leq{}i\leq{}n+1\) les points \(P_{i}\), \(P_{i+1}\) et \(P_{i+2}\) sont parcourus positivement
  1. Écrire une fonction ccw (pour counterclockwise: sens inverse des aiguilles d'une montre) acceptant trois tuples de longueur 2 représentant des points du plan et renvoyant True s'ils sont dans le sens trigonométrique et false sinon. Pour tester votre fonction vous vérifierez votre résultat sur les triplets de points de la liste triplet_points_dans_sens_trigo (qui sont tous dans le sens trigo) ci-dessous et sur les triplet obtenus en permutant les deux premiers ou les deux derniers points

    triplet_points_dans_sens_trigo = [
        [(0,1), (0,0), (1, 0)],
        [(0,2), (0,0), (1, 0)],
        [(0,3), (-5,-5), (2, 0)]
    ]
    print("ooo")
    
  2. Écrire une fonction is_positively_directed_convexe_hull acceptant un unique argument que l'on supposera être une liste de tuples de longueur 2 représentant des points du plan et qui retourne True si et seulement s'ils représentent une enveloppe convexe parcourue dans le sens trigonométrique.
  3. En utilisant le code ci-dessous

    import random
    
    def random_point():
        """Retourne un point au hasard et de façon uniforme parmi ceux dont
        les coordonnées sont comprises entre 0 et 1.
    
        Aucun argument.
    
        Retourne un tuple dez coordonées choisies indépendamment et
        uniformément entre 0 et 1.
        """
        x_coord = random.random(0, 1) 
        y_coord = random.random(0, 1)
        return (x_coord, y_coord)
    

    déterminer une valeur approchée de la probabilité que 4 points choisis au hasard uniformément entre 0 et 1 décrivent une enveloppe convexe parcourue dans le sens trigonométrique.

3.2. Une premiere méthode

L'idée est de construire l'enveloppe convexe parcourue dans le sens trigonométrique de façon itérative

  • on commence par y mettre le pivot \(P\)
  • si \(D\) est le dernier point ajouté à l'enveloppe convexe alors on ajoute le point \(Q\) de la liste de point dont l'angle \(\angle{\vec{OI}}{\vec{DQ}}\) pris dans \(\interfo{0}{2\pi}\) est minimal.
  1. Écrire l'algorihme correspondant
  2. Quelle est la complexité de cette algorithme en fonction du nombre de points initiaux (c'est à dire en fonction de la taille des entrées)?

    def indice_pivot(L):
        assert len(L) > 0
        indice_pt_with_min_ord = 0
        for i in range(len(L)):
            Q = L[i]
            if Q[1] < L[indice_pt_with_min_ord][1]:
                indice_pt_with_min_ord = i
        return indice_pt_with_min_ord
    
    pivot_test = [
        [[[1, 2], [3, 4],  [4, 3]],  0],
        [[[1, 2], [3, -4], [4, 3]],  1],
        [[[1, 2], [3, 4],  [4, -3]], 2]
        ]
    
    for L, ind_min in pivot_test:
        assert indice_pivot(L) == ind_min, "Erreur pivot"
    
    def convex_hull(Linit):
        convex_hull = []
        L = Linit.copy()
        P = pivot(L)
    
    

3.3. Parcourt de Graham

  1. Écrire une fonction pivot_de_graham qui accepte un unique argument à savoir notre liste de points et qui retourne un point \(P\) d'ordonnée minimale parmi ces points. Ce point sera appelé pivot dans la suite, toujours noté \(P\) et stoquée dans une variable globale (je sais, c'est mal…) de même nom P.
  2. Écrire une fonction coeff_dir qui accepte un tuple représentant un point \(A\) et qui retourne le coefficient directeur de la droite \((AP)\) où \(P\) est le pivot (on utilisera donc la variable globale P).
  3. L'algorithme de Graham pour la détermination d'une enveloppe convexe parcourue dans le sens trigonométrique procède ainsi:
    • On déterminer le pivot \(P\).
    • On trie les autres points par ordre croissant du coefficient directeur de la droite qu'ils forment avec le pivot et on met le point \(P\) en premier. Rappel: Si L est notre liste de points alors L.sort(key=coeff_dir) trie la liste L suivant la quantité renvoyée par coeff_dir.
    • puis on construit l'enveloppe convexe de façon itérative:
      • \(P\) et le premier point de la liste

Auteur: Aurélien Bosché

Created: 2023-12-08 Fri 20:03

Validate