Série 9 :
Structures.

Buts

Cette série a pour but de vous faire pratiquer les struct.

Préliminaires :

Avant d'effectuer les manipulations décrites dans cette série, créez le répertoire ~/Desktop/posixfs/cpp/serie09 (i.e. créez le sous-répertoire serie09 dans le répertoire ~/Desktop/posixfs/cpp).

Exercice 0 : reprise de l'exemple du cours (structures, niveau 0)

Cet exercice est disponible en page 44 de l'ouvrage C++ par la pratique
(page 45 dans la 2e édition).

Le but de cet exercice est de reprendre l'exemple du cours illustrant la gestion des structures.

Cliquez ici si vous souhaitez faire cet exercice.


Exercice 1 : Nombres complexes (structures, niveau 1)

Exercice n°22 (pages 60 et 225) de l'ouvrage C++ par la pratique.
Exercice n°19 du MOOC (semaine 6)

Le but de ce programme est d'effectuer les manipulations élémentaires sur les nombres complexes : addition, soustraction, multiplication et division.

Dans le fichier complexes.cc, définissez une structure Complexe représentant un nombre complexe comme deux double (forme cartésienne).

Ensuite, prototypez puis définissez une procédure affiche qui prend un nombre complexe en argument et l'affiche.

Dans le main(), déclarez et initialisez un nombre complexe. Affichez-le. Compilez et exécutez votre programme pour vérifier que tout fonctionne comme prévu jusqu'ici.

Prototypez puis définissez une fonction addition qui prend deux nombres complexes en argument et retourne leur somme.

Testez votre fonction dans le main().

Terminez l'exercice en écrivant puis testant les fonctions soustraction, multiplication et division.

Rappel : la multiplication de z=(x,y) par z'=(x',y') est le nombre complexe z*z'=(x*x'-y*y', x*y'+y*x').
la division de z=(x,y) par z'=(x',y') est le nombre complexe z/z'=((x*x'+y*y')/(x'*x'+y'*y'), (y*x'-x*y')/(x'*x'+y'*y')).

Exemple d'exécution

(1,0) + (0,1) = (1,1)
(0,1) * (0,1) = (-1,0)
(1,1) * (1,1) = (0,2)
(0,2) / (0,1) = (2,0)
(2,-3) / (1,1) = (-0.5,-2.5)

Exercice 2 : Questionnaire QCM (structures + vectors, niveau 2)

Exercice n°24 (pages 61 et 228) de l'ouvrage C++ par la pratique.
Exercice n°20 du MOOC (semaine 6)

On cherche ici à faire un programme d'examen sous forme de questionnaire à choix multiple (QCM) où une question est posée et la réponse est à choisir parmi un ensemble de réponses proposées (une seule bonne réponse possible par question).

Dans un programme qcm.cc, définissez une structure QCM comprenant 3 champs :

  1. un champ question, chaîne de caractères, qui contiendra la question à poser
  2. un champ reponses qui sera un tableau de taille variable de chaînes de caractères contenant les réponses proposées
    (c'est un tableau de taille variable car les questions n'auront pas toutes le même nombre de réponses proposées)
  3. un champ entier solution (entier positif) qui contient le numéro de la bonne réponse (dans le champ reponses).

Prototypez puis définissez une procédure affiche qui prend un QCM en argument et l'affiche. Par exemple :

Combien de dents possède un éléphant adulte ?
    1- 32
    2- de 6 à 10
    3- beaucoup
    4- 24
    5- 2

Dans le main(), créez et initialisez le QCM ci-dessus, puis affichez le. Compilez et vérifiez que tout fonctionne correctement jusqu'ici.

Reprenez la fonction demander_nombre (avec 2 arguments, point 4 de l'exercice 2 de la série 5) définie dans ../serie05/proto.cc, et créez une fonction poser_question qui prend un QCM en argument, appelle successivement affiche et demander_nombre et retourne la réponse de l'utilisateur.

Avant de continuer, testez votre programme (affichage de la question et saisie de la réponse).

On cherche maintenant à faire un examen de plusieurs questions. Définissez le type Examen comme un tableau dynamique de QCM. Créez un Examen dans le main(), puis remplissez-le (dans une fonction creer_examen c'est mieux !) avec les questions suivantes (code partiel) :

C++11 (version 98 plus bas)

    // Question 1
    { "Combien de dents possède un éléphant adulte",
      { "32", "de 6 à 10", "beaucoup", "24", "2" },
      2 // réponse
    },

    // Question 2
    { "Laquelle des instructions suivantes est un prototype de fonction",
      { "int f(0);"     ,
        "int f(int 0);" ,
        "int f(int i);" ,
        "int f(i);"     },
      3 // réponse
    },

    // Question 3
    { "Qui pose des questions stupides",
      { "le prof. de math",
        "mon copain/ma copine",
        "le prof. de physique",
        "moi",
        "le prof. d'info",
        "personne, il n'y a pas de question stupide",
        "les sondages" } ,
      6 // réponse
    }
    

C++98 (version C++11 ci-dessus)

q.question = "Combien de dents possède un éléphant adulte";
q.reponses.push_back("32");
q.reponses.push_back("de 6 à 10");
q.reponses.push_back("beaucoup");
q.reponses.push_back("24");
q.reponses.push_back("2");
q.solution=2; 

q.question = "Laquelle des instructions suivantes est un prototype de fonction";
q.reponses.push_back("int f(0);");
q.reponses.push_back("int f(int 0);");
q.reponses.push_back("int f(int i);");
q.reponses.push_back("int f(i);");
q.solution=3; 

q.question = "Qui pose des questions stupides";
q.reponses.push_back("le prof. de math");
q.reponses.push_back("mon copain/ma copine");
q.reponses.push_back("le prof. de physique");
q.reponses.push_back("moi");
q.reponses.push_back("le prof. d'info");
q.reponses.push_back("personne, il n'y a pas de question stupide");
q.reponses.push_back("les sondages");
q.solution=6; 

Pour terminer :

  1. Posez les questions une à une ;
  2. Comptez le nombre de bonnes réponses
  3. Donnez le score à la fin

Exemple d'exécution

Combien de dents possède un éléphant adulte ?
    1- 32
    2- de 6 à 10
    3- beaucoup
    4- 24
    5- 2
Entrez un nombre entier compris entre 1 et 5 : 2
Laquelle des instructions suivantes est un prototype de fonction ?
    1- int f(0);
    2- int f(int 0);
    3- int f(int i);
    4- int f(i);
Entrez un nombre entier compris entre 1 et 4 : 3
Qui pose des questions stupides ?
    1- le prof. de math
    2- mon copain/ma copine
    3- le prof. de physique
    4- moi
    5- le prof. d'info
    6- personne, il n'y a pas de question stupide
    7- les sondages
Entrez un nombre entier compris entre 1 et 7 : 5
Vous avez trouvé 2 bonnes réponses sur 3.

Exercice 3 : Nombres complexes revisités (structures, niveau 2)

Exercice n°25 (pages 63 et 230) de l'ouvrage C++ par la pratique.

On s'intéresse ici à la résolution d'équations du second degré sur le corps des complexes.

Copiez le programme complexes.cc dans le programme complexes2.cc et éditez ce dernier.

Écrivez la fonction Complexe sqrt(Complexe); qui calcule la racine carrée de partie réelle positive d'un nombre complexe.

On peut montrer que la racine carrée z'=(x',y') de partie réelle positive d'un nombre complexe z=(x,y) est donnée par :

Testez avant de continuer. Calculez par exemple la racine carrée de -1.

Déclarez le type Solutions comme une structure à 2 champs Complexes : z1 et z2.

Pour finir prototypez puis définissez la fonction resoudre_second_degre qui prend deux arguments Complexe b et c et retourne, sous forme de Solution, les solution de l'équation
z*z + b*z + c= 0

Exemples de solutions

Avec b=0 et c=1 on a :
  z1=-i
  z2=i

Avec b=3-2i et c=-5+i on a :
  z1=-4.11442+1.76499i
  z2=1.11442+0.235013i

Exercice 4 : Nombres rationnels (structures, niveau 1)

Exercice supplémentaire n°17 du MOOC (semaine 6)

Nous allons définir une structure Fraction, qui permettra de représenter des fractions, c'est-à-dire un numérateur et un dénominateur (entiers).

Nous voulons que les fractions soient toujours irréductibles, même après un calcul. Par exemple, le produit des fractions 425 et 152 devra donner la fraction 65, et non pas la fraction 6050. Pour cela, on pourra utiliser la fonction pgcd de l'exercice 6 de la série 5.

Ainsi, la fonction init_frac s'écrit (par exemple, il y a d'autres solutions) :

Fraction init_frac(int num, int den)
{
  const int div( pgcd( abs(num), abs(den) ) );

  if (num < 0 and den < 0) {
    num = -num;
    den = -den;
  }

  return { num / div , den / div };
}

Ecrivez les fonctions afficher_frac, add_frac, mult_frac, mult_scal_frac, dont le but est, respectivement, d'afficher une fraction, d'additioner 2 fractions, de multiplier 2 fractions et de multiplier une fraction par un scalaire.

En utilisant la fonction init_frac, les fonctions add_frac, mult_frac, mult_scal_frac peuvent s'écrire très simplement, sur une seule ligne.

Testez vos fonctions sur des exemples numériques.


Exercice 5 : LIEN COURS ICC : échantillonage de signal (niveau 2)

Je vous propose ici de coder l'échantillonnage d'un signal et sa reconstruction à partir des échantillons.

5.1 Signal = somme de sinusoides

Pour commencer, définissez un type Sinuoide qui regroupe les trois caractéristique d'une sinusoide : amplitude, fréquence et déphasage.

Faites ensuite une fonction sin_pure qui calcule la valeur en un point x d'une sinusoide telle que définie ci-dessus ; par exemple :

double sin_pure(double x, Sinusoide const& s = { 1.0, 1.0, 0.0 });

Définissez ensuite un type Signal qui regroupe plusieurs sinusoïde pures et une fonction signal qui calcule la valeur en un point x d'un tel signal, somme de sinusoïde ; par exemple :

double signal(double x, Signal const& s);

5.2 Signal échantilloné

Définissez maintenant un type Signal_Echant qui regroupe :

Définissez ensuite une fonction echantillonne qui à partir d'un signal, d'une fréquence d'échantillonnage, d'un temps initial et d'un temps final, construit (et retourne) le signal échantillonné. Son prototype pourrait par exemple être :

Signal_Echant echantillonne(Signal const& s, double freq,
                            double t_min = 0.0,
                            double t_max = 1.0);

Pour construire un tel signal échantillonné, il faut 1+size_t(freq * (t_max - t_min)) échantillons correspondant chacun au signal évalué au temps t_min + i / freq.

5.3 Reconstruction

Comme nous ne pouvons pas faire une somme infinie, la formule de reconstruction implémentée ici ne sera qu'une approximation de la formule théorique : nous allons simplement sommer sur tous les échantillons (elle sera donc plus fausse aux bords) :
la valeur reconstruite en un points x est la somme sur tous les échantilllons, chacun multiplié par le sinus cardinal de la fréquence d'échantiollanage fois x moins le temps initial, le tout moins i, le numéro d'échantillon:

sinc(freq_ech * (x - t_initial) - i)

Nous vons conseillons pour cela, bien sûr, d'écrire une fonction auxiliaire « sinus cardinal ». Afin d'éviter le problème du calcul en 0 : retourner la valeur 1 si l'argument est plus petit que, disons, 1e-8.

5.4 main()

Pour voir/expérimenter les effets du théorème d'échantillonnage, je vous propose dans le main() de :

0 0.189358 0.189358 0.189358
0.002 0.287194 0.23914 0.198669
0.004 0.383332 0.291892 0.208352
... etc.

Si vous ne savez pas comment faire tout ça, voici un exemple de main() possible :

int main()
{
  Signal s({
    { 1.0 , 2.0, 0.0 }, // première composante
    { 0.5 , 4.0, 0.1 }, // ...
    { 0.33, 6.0, 0.2 }, // ...
    { 0.25, 8.0, 0.3 }, // ...
   });
 
  constexpr double t_min(0.0);
  constexpr double t_max(2.0);
 
  Signal_Echant se1(echantillonne(s, 20.0, t_min, t_max));
  Signal_Echant se2(echantillonne(s, 11.0, t_min, t_max));
 
  // « dessin » des trois courbes
  constexpr size_t nb_points(1000);
  constexpr double dt((t_max-t_min) / nb_points);
 
  for (double t(t_min); t <= t_max; t += dt) {
    cout << t 
         << " " << signal(t, s)
         << " " << reconstruction(t, se1)
         << " " << reconstruction(t, se2)
      << endl;
  }
 
  return 0;
}

5.5 Déboguer

Pour vous aider, si nécessaire, à corriger votre programme, expliquons d'où vient la seconde ligne donnée ci-dessus :

0.002 0.287194 0.23914 0.198669

Ces quatre nombres représentent respectivement, le temps, le signal d'origine, le signal échantillonné à la première fréquence (20 Hz) puis reconstruit et le signal échantillonné à la seconde fréquence (11 Hz) puis reconstruit, provenant de la ligne du main() :

    cout << t 
         << " " << signal(t, s)
         << " " << reconstruction(t, se1)
         << " " << reconstruction(t, se2)
      << endl;

Le signal calculé ici est
sin(4πt)+0.5 sin(8πt+0.1)+0.33 sin(12πt+0.2)+0.25 sin(16πt+0.3),
ce qui, pour t=0.02 donne :
0.0251301 + 0.0748503 + 0.089737 + 0.0974768 = 0.287194.

Le signal échantilloné à 20 Hz puis reconstruit est calculé comme la somme des 41 termes suivants :

 0.189358  * sinc(20 * (0.002 - 0) -  0 ) =  0.189358  *  0.99737     =  0.18886
 1.44431   * sinc(20 * (0.002 - 0) -  1 ) =  1.44431   *  0.0415571   =  0.0600213
 0.75564   * sinc(20 * (0.002 - 0) -  2 ) =  0.75564   * -0.0203545   = -0.0153807
 0.731161  * sinc(20 * (0.002 - 0) -  3 ) =  0.731161  *  0.013478    =  0.00985457
 0.257756  * sinc(20 * (0.002 - 0) -  4 ) =  0.257756  * -0.0100744   = -0.00259675
 0.0582359 * sinc(20 * (0.002 - 0) -  5 ) =  0.0582359 *  0.00804331  =  0.000468409
-0.305928  * sinc(20 * (0.002 - 0) -  6 ) = -0.305928  * -0.00669376  =  0.00204781
-0.660188  * sinc(20 * (0.002 - 0) -  7 ) = -0.660188  *  0.00573201  = -0.0037842
-0.896827  * sinc(20 * (0.002 - 0) -  8 ) = -0.896827  * -0.00501191  =  0.00449481
-1.57352   * sinc(20 * (0.002 - 0) -  9 ) = -1.57352   *  0.00445255  = -0.00700616
 0.189358  * sinc(20 * (0.002 - 0) - 10 ) =  0.189358  * -0.0040055   = -0.000758473
 1.44431   * sinc(20 * (0.002 - 0) - 11 ) =  1.44431   *  0.00364004  =  0.00525734
 0.75564   * sinc(20 * (0.002 - 0) - 12 ) =  0.75564   * -0.00333569  = -0.00252058
 0.731161  * sinc(20 * (0.002 - 0) - 13 ) =  0.731161  *  0.0030783   =  0.00225073
 0.257756  * sinc(20 * (0.002 - 0) - 14 ) =  0.257756  * -0.00285779  = -0.000736615
 0.0582359 * sinc(20 * (0.002 - 0) - 15 ) =  0.0582359 *  0.00266677  =  0.000155301
-0.305928  * sinc(20 * (0.002 - 0) - 16 ) = -0.305928  * -0.00249967  =  0.00076472
-0.660188  * sinc(20 * (0.002 - 0) - 17 ) = -0.660188  *  0.00235229  = -0.00155295
-0.896827  * sinc(20 * (0.002 - 0) - 18 ) = -0.896827  * -0.00222131  =  0.00199213
-1.57352   * sinc(20 * (0.002 - 0) - 19 ) = -1.57352   *  0.00210416  = -0.00331093
 0.189358  * sinc(20 * (0.002 - 0) - 20 ) =  0.189358  * -0.00199874  = -0.000378476
 1.44431   * sinc(20 * (0.002 - 0) - 21 ) =  1.44431   *  0.00190338  =  0.00274907
 0.75564   * sinc(20 * (0.002 - 0) - 22 ) =  0.75564   * -0.0018167   = -0.00137277
 0.731161  * sinc(20 * (0.002 - 0) - 23 ) =  0.731161  *  0.00173758  =  0.00127045
 0.257756  * sinc(20 * (0.002 - 0) - 24 ) =  0.257756  * -0.00166506  = -0.000429179
 0.0582359 * sinc(20 * (0.002 - 0) - 25 ) =  0.0582359 *  0.00159835  =  9.30813e-05
-0.305928  * sinc(20 * (0.002 - 0) - 26 ) = -0.305928  * -0.00153678  =  0.000470144
-0.660188  * sinc(20 * (0.002 - 0) - 27 ) = -0.660188  *  0.00147978  = -0.000976931
-0.896827  * sinc(20 * (0.002 - 0) - 28 ) = -0.896827  * -0.00142685  =  0.00127964
-1.57352   * sinc(20 * (0.002 - 0) - 29 ) = -1.57352   *  0.00137758  = -0.00216765
 0.189358  * sinc(20 * (0.002 - 0) - 30 ) =  0.189358  * -0.0013316   = -0.000252149
 1.44431   * sinc(20 * (0.002 - 0) - 31 ) =  1.44431   *  0.00128859  =  0.00186113
 0.75564   * sinc(20 * (0.002 - 0) - 32 ) =  0.75564   * -0.00124827  = -0.000943246
 0.731161  * sinc(20 * (0.002 - 0) - 33 ) =  0.731161  *  0.0012104   =  0.000884998
 0.257756  * sinc(20 * (0.002 - 0) - 34 ) =  0.257756  * -0.00117476  = -0.000302802
 0.0582359 * sinc(20 * (0.002 - 0) - 35 ) =  0.0582359 *  0.00114116  =  6.64562e-05
-0.305928  * sinc(20 * (0.002 - 0) - 36 ) = -0.305928  * -0.00110942  =  0.000339403
-0.660188  * sinc(20 * (0.002 - 0) - 37 ) = -0.660188  *  0.0010794   = -0.00071261
-0.896827  * sinc(20 * (0.002 - 0) - 38 ) = -0.896827  * -0.00105097  =  0.000942538
-1.57352   * sinc(20 * (0.002 - 0) - 39 ) = -1.57352   *  0.00102399  = -0.00161127
 0.189358  * sinc(20 * (0.002 - 0) - 40 ) =  0.189358  * -0.000998369 = -0.000189049

ce qui donne la valeur 0.23914.

Le signal échantilloné à 11 Hz puis reconstruit est calculé comme la somme des 23 termes suivants :

 0.189358  * sinc(11 * (0.002 - 0) -  0 ) =  0.189358  *  0.999204   =  0.189207
 0.851989  * sinc(11 * (0.002 - 0) -  1 ) =  0.851989  *  0.022477   =  0.0191502
 0.482616  * sinc(11 * (0.002 - 0) -  2 ) =  0.482616  * -0.0111135  = -0.00536355
-0.0101872 * sinc(11 * (0.002 - 0) -  3 ) = -0.0101872 *  0.00738163 = -7.51984e-05
-0.643095  * sinc(11 * (0.002 - 0) -  4 ) = -0.643095  * -0.00552602 =  0.00355375
-1.53079   * sinc(11 * (0.002 - 0) -  5 ) = -1.53079   *  0.00441593 = -0.00675985
 1.45684   * sinc(11 * (0.002 - 0) -  6 ) =  1.45684   * -0.00367723 = -0.00535713
 0.72608   * sinc(11 * (0.002 - 0) -  7 ) =  0.72608   *  0.00315026 =  0.00228734
 0.0696877 * sinc(11 * (0.002 - 0) -  8 ) =  0.0696877 * -0.00275539 = -0.000192017
-0.528292  * sinc(11 * (0.002 - 0) -  9 ) = -0.528292  *  0.00244848 = -0.00129351
-1.06421   * sinc(11 * (0.002 - 0) - 10 ) = -1.06421   * -0.0022031  =  0.00234455
 0.189358  * sinc(11 * (0.002 - 0) - 11 ) =  0.189358  *  0.00200241 =  0.000379172
 0.851989  * sinc(11 * (0.002 - 0) - 12 ) =  0.851989  * -0.00183524 = -0.0015636
 0.482616  * sinc(11 * (0.002 - 0) - 13 ) =  0.482616  *  0.00169383 =  0.000817468
-0.0101872 * sinc(11 * (0.002 - 0) - 14 ) = -0.0101872 * -0.00157265 =  1.60209e-05
-0.643095  * sinc(11 * (0.002 - 0) - 15 ) = -0.643095  *  0.00146765 = -0.00094384
-1.53079   * sinc(11 * (0.002 - 0) - 16 ) = -1.53079   * -0.0013758  =  0.00210605
 1.45684   * sinc(11 * (0.002 - 0) - 17 ) =  1.45684   *  0.00129476 =  0.00188626
 0.72608   * sinc(11 * (0.002 - 0) - 18 ) =  0.72608   * -0.00122274 = -0.000887809
 0.0696877 * sinc(11 * (0.002 - 0) - 19 ) =  0.0696877 *  0.00115831 =  8.07203e-05
-0.528292  * sinc(11 * (0.002 - 0) - 20 ) = -0.528292  * -0.00110033 =  0.000581298
-1.06421   * sinc(11 * (0.002 - 0) - 21 ) = -1.06421   *  0.00104788 = -0.00111516
 0.189358  * sinc(11 * (0.002 - 0) - 22 ) =  0.189358  * -0.0010002  = -0.000189396

ce qui donne la valeur 0.198669.

5.6 Dessiner les résultats

Le plus simple pour visualiser ce que votre programme a sorti est d'utiliser un outil de dessin externe, comme par exemple gnuplot, qui est un programme (à lancer depuis la ligne de commande) de dessin de données et de fonctions.
Vous pouvez aussi, naturellement, utiliser n'importe quel autre logiciel capable de dessiner des courbes à partir de points, comme des tableurs (LibreOffice, ou autres) ou Matplotlib, Octave, etc.

gnuplot lit des fichiers dont chaque ligne contient un point à dessiner, les coordonnées étant par colonnes, exactement comme le sort votre programme. Pour que la sortie de votre programme soit stockée dans un fichier, il suffit de le lancer depuis un terminal et « rediriger sa sortie » dans un fichier avec le signe >. Cela se fait simplement comme ceci:

./signal > data.txt

C'est aussi simple que cela ! Avec ça, vous devriez avoir, dans le répertoire courant, un fichier data.txt qui contient :

0 0.189358 0.189358 0.189358
0.002 0.287194 0.23914 0.198669
0.004 0.383332 0.291892 0.208352
0.006 0.477329 0.347325 0.218412
0.008 0.568761 0.405119 0.228854
0.01 0.657223 0.464923 0.23968
... etc.

Une fois que vous avez un tel fichier (bon pour gnuplot), il suffit de lancer la commande gnuplot dans le terminal, puis, dans gnuplot, de taper

set style data line
plot "data.txt" u 1:2 t "signal", "data.txt" u 1:3 t "fe 20",
     "data.txt" u 1:4 t "fe 10"

et vous devriez alors voir ceci (pour le main() donné plus haut) :

NOTE : les écarts aux bords (en début et fin) entre le signal d'origine et le premier signal reconstruit (correctement échantilloné) ne sont simplement du qu'au fait que nous ne faisons pas une somme infinie mais simplement une somme finie dans la formule de reconstruction. Il faut donc regarder « au milieu » pour voir que la reconstruction est assez bonne (parfaite en théorie avec sommes infinies).

Pour quitter gnuplot : simplement quit ou 'Control-D'.

Annexe : explication des commandes gnuplot utilisées

Le « u 1:3 » signifie que l'on utilise la 1àre et la 3e colonne pour dessiner. Sinon gnuplot utilise les deux premières (i.e. il fait un « u 1:2 »).

Le « t "..." » est pour donner un titre à une courbe (légende sur le coté en haut à droite).

La commande « set style data line » demande à gnuplot d'utiliser simplement des lignes pour dessiner les données; sinon par défaut gnuplot utilise des « points » (petits symoles, en fait) pour dessiner les fichiers de données.


Exercice 6 : LIEN COURS ICC : problème du sac à dos (structures, tableaux, fonctions, niveau 2)

Cadre général

Le problème dit « du sac à dos » consiste à trouver, parmi un ensemble initial d'objets ayant chacun un poids et une valeur, quels objets mettre dans le sac de sorte que celui-ci ai une valeur maximale, mais ne pèse pas plus qu'un poids fixé au départ.

Par exemple, on chercher à remplir au mieux (valeur maximale) le sac à dos à partir des objets suivants, mais sans dépasser un poids total de 9 kg :

A noter que c'est bien la contrainte du poids maximal à ne pas dépasser qui rend ce problème « intéressant ». Sans une telle contrainte, il suffirait de tout mettre dans le sac pour maximiser sa valeur !

Ce problème « du sac à dos » est un problème NP-complet, c.-à-d. qu'à ce jour on ne connaît pas de meilleur algorithme pour le résoudre que celui qui consiste à essayer toutes les solutions.

Face à ce genre de problèmes, on peut alors chercher à ne pas le résoudre de façon optimale, mais utiliser un algorithme plus efficace que la recherche exhaustive, mais qui ne donnera qu'une solution approximative (une bonne solution mais qui n'est pas garantie pour être la meilleure).

Par exemple, un algorithme simple non optimal consiste à simplement remplir le sac avec les objets dans l'ordre dans lequel ils sont donnés/posés sur la table, si c'est possible (c.-à-d. ajouter l'objet ne fait pas dépasser le poids maximal autorisé).

Un autre algorithme simple non optimal consiste remplir le sac en mettant l'objet le plus lourd en premier si c'est possible, puis ensuite le 2e objet le plus lourd si c'est possible, etc. C'est le même algorithme que le précédent mais où l'on a au préalable trié les objets par poids décroissant. On appelle cet algorithme, un algorithme « glouton » car il « veut » consommer le plus en premier.

Si l'on applique le premier algorithme à l'exemple ci-dessus :

Au final, on arrive avec un poids total = 6 kg et une valeur totale de 11.

Si l'on applique l'algorithme glouton :

Au final, on arrive avec un poids total = 8 kg et une valeur totale de 13, ce qui est déjà mieux.

Mais la solution optimale consiste ici à prendre la boîte et le paquet. Le poids total est de 9 kg et la valeur totale de 15.

Mise en pratique

Le but est de programmer ces trois algorithmes. Commencez pour cela par représenter la liste des objets possibles, par exemple comme un ensemble de paires de valeurs.

Implémentez ensuite la recherche naïve (premier algorithme) dans une fonction qui prend en paramètres une liste d'objets et un poids maximal. Cette fonction affichera la valeur maximale trouvée et le poids restant libre dans le sac (avec notre exemple, elle afficherait : « valeur : 11, poids restant : 3 kg »).

Implémenez ensuite l'algorithme glouton en :

  1. triant l'ensemble d'objets par valeurs décroissantes ;
  2. appliquant la fonction précédente.

La partie la plus difficile est la recherche exacte. Celle-ci se fera de façon récursive en considérant le meilleur résultat des deux solutions consistant à prendre ou non le premier objet de la liste puis rechercher sur la suite de la liste des objets.
Plus précisément : écrire une fonction (récursive) qui prend une liste d'objets, un index de début et un poids maximum et qui

  1. si la liste est vide retourne 0 (valeur d'une liste d'objets vide) ;
  2. sinon, lance la recherche sur la liste privée du premier élément (c.-à-d. en passant simplement l'index de départ à un de plus que celui reçu) et mémorise la valeur obtenue (appelons-la « valeur 1 ») ;
  3. puis, si le poids du 1er objet est inférieur au poids maximum possible :

Exercice 7 : LIEN COURS ICC : codes de Huffman (structures, tableaux, fonctions, ..., niveau 3)

Le but de cet exercice est de réaliser des codes de Huffman de textes. Le principe du code de Huffman (binaire) est assez simple (cf cours ICC): il consiste à regrouper les deux « entités » les moins probables à chaque étape; une « entité » étant soit une lettre de départ, soit un groupe d'entités déjà traitées (une, deux, trois, ... lettres déjà traitées).

Le plus simple est peut être de commencer directement par l'algorithme lui-même à un niveau d'abstraction assez haut (approche descendante) :
à partir de la distribution de probabilité initiale des lettres (leur compte), on va, de proche en proche tant qu'il nous reste plus que deux « entités » à traiter

  1. rechercher les deux entités les moins probables ;

  2. ajouter le symbole '0' devant tous les codes déjà produits pour la première de ces deux entités ;

  3. ajouter le symbole '1' devant tous les codes déjà produits pour la seconde de ces deux entités ;

  4. fusionner ces deux entités en une nouvelle entité, dont on calcule la probabilité : somme des probabilités des deux entités fusionnées;

    pour cela on écrasera une de ces deux entités par leur fusion et on supprimera l'autre.

    Notez qu'on a donc une entité de moins à chaque fois (la fusion supprime les deux entités, mais n'en rajoute qu'une seule nouvelle).

Il nous faut donc représenter ces « entités » (et un ensemble d'entités). Je vous propose de le faire de la façon suivante :

Nous aurons aussi besoin de représenter le code lui-même. Un code est une bijection entre les mots à coder et leur code. La bonne structure de données pour représenter cela est ce que l'on appelle des « tables associatives », que nous n'avons pas encore vues (2e semestre). A ce stade du cours, nous vous proposons donc simplement de représenter le code comme un tableau dynamique de structures contenant:

Prenons un exemple (cf cours ICC) :
supposons que l'on veuille coder la phrase « JE PARS A PARIS »

On commencera par créer un « code » (il n'est pas complet à ce stade, tous ses mots sont vides) contenant par exemple (ordre de lecture ici, mais vous pouvez changer cet ordre, bien sûr):

Une fois ce « code » (mots vides) créé, on pourra créer la version initiale de la liste d'« entités » nécessaires pour l'algorithme décrit plus haut (ordonnée à nouveau ici dans l'ordre de lecture, mais vous pouvez choisir un autre ordre):

Pour l'instant cette liste d'« entités » ne semble pas très intéressante et semble faire double emploi avec le code : c'est normal : le code de Huffman commence par les lettres seules.

Mais dès la première étape de codage, cela va changer : on commence par regrouper les deux moins probables, disons par exemple le "J" et le "E" (les deux premiers moins probables dans l'ordre de lecture, là encore votre choix peut être différent suivant comment vous écrivez l'algorithme, cela ne change rien au final sur la longueur moyenne du code). On aura alors une nouvelle entité :
indices: 0, 1, probabilité : 2./12.;
et la liste d'« entités » devient (elle ne contient plus que 7 éléments):

Et comme dit au départ de cet exercice : une fois la fusion ci-dessus effectuée (ou même avant), on ajoutera respectivement '0' et '1' à chacun des mots de code correspondants. Dans cet exemple précis, cela à revient à les ajouter à chacun des deux mots de code, vides, de "J" et "E".

Voilà pour les structures de données proposées pour cet algorithme.

Pour l'algorithme lui-même : adoptez (bien sûr !) une démarche très modulaire en introduisant des fonctions pour chacune des tâches élémentaires ; par exemple : compter les lettres, normaliser les probabilités, fusionner 2 entités, rechercher les deux entités les moins probables, ajouter un symbole à un mot de code, construire les entités initiales à partir d'un « code » (mots vides), etc. etc.


Exercice 8 : LIEN COURS ICC : Machine de Turing (programmation, niveau 3)

Cet exercice n'est pas présent dans l'ouvrage C++ par la pratique

Le but est d'écrire un programme turing.cc permettant de simuler une machine de Turing.

Si vous n'avez aucune idée sur la manière de procéder, vous pouvez utiliser la modélisation proposée ci-dessous, sinon allez y puis reprenez l'exercice ici.

Proposition de méthode :

  1. Pour simuler une bande de longueur infinie, vous pouvez utiliser une string  (définir par exemple un type Bande) :
  2. La tête de lecture sera représentée simplement par un entier, indiquant sa position courante. Je vous conseille aussi de définir un type ici, par exemple Tete.
  3. Créez ensuite les fonctions implémentant les primitives de lecture, écriture et positionnement de la tête de lecture :
    char lecture(Bande& b, Tete& t); lit le caractère 'courant'
    void ecriture(char c, Bande& b, Tete& t); remplace le caractère courant par la valeur transmise en argument
    void avance(Tete& t); déplace la tête de lecture vers la droite
    void recule(Tete& t); déplace la tête de lecture vers la gauche

    Notez que, dans l'implémentation proposée, lecture et ecriture peuvent changer la valeur de tete : comme indiqué au point 1. ci-dessus, l'aspect infini de la bande est simulé en insérant le bon nombre d'epsilons en début de bande lorsque la position de la tête devient négative. Il faut alors naturellement repositionner la tête à la valeur zéro lorsque cela a été fait, et donc modifier sa valeur. On aurait également pu choisir d'effectuer ces opérations (simuler une bande infinie) lors des déplacements de la tête plutôt que lors de la lecture/écriture...(libre à vous)

  4. Définissez une fonction initialise(Bande& b, string valeur) permettant d'initialiser la bande au moyen d'une chaîne de caractères
  5. Pour représenter la table de transitions, utilisez un tableau dynamique :
    struct Transition
    {
      Etat etat;
      char caractere;
      int deplacement;
    }
    typedef array Ligne; // 3 : caractères  0 , 1 , epsilon 
    

    Avec une telle représentation, une table de transitions sera simplement un tableau de Lignes.

  6. Écrivez une structure TMachine, simulant une machine de Turing.

    Vous aurez besoin des champs suivants :

  7. Définissez les fonctions suivantes :

Application 1: Test de Parité

Testez votre machine avec l'exemple du cours testant la parité de l'entrée.

Avec la modélisation proposée, utilisez la syntaxe suivante pour définir une table de transition (dans la fonction cree :

  TableTransition table = {
    { {    1,     '0', +1}, {    1,     '1', +1}, {    2, EPSILON, -1} },
    { {    3, EPSILON, -1}, {    4, EPSILON, -1}, {UNDEF, EPSILON, -1} },
    { {    3, EPSILON, -1}, {    3, EPSILON, -1}, {    5,     '1', +1} },
    { {    4, EPSILON, -1}, {    4, EPSILON, -1}, {    5,     '0', +1} },
    { {UNDEF, EPSILON, -1}, {UNDEF, EPSILON, -1}, {UNDEF, EPSILON, -1} }
  };

Note : il se peut qu'avec la version actuelle (4.6) du compilateur C++11 vous ayez à écrire :

  TableTransition table = {
    {{ {    1,     '0', +1}, {    1,     '1', +1}, {    2, EPSILON, -1} }},
    {{ {    3, EPSILON, -1}, {    4, EPSILON, -1}, {UNDEF, EPSILON, -1} }},
    {{ {    3, EPSILON, -1}, {    3, EPSILON, -1}, {    5,     '1', +1} }},
    {{ {    4, EPSILON, -1}, {    4, EPSILON, -1}, {    5,     '0', +1} }},
    {{ {UNDEF, EPSILON, -1}, {UNDEF, EPSILON, -1}, {UNDEF, EPSILON, -1} }}
  };

là où l'écriture précédente aurait du fonctionner...

La constante UNDEF représente un état non défini également utilisé pour "l'ordre de fin d'exécution".

Dans cet exemple, on suppose que le déplacement vers l'avant est représenté par la valeur +1, et le déplacement vers l'arrière par -1.

Exemple (détaillé) d'execution :

Lancement de la machine de Turing
    --------------------------------
    Etat actuel : 1
    Bande :
       10
       ^ (Tete : 0)
    --------------------------------
 lu : 1, nouvel état : 1, écrit : 1, avance
    --------------------------------
    Etat actuel : 1
    Bande :
       10
        ^ (Tete : 1)
    --------------------------------
 lu : 0, nouvel état : 1, écrit : 0, avance
    --------------------------------
    Etat actuel : 1
    Bande :
       10
         ^ (Tete : 2)
    --------------------------------
 lu : e, nouvel état : 2, écrit : e, recule
    --------------------------------
    Etat actuel : 2
    Bande :
       10e
        ^ (Tete : 1)
    --------------------------------
 lu : 0, nouvel état : 3, écrit : e, recule
    --------------------------------
    Etat actuel : 3
    Bande :
       1ee
       ^ (Tete : 0)
    --------------------------------
 lu : 1, nouvel état : 3, écrit : e, recule
    --------------------------------
    Etat actuel : 3
    Bande :
       eee
      ^ (Tete : -1)
    --------------------------------
 lu : e, nouvel état : 5, écrit : 1, avance
    --------------------------------
    Etat actuel : 5
    Bande :
       1eee
        ^ (Tete : 1)
    --------------------------------
 lu : e, nouvel état : 0, écrit : e, recule
    --------------------------------
    Etat actuel : 0
    Bande :
       1eee
       ^ (Tete : 0)
    --------------------------------
Arrêt de la machine de Turing.

Application 2: Inversion de séquence sur une Machine de Turing

Écrivez (sur papier) la table de transition d'une machine de Turing réalisant l'inversion d'une séquence de bits.

Par exemple, si l'on a 1101 en entrée, on devra avoir 1011 en sortie.

On supposera que:

Remarquez bien qu'il n'est pas nécessaire que la séquence inversée occupe, sur la bande, la même position que la séquence initiale.

Testez votre machine sur un exemple simple (du moins dans un premier temps) !

Indications: si vous êtes en panne d'inspiration, vous pouvez essayer l'algorithme proposé ci-après.

  1. Si la séquence d'entrée est vide, terminer. Sinon, aller jusqu'à la cellule immédiatement à droite de la frontière droite de la séquence, en mémorisant (dans la valeur des états : voir l'algorithme de parité ci-dessus) la valeur du dernier élément de la séquence.

  2. Si la (nouvelle) séquence n'est pas vide, aller tout d'abord à sa frontière droite (toujours en mémorisant le dernier élément), puis écrire l'élément mémorisé (qui est alors le dernier élément de la nouvelle séquence), se déplacer à la fin de la séquence d'entrée courante, effacer son dernier élément et se déplacer à gauche. (Si la séquence n'est pas vide, on est alors de nouveau sur le dernier élément de la séquence d'entrée 'courante')

  3. Si la séquence d'entrée courante est vide, aller à la frontière gauche de la nouvelle séquence et terminer. Sinon, aller au début de la nouvelle séquence en mémorisant le dernier caractère de la séquence d'entrée courante, puis recommencer en (2)


Dernière mise à jour le 17 novembre 2016
Last modified: Thu Nov 17, 2016